1.4.6 Tamaño de los ejecutables
§1 Justificación
Aunque importante, la cuestión del tamaño de los ejecutables quizás no debería merecer atención especial en un curso básico de programación C++ como el presente. Sin embargo, el asunto suele ser motivo de desconcierto entre los que se aproximan por primera vez a la programación C++. Por ejemplo, en los foros son frecuentes las controversias sobre los tamaños obtenidos en las compilaciones C y C++. En consecuencia, hemos decidido incluir aquí unas breves consideraciones sobre el tema, con la esperanza de que ayuden al lector a contemplar el asunto en sus justos términos y a identificar los factores que influyen en él.
§2 Sinopsis
Deberíamos comenzar diciendo que "el tamaño del ejecutable" es
una expresión un tanto ambigua. Recordemos que una cosa es el fichero .exe
(conocido también como fichero-imagen), tal como aparece en un medio de almacenamiento externo (disco), y otra su
disposición y por consiguiente su tamaño, cuando se ha cargado en
memoria. En principio ambos tamaños no tienen ninguna
relación entre sí, de forma que a un fichero .exe pequeño puede corresponderle un tamaño
en memoria mucho mayor. Por ejemplo, si el fuente define una matriz cuyo
espacio se reserva en runtime. Pensemos también que la pila puede crecer
casi indefinidamente en el caso de la invocación de funciones con gran nivel de
anidamiento (
4.4.6b) como es el caso de
las funciones recursivas (
4.4.6c). Incluso para ser más exactos, debemos
recordar que la disposición en memoria del ejecutable no se refiere
necesariamente a un espacio único, ya que generalmente ocupa distintas áreas
que no son necesariamente contiguas (
1.3.2). Por consiguiente, más que
de tamaño del ejecutable en memoria, deberíamos referirnos al de sus distintos
módulos.
§2.1 Ejecutable en memoria
En tiempos, la memoria disponible para el ejecutable era una cuestión
primordial. En MS-DOS venía limitada por la fatídica barrera de los 640 KB
(
H5.1). Sin embargo, la memoria dinámica en los
sistemas actuales ha reducido su importancia, solo reseñable quizás en
sistemas embebidos (microporcesadores dedicados) donde la
memoria suele ser limitada. En lo que se refiere al montón y a la pila, recordemos que los compiladores actuales disponen de
opciones para limitar el tamaño de memoria que utilizará el ejecutable (
1.4.4a Heapsize, Stacksize).
En lo referente a los tamaños del segmento de datos y del código ("data
segment" y "code segment"), son de aplicación algunas de las
observaciones de carácter general que se relacionan a continuación.
§2.2 Ejecutable externo
En lo que respecta al tamaño del ejecutable en su forma externa (.exe), que es el aspecto más aparente, y por consiguiente el más generalmente utilizado, son pertinentes algunas observaciones de carácter general:
El tamaño del fichero .exe solo tiene importancia en el momento inicial de la carga; cuando es traído a memoria desde el disco, o a través de la red.
Si exceptuamos casos extremos, como las aplicaciones embebidas ya señaladas, el espacio de almacenamiento del fichero .exe, con ser importante, no lo es tanto como en épocas pasadas, cuando las aplicaciones debían ser distribuidas en disquetes. Actualmente la distribución se realiza mayoritariamente por la red en sus distintas formas, o mediante CDs y DVDs, cuyas capacidades exceden con mucho la que necesitan las aplicaciones más exigentes.
Nota: la distribución de MS-DOS 6.2 de 1993, ocupaba 4 disquetes de alta densidad HD (1.44 MB). La distribución de Microsoft Windows para Trabajo en Grupo del mismo año, ocupaba 9 disquetes HD. En la actualidad (2006) las distribuciones de Windows ocupan varios cientos de MB.
Recordar que existen utilidades ("packers") capaces de reducir ("squeeze") el tamaño del ejecutable, lo que puede ser útil para su almacenamiento y para su transporte por redes. Sin embargo, estos ficheros deben ser descomprimidos en memoria para su carga, momento en que desaparecen las bondades del sistema [1].
§3 Factores que influyen en el tamaño del ejecutable
Recordemos que, al igual que ocurre con la velocidad de ejecución, en lo relativo al tamaño no existen medidas mágicas o milagrosas. Es más bien una cuestión de paciencia y de método (ensayar muchas alternativas que comienzan con el diseño del código). A continuación reseñamos algunos factores que influyen en el tamaño del ejecutable, pero debemos advertir que su influencia depende de las circunstancias particulares; unas veces puede ser mínima, otras decisiva y espectacular. En cualquier caso, es incumbencia de desarrollador conocerlos y decidir la combinación de medidas más adecuada a sus circunstancias.
- Los compiladores modernos utilizan distintos criterios de
optimización. Generalmente permiten seleccionar que criterio será
dominante: la velocidad de ejecución o el tamaño del ejecutable (
1.2).
- La utilización de macros en lugar de funciones (
5.1), mejora la velocidad de ejecución en detrimento del tamaño del ejecutable.
- La utilización del mecanismo de identificación de tipos en tiempo de
ejecución (RTTI
4.9.14) obliga a la inclusión de determinadas librerías que aumentan el tamaño del ejecutable.
- La utilización del mecanismo de excepciones (
1.6) obliga al compilador a incluir código extra necesario para lanzar y propagar la excepción. En algunos casos, esto obliga a generar la información necesaria para descargar el marco de pila ("frame unwind") para todas las funciones, lo que se traduce en un aumento significativo del tamaño del código. Recordar que en los compiladores C++, este mecanismo suele estar habilitado por defecto, aunque puede ser deshabilitado.
- La utilización de determinados recursos de la Librería Estándar de
Plantillas STL (
5.1) puede originar un aumento considerable del tamaño del ejecutable resultante ("code bloat"). En general la utilización juiciosa de la STL exige cierta práctica.
- Como señalábamos en el capítulo anterior (
1.4.5), la información de depuración es sin duda la mayor responsable del aumento del tamaño de los ejecutables. En consecuencia, no olvide que esta información debe ser suprimida en las versiones definitivas de los ejecutables, o en las que no requieran depuración. Lo más práctico es disponer de makefiles (
1.4.0a) distintos para las versiones de desarrollo y para las de campo.
- Los ejecutables pueden incluir una tabla de símbolos. Esta información es incluida por el enlazador para ayudar en la depuración y para distintas utilidades. En ocasiones, una parte de esta información es imprescindible. Por ejemplo la que relaciona símbolos situados en el exterior (importables). Pero es frecuente, incluso cuando no se ha solicitado expresamente información sobre depuración, que el enlazador incluya símbolos no estrictamente necesarios para la ejecución (la mayoría ha desaparecido durante el enlazado). Esta tabla, que en ocasiones es enorme y responsable de buena parte del tamaño del ejecutable, puede ser reducida mediante ciertas opciones de compilación que "desnudan" el código durante el proceso de enlazado [3], y mediante utilidades como strip, que eliminan todos los símbolos que no son estrictamente necesarios para la ejecución sobre un ejecutable ya construido.
- Recuerde que las librerías estáticas incrementan el tamaño del
ejecutable, mientras que las dinámicas permiten concentrar parte de la
funcionalidad requerida en ficheros independientes (.dll), y que
estos pueden ser compartidos por varios ejecutables (
1.4.4b).
Nota: Como ejemplo de lo anterior, incluimos algunos comentarios
(respetando su redacción original en inglés) relativas al efecto de la
inclusión de la Librería Estándar en el tamaño de los ejecutables:
Why is my C++ binary so large?
C++ programs using the Standard Template Library (ie/ #include <iostream>) cause a large part of the library to be statically linked into the binary. The need to statically link the stdc++ into the binary is two fold. First MSVCRT.dll does not contain C++ stdlib constructs. Second the legal implications of generating a libstdc++.dll are restricted by the licensing associated with the library. If you wish to keep your file size down use strip to remove debugging information and other verbatim found in the binary.
strip --strip-all SOMEBINARY.exe
Tomado de: MinGW - Frequently Asked Questions www.mingw.org
Why is the compiled executable file so large?
People usually ask this question when they compile a simple program which uses iostreams. The first thing you can do is to add -s to Project Options - Parameters - Linker, but the result may be still too large for your taste. In this case, either try to live with it (it actually doesn't matter so much!), or avoid iostreams (use cstdio), or use another compiler. Also note that there are some exe compressors on the net, e.g. upx.
The reason why iostream increases the size so much is that the linker links entire object files (from inside of libraries) if they contain at least one necessary reference, and the library for iostream is not well separated into small object files. Also, the linker should be able to link only certain sections of the object files (see "--gc-sections"), but this particular feature doesn't work yet on the mingw target (and that affects all libraries and object files).
Tomado de: Adrian Sandor www14.brinkster.com
§4 Ejemplo
Como muestra utilizaremos una aplicación C++ de consola, con el consabido "Hola mundo", en dos versiones: la primera p1.cpp, utiliza los recursos de la Librería Estándar C++; la segunda p2.cpp, utiliza la librería clásica para producir la salida. Construimos versiones del ejecutable con el compilador Borland C++ 5.5 y con la versión c++ de GNU para Windows de MinGW.
// p1.cpp Librería Estándar C++
#include <iostream>
int main() {
std::cout << "Hola mundo" << std::endl;
system ("PAUSE");
return EXIT_SUCCESS
}
// p2.cpp Librería Clásica
#include <stdio.h>
#include <stdlib.h>
int main() {
printf("Hola mundo\n");
system ("PAUSE");
return EXIT_SUCCESS;
}
Para cada versión del fuente se realizan dos compilaciones; la primera con las opciones por defecto; la segunda con las opciones adecuadas para reducir al máximo el tamaño del ejecutable. La tabla adjunta muestra los resultados obtenidos en cada caso.
Fuente | Compilador | Makefile ![]() |
Tamaño [2] |
p1.cpp | Borland C++ | Bdefault | 117.248 |
Bopt | 117.248 | GNU c++ | GNUdefault | 474.953 |
GNUopt | 266.240 | ||
p2.cpp | Borland C++ | Bdefault | 56.320 |
Bopt | 56.320 | ||
GNU c++ | GNUdefault | 15.839 | |
GNUopt | 4.096 |
Puede comprobarse que en ambos casos, el compilador Borland se muestra muy eficiente respecto a la optimización alcanzada con las opciones por defecto. No obstante, los ejecutables p1.exe y p2.exe obtenidos, de 117.248 y 56320 Bytes, pueden ser reducidos hasta 116.736 y 55.808 Bytes respectivamente mediante la utilidad strip [4].
Puede observarse también que las Librerías Estándar de la versión GNU para Windows parecen poco optimizadas en cuanto al tamaño; el mejor resultado es casi el doble que el conseguido con Borland. Sin embargo, la ventaja de GNU para las librerías clásicas es aplastante respecto a los resultados de Borland. Observe finalmente que la diferencia entre los tamaños extremos conseguidos, 4.000 frente a casi 475.000 Bytes, muestran claramente que en esta cuestión, las diferencias pueden ser muy abultadas en función del compilador y de las circunstancias.
Los mekefiles utilizados son los siguientes:
# Bdefault Makefile para Borland; opciones por
defecto
LIBS = -LE:\BorlandCPP\Lib
INCS = -IE:\BorlandCPP\Include
all: p1.exe
p1.exe:
bcc32 $(INCS) $(LIBS) p1.Cpp
# Bopt Makefile para Borland optimizado
LIBS = -LE:\BorlandCPP\Lib
INCS = -IE:\BorlandCPP\Include
all: p1.exe
p1.exe:
bcc32 $(INCS) $(LIBS) -v- -O1 -RT- -x- -xd- p1.Cpp
# GNUdefault Makefile para GNU; opciones por defecto
LIBS = -L"C:/DEV-CPP/lib"
CXXFLAGS = -I"C:/DEV-CPP/lib/gcc/mingw32/3.4.2/include" \
-I"C:/DEV-CPP/include/c++/3.4.2/backward" \
-I"C:/DEV-CPP/include/c++/3.4.2/mingw32" \
-I"C:/DEV-CPP/include/c++/3.4.2" -I"C:/DEV-CPP/include"
all: p1.exe
p1.exe: p1.o
g++ p1.o -o "p1.exe" $(LIBS)
# GNUopt Makefile para GNU optimizado
LIBS = -L"C:/DEV-CPP/lib" # -lcomctl32
CXXFLAGS = -I"C:/DEV-CPP/lib/gcc/mingw32/3.4.2/include" \
-I"C:/DEV-CPP/include/c++/3.4.2/backward" \
-I"C:/DEV-CPP/include/c++/3.4.2/mingw32" \
-I"C:/DEV-CPP/include/c++/3.4.2" -I"C:/DEV-CPP/include"
all: p1.exe
p1.exe: p1.o
g++ p1.o -o "p1.exe" -s -Os -fno-rtti -fno-exceptions $(LIBS)
[1] Por ejemplo UPX
upx.sourceforge.net.
[2] Tamaño real del fichero del ejecutable en Bytes. Sin contar el espacio ocupado debido al redondeo al próximo cluster.
[3] En el compilador GNU gcc es la opción -s (de "strip"). Para dar una idea de la disminución que puede alcanzarse, digamos que un ejecutable para Windows32, a partir de un código C++ compilado con g++ para Windows (MinGW), sin incluir expresamente ninguna opción de depuración, resultó con un tamaño de 878.736 Bytes. La eliminación de la tabla de símbolos compilando con la -s, produjo un ejecutable de 536.576 Bytes.
[4] Hemos utilizado la utilidad strip.exe de las "binutils" de MinGW, sobre el ejecutable construido con Borland. El comando es el siguiente:
strip -s -o p11.exe p1.exe
Vemos que la utilidad puede todavía arañar algunos bytes al
fichero, ya de por sí bastante optimizado. En ambos casos, el ejecutable obtenido, p11.exe, resulta 512 Bytes menor que el
original.