programación - objective-c++
Perfil de Objective-C tamaño de la imagen binaria (2)
Estoy buscando herramientas y enfoques para determinar qué partes de mis programas Cocoa y Cocoa-Touch son las que más contribuyen al tamaño final de la imagen binaria y las formas de reducirlo. No estoy buscando una bandera de compilador "bala mágica". Estoy buscando técnicas de creación de perfiles para evaluar y reducir el desperdicio de tamaño de imagen en la misma línea que la ayuda de Shark and Instruments para la evaluación en tiempo de ejecución.
Una aproximación de primer orden puede ser el tamaño de los .o, pero ¿qué tan confiable es esto en términos del tamaño final de la imagen después de las optimizaciones y el borrado de código muerto? Si sumo todos los .o, son mucho más grandes que mi imagen final, así que claramente el enlazador ya me está ayudando significativamente. Pero esto significa que el tamaño de los .o puede no ser una medida útil.
¿Dónde buscan otros reducir el tamaño de la imagen sin socavar la capacidad de mantenimiento del código?
Apple tiene algunos documentos impresionantes sobre las Pautas de rendimiento de tamaño de código , casi todos los cuales se aplican a esta pregunta de alguna forma. Incluso hay consejos para los enfoques pedantes, como ordenar manualmente los símbolos en el binario, si así lo desea. :-)
Soy un fan de los códigos simples y delgados, y de minimizar el espacio en disco / memoria. La optimización prematura siempre es una mala idea, pero una limpieza constante puede ser una buena manera de evitar que se acumule el cruft. Desafortunadamente, no conozco una forma automatizada de perfilar los tamaños de código, pero existen varias herramientas que pueden ayudar a proporcionar una visión específica.
Tamaño de imagen binaria
Los archivos de objetos no son una aproximación tan terrible como supongo. Una razón por la que la suma es más pequeña que las partes es porque el código está unido con un solo encabezado. Aunque los porcentajes no serán precisos, los archivos de objetos más grandes son las partes más grandes del binario vinculado.
Para comprender la longitud en bruto de cada método en particular en un archivo de objeto, puede usar /usr/bin/otool
para imprimir el código de ensamblaje, puntuado por los nombres de los métodos de Objective-C:
$ otool -tV MyClass.o
Busco largos tramos de ensamblaje que corresponden a métodos relativamente cortos o simples y examino si el código se puede simplificar o eliminar por completo.
Además de otool
, he encontrado que /usr/bin/size
puede ser bastante útil, ya que divide segmentos y secciones jerárquicamente y te muestra el tamaño de cada uno, tanto para archivos de objetos como binarios compilados. Por ejemplo:
$ size -m -s __TEXT __text MyClass.o
$ size -m /Applications/iCal.app/Contents/MacOS/iCal
Esta es una vista de "imagen más grande", aunque generalmente refuerza que __TEXT __text
es a menudo uno de los más grandes en el archivo y, por lo tanto, es un buen lugar para comenzar la poda.
Identificación de código muerto
Nadie realmente quiere que su binario esté lleno de código que nunca se usa. En un lenguaje dinámico y poco acoplado como Objective-C, puede ser difícil o imposible determinar estáticamente si un código específico se "usa" o no. Incluso si se crea una instancia de una clase o se llama a un método, el rastreo de las rutas de código (tanto teóricas como reales) puede ser un dolor de cabeza. Utilizo algunos trucos para ayudar con esto.
- Para el análisis estático, recomiendo encarecidamente el analizador estático Clang (que está integrado en Xcode 3.2 en Snow Leopard). Entre todas sus otras virtudes, esta herramienta puede rastrear rutas de código e identificar trozos de código que no pueden ejecutarse, y deben eliminarse o el código circundante debe fijarse para que pueda llamarse.
- Para el análisis dinámico, uso
gcov
(con pruebas unitarias) para identificar qué código se ejecuta realmente . Los informes de cobertura (leídos con algo como CoverStory ) revelan un código no ejecutado que, junto con el examen y las pruebas manuales, pueden ayudar a identificar el código que puede estar muerto. Tiene que modificar algunos ajustes y ejecutar gcov manualmente en sus binarios. Usé esta entrada de blog para empezar.
En la práctica, es poco común que el código muerto sea una proporción suficientemente grande del código para hacer una diferencia sustancial en el tamaño binario o el tiempo de carga, pero el código muerto ciertamente complica el mantenimiento, y es mejor deshacerse de él si es posible.
Visibilidad del símbolo
Reducir la visibilidad de los símbolos puede parecer una recomendación extraña, pero facilita mucho las cosas para dyld
(el vinculador que carga los programas en tiempo de ejecución) y permite que el compilador realice mejores optimizaciones. Considere la posibilidad de ocultar variables globales (que no están declaradas como static
), etc., colocándolas como prefijo con un atributo "oculto" o habilitando "Símbolos ocultos por defecto" en Xcode y haciendo que los símbolos sean visibles de manera explícita. Yo uso las siguientes macros:
#define HIDDEN __attribute__((visibility("hidden")))
#define VISIBLE __attribute__((visibility("default")))
Considero que /usr/bin/nm
invaluable para identificar símbolos innecesariamente visibles, y para identificar posibles dependencias externas que quizás no conozca o que no haya considerado.
$ nm -m -s __TEXT __text MyClass.o # -s displays only a given section
$ nm -m -p MyClass.o # -p preserves symbol table ordering (no sort)
$ nm -m -u MyClass.o # -u displays only undefined symbols
Si bien es poco probable que la reducción de la visibilidad de los símbolos reduzca directamente el tamaño de su binario, el compilador puede realizar mejoras que de otro modo no podría hacer. Además, puede reducir las dependencias accidentales de los símbolos que no pretende exponer.
Analizando las dependencias de la biblioteca y cargando
Además del tamaño binario sin formato, a menudo puede ser muy útil analizar las bibliotecas dinámicas a las que se vincula y eliminar las que podrían ser innecesarias, especialmente los marcos de uso menos común que aún no se han cargado. (También puedes ver esto desde Xcode, pero con proyectos complejos, a veces las cosas se resbalan, por lo que también es útil para un control de cordura después de la construcción). De nuevo, otool al rescate ...
$ otool -L MyClass.o
Otra alternativa (extremadamente detallada) es tener las bibliotecas cargadas de impresión dyld
, así (desde Terminal):
$ export DYLD_PRINT_LIBRARIES=1
$ /Applications/iCal.app/Contents/MacOS/iCal
Esto muestra exactamente lo que se está cargando, incluidas las dependencias de las bibliotecas a las que se vincula su código.
Analizando el rendimiento de lanzamiento
Por lo general, lo que realmente le importa es si el tamaño del código y las dependencias de la biblioteca realmente afectan el tiempo de inicio. Establecer esta variable de entorno hará que dyld
informe las estadísticas de carga, lo que realmente puede ayudar a determinar cómo se gastó el tiempo en la carga:
$ export DYLD_PRINT_STATISTICS=1
$ /Applications/iCal.app/Contents/MacOS/iCal
En Leopard y más adelante, notará entradas sobre " caché compartida dyld ". Básicamente, el enlazador dinámico crea una "súper biblioteca" consolidada compuesta de las bibliotecas dinámicas más utilizadas. Se menciona en esta documentación de Apple y el comportamiento se puede modificar con las variables de entorno DYLD_SHARED_REGION
y DYLD_NO_FIX_PREBINDING
, similares a las anteriores. Vea dyld para más detalles.
Es posible que desee mirar otool. Específicamente, es probable que desee usar el indicador -l que muestra todos los comandos de carga (también conocidos como secciones y segmentos) que forman su binario.
Habiendo dicho todo esto, por lo general, encontrará que los recursos son más importantes que el código que escribe, por lo que me pregunto qué problema ha encontrado que está tratando de resolver. Nuestras aplicaciones tienen un poco de código, pero aún son solo unos pocos MB. Tal vez esté enlazando estáticamente con algunas bibliotecas grandes, no lo sé.
Si la mayoría de su código es Objective-C, se eliminará muy poco con la eliminación del código muerto (por razones obvias), por lo que no habrá mucha diferencia.
Lo que hará una diferencia es la información de depuración que será sustancial. Sus archivos de objetos incluirán esto, pero normalmente lo tendrá almacenado en un paquete dSYM separado cuando lo vincule, por lo que no se incluirá en el binario final (o al menos esto es lo que debería hacer).
Su código estará en __TEXTO, ___ segmento / sección de texto.
Estoy bastante seguro de que el enlazador unirá las cadenas equivalentes, por lo que el total será menor que la suma de las partes de estas secciones, pero, supongo, normalmente no mucho.
También espero que sus secciones de reubicación y símbolos sean menores que la suma de las partes. Debería eliminar su binario vinculado de símbolos innecesarios para ahorrar espacio (que no es lo mismo que eliminar la información de depuración). Consulte la configuración "Eliminar producto vinculado" en Xcode.
Otra cosa para recordar es que su binario vinculado será un binario FAT, mientras que los archivos de objetos generalmente no lo son.