2011-01-29 19 views
12

Me gustaría saber por qué el archivo .o que obtenemos al compilar un archivo .c que imprime "Hello, World!" es más grande que un archivo .class de Java que también imprime "Hello, World!"?¿Por qué los archivos compilados de clase Java son más pequeños que los compilados en C?

+1

No lo sé (tengo conjeturas, pero las guardaré para mí y dejaré que los gurús respondan), pero ese es un programa bastante malo para comparar el tamaño de salida del compilador. – delnan

+7

La comparación puede hacerse más justa vinculando el programa C estáticamente y comparando su tamaño con el programa Java + la VM Java. –

+0

Además, ¿cuánto más grande hablamos? – delnan

Respuesta

13

Java utiliza Bytecode para ser independiente de la plataforma y "precompilado", pero el bytecode es utilizado por el intérprete y se sirve para ser lo suficientemente compacto, por lo que no es lo mismo el código de máquina que se puede ver en el programa C compilado. Solo eche un vistazo al proceso completo de compilación de Java:

Java program 
-> Bytecode 
    -> High-level Intermediate Representation (HIR) 
    -> Middle-level Intermediate Representation (MIR) 
     -> Low-level Intermediate Representation (LIR) 
     -> Register allocation 
      -> EMIT (Machine Code) 

esta es la cadena para la transformación del código del Programa Java a Máquina. Como ves, bytecode está muy lejos del código de la máquina. No puedo encontrar en Internet cosas buenas para mostrarle este camino en el programa real (un ejemplo), todo lo que encontré es this presentation, aquí puede ver cómo cada paso cambia la presentación del código. Espero que te explique cómo y por qué el programa compilado c y el bytecode de Java son diferentes.

ACTUALIZACIÓN: Todos los pasos que son después de "código de bytes" son realizadas por JVM en tiempo de ejecución dependiendo de su decisión para compilar el código (que es otra historia ... JVM es el equilibrio entre la interpretación de código de bytes y su compilación de plataforma nativa código dependiente)

Finalmente encontramos un buen ejemplo, tomado de Linear Scan Register Allocation for the Java HotSpot™ Client Compiler (por cierto, buena lectura para comprender qué está pasando dentro de JVM). Imaginemos que tenemos programa Java:

public static void fibonacci() { 
    int lo = 0; 
    int hi = 1; 
    while (hi < 10000) { 
    hi = hi + lo; 
    lo = hi - lo; 
    print(lo); 
    } 
} 

entonces su código de bytes es:

0: iconst_0 
1: istore_0 // lo = 0 
2: iconst_1 
3: istore_1 // hi = 1 
4: iload_1 
5: sipush 10000 
8: if_icmpge 26 // while (hi < 10000) 
11: iload_1 
12: iload_0 
13: iadd 
14: istore_1 // hi = hi + lo 
15: iload_1 
16: iload_0 
17: isub 
18: istore_0 // lo = hi - lo 
19: iload_0 
20: invokestatiC#12 // print(lo) 
23: goto 4 // end of while-loop 
26: return 

cada comando tiene 1 byte (JVM soporta 256 comandos, pero en realidad tiene menos de ese número) + argumentos. Juntos, toma 27 bytes. Omito todas las etapas, y aquí está listo para ejecutar código de máquina:

00000000: mov dword ptr [esp-3000h], eax 
00000007: push ebp 
00000008: mov ebp, esp 
0000000a: sub esp, 18h 
0000000d: mov esi, 1h 
00000012: mov edi, 0h 
00000017: nop 
00000018: cmp esi, 2710h 
0000001e: jge 00000049 
00000024: add esi, edi 
00000026: mov ebx, esi 
00000028: sub ebx, edi 
0000002a: mov dword ptr [esp], ebx 
0000002d: mov dword ptr [ebp-8h], ebx 
00000030: mov dword ptr [ebp-4h], esi 
00000033: call 00a50d40 
00000038: mov esi, dword ptr [ebp-4h] 
0000003b: mov edi, dword ptr [ebp-8h] 
0000003e: test dword ptr [370000h], eax 
00000044: jmp 00000018 
00000049: mov esp, ebp 
0000004b: pop ebp 
0000004c: test dword ptr [370000h], eax 
00000052: ret 

que tarda 83 (52 en hexadecimal de 1 byte +) Bytes resultado.

