Disponible la nueva versión "donationware" 7.3 de OrganiZATOR
Descubre un nuevo concepto en el manejo de la información.
La mejor ayuda para sobrevivir en la moderna jungla de datos la tienes aquí.

Curso C++

[Home]  [Inicio]  [Índice]


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)

  Inicio.


[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.