PS. No tomo en cuenta los enlaces (fue mencionado por otros), así como los encabezados de los archivos compiledc y bytecode (probablemente también sean diferentes; no sé cómo es con c, pero en el archivo bytecode todas las cadenas se mueven a piscina cabecera especial, y en el programa no se utiliza su "posición" en la cabecera, etc.)

Update2: Probablemente vale la pena mencionar, que funciona con pila de java (iStore/comandos iLoad), aunque código de máquina basado en x86 y la mayoría de las demás plataformas funcionan con registros. Como puede ver, el código de la máquina está "lleno" de registros y le da un tamaño extra al programa compilado en comparación con un bytecode más simple basado en la pila.

+0

Gracias por esta respuesta exhaustiva. – palAlaa

+0

¡su respuesta parece un artículo de investigación! –

+0

excepto que esta lejos de cuentas para toda la diferencia. El código puede ser 2-3 veces más corto, pero la diferencia suele ser mucho mayor que eso. La respuesta de @ axtavt explica con mucha más precisión la diferencia – jalf

0

Un archivo de clase es el código de bytes Java.

Lo más probable es que sea más pequeño ya que las bibliotecas C/C++ y las bibliotecas del sistema operativo están vinculadas al código del objeto que produce el compilador C++ para finalmente crear un archivo binario ejecutable.

En pocas palabras, es como comparar el código de Java byte con el código objeto producido por un compilador de C antes de vincularlo para crear un archivo binario. La diferencia es el hecho de que una JVM interpreta el código de bytes de Java para hacer lo que el programa debe hacer, mientras que C requiere información del sistema operativo ya que el sistema operativo funciona como el intérprete.

También en C Se importan todos los símbolos (funciones, etc.) a los que hace referencia desde una biblioteca externa al menos una vez en uno de los archivos objeto. Si lo está utilizando en varios archivos de objeto, todavía se importa una sola vez. Hay dos formas en que esta "importación" puede suceder. Con la vinculación estática, el código real para una función se copia en el ejecutable. Esto aumenta el tamaño del archivo pero tiene la ventaja de que no se necesitan bibliotecas externas (archivos .dll/.so). Con la vinculación dinámica esto no ocurre, pero como resultado, su programa requiere bibliotecas adicionales para ejecutarse.

En Java, todo está "vinculado" dinámicamente, por así decirlo.

+3

El archivo '.o' no es un ejecutable completo y no está enlazado con ninguna biblioteca. La vinculación viene después. – delnan

0

Java se compila en un lenguaje independiente de la máquina. Esto significa que, una vez compilado, la Máquina Virtual de Java (JVM) lo traduce en tiempo de ejecución. C se compila a las instrucciones de la máquina y, por lo tanto, es todo el binario para que el programa se ejecute en la máquina de destino.

Dado que Java se compila en un lenguaje independiente de la máquina, la JVM maneja los detalles específicos para una máquina en particular. (Es decir, C tiene la tara específica de la máquina)

Eso es lo que pienso respecto de todos modos :-)

3

programas en C, a pesar de que están compilados a código máquina nativo que se ejecuta en el procesador (enviado a través del sistema operativo , por supuesto), tienden a necesitar una gran cantidad de configuración y destrucción para el sistema operativo, cargando bibliotecas vinculadas dinámicamente como la biblioteca C, etc.

Java, por otro lado, compila en bytecode para una plataforma virtual (básicamente una computadora simulada dentro de una computadora), que está específicamente diseñada junto con la propia Java, por lo que una gran cantidad de esta sobrecarga (incluso si fuera necesario, ya que tanto el código como la interfaz de VM están bien definidos d) se puede mover a la máquina virtual, dejando el código del programa delgado.

Varía de compilador a compilador, sin embargo, y hay varias opciones para reducirlo o compilar el código de manera diferente, lo que tendrá diferentes efectos.

Dicho todo esto, no es realmente tan importante.

+2

La mayor parte de esa configuración y desmontaje se realiza en una función de biblioteca estándar (por ejemplo, '_start' y' _exit') y, por lo tanto, no se incluye en el archivo objeto antes de vincular. – delnan

+0

Correcto, remonté la instrucción de ensamblaje por instrucción hace un tiempo, y recuerdo que estaba allí. Gracias. Aún así, todavía hay bastante basura adicional (necesaria), en comparación a decir, compilar con nasm o algo así. – Kamalnayan

1

En resumen: los programas de Java se compilan con el código de bytes de Java, que requiere la ejecución de un intérprete independiente (Máquina virtual de Java).

No existe una garantía del 100% de que el archivo .o producido por el compilador c sea más pequeño que el archivo .class producido por el compilador de Java. Todo depende de la implementación del compilador.

-1

Algunas razones posibles:

  • archivo de clase Java no incluye el código de inicialización en absoluto. Simplemente tiene una clase y una función en ella, muy pequeña en verdad. En comparación, el programa C tiene algún grado de código de inicialización enlazado estáticamente, y posiblemente thunk DLL.
  • El programa C también puede tener secciones alineadas con los límites de la página; esto agregaría un mínimo de 4kb al tamaño del programa así para garantizar que el segmento de código se inicie en un límite de página.
1

Una de las principales razones de las diferencias en los tamaños de .o y .class archivos es que los códigos de bytes de Java son un poco más alto nivel de instrucciones de la máquina. No mucho más alto, por supuesto, todavía es algo de bajo nivel, pero eso marcará la diferencia porque actúa para comprimir el programa completo . (Tanto el código de C como el de Java pueden tener código de inicio allí).

Otra diferencia es que los archivos de clase de Java a menudo representan piezas de funcionalidad relativamente pequeñas. Si bien es posible tener archivos de objetos C que se correlacionen con piezas incluso más pequeñas, a menudo es más común poner más funcionalidades (relacionadas) en un solo archivo. Las diferencias en las reglas de alcance también pueden actuar para enfatizar esto (C realmente no tiene nada que corresponda al alcance de nivel de módulo, pero sí tiene alcance de nivel de archivo; el alcance de paquete de Java funciona en varios archivos de clase). Obtienes una mejor métrica si comparas el tamaño de un programa completo.

En términos de tamaños "vinculados", los archivos JAR ejecutables de Java tienden a ser más pequeños (para un nivel dado de funcionalidad) porque se entregan comprimidos. Es relativamente raro entregar programas C en forma comprimida. (También hay diferencias en el tamaño de la biblioteca estándar, pero también podrían ser un lavado porque los programas C pueden contar con bibliotecas que no sean libc, y los programas Java tienen acceso a una gran biblioteca estándar. Seleccionar quién tiene la ventaja es incómodo.)

Luego, también está la cuestión de la información de depuración. En particular, si compila un programa C con depuración en IO, obtendrá mucha información sobre los tipos en la biblioteca estándar incluida, simplemente porque es un poco incómodo filtrarlo. El código Java solo tendrá información de depuración sobre el código compilado real, ya que puede contar con la información relevante que está disponible en el archivo objeto. ¿Esto cambia el tamaño real del código? No. Pero puede tener un gran impacto en el tamaño de los archivos.

En general, supongo que es difícil comparar los tamaños de los programas C y Java. O mejor dicho, puede compararlos y aprender fácilmente nada útil.

7

La causa principal de la diferencia de tamaño en este caso es la diferencia en los formatos de archivo. Para un formato de programa tan pequeño del archivo ELF (.o) introduce una sobrecarga importante en términos de espacio. Por ejemplo, mi archivo de muestra .o del programa "Hola, mundo" toma 864 bytes. Se compone de (explorado con readelf comando):

  • 52 bytes de cabecera del archivo
  • 440 bytes de encabezados de sección (40 bytes x 11 secciones)
  • 81 bytes de nombres de sección
  • 160 bytes de tabla de símbolos
  • 43 bytes de código
  • 14 bytes de datos (Hello, world\n\0)
  • etc

.class archivo del programa similar tiene solamente 415 bytes, a pesar del hecho de que contiene más nombres de símbolos y estos nombres son largos.Se compone de (explorado con Java Class Viewer):

  • 289 bytes de piscina constante (incluye constantes, nombres de símbolos, etc.)
  • 94 bytes de tabla de métodos (código)
  • 8 bytes de tabla de atributos (fuente de referencia de nombre de archivo)
  • 24 bytes de las cabeceras de tamaño fijo

Ver también:

1

más (hasta un 90% para las funciones simples) de un archivo ELF formato .o es basura. Para un archivo .o que contiene un único cuerpo de la función vacía, se puede esperar una avería del tamaño como:

  • 1% código
  • 9% símbolo y reubicación de mesa (esencial para la vinculación)
  • 90% por encima de cabecera, notas inútiles versión/proveedor almacenados por el compilador y/o ensamblador, etc.

Si desea ver el tamaño real de código C compilado, utilice el comando size.

Cuestiones relacionadas