¿Por qué son los archivos de clase Java compilados más pequeños que los archivos compilados C?

StackOverflow https://stackoverflow.com/questions/4838221

Pregunta

Me gustaría saber por qué el archivo .o que obtenemos al compilar un archivo .c que imprime "¡Hola, mundo!" ¿Es más grande que un archivo Java .class que también imprime "¡Hola, mundo!"?

¿Fue útil?

Solución

Java usa 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 el mismo que el código de la máquina que 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 Java a la máquina. Como ve, Bytecode está 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 he encontrado es esta presentación, aquí puede ver cómo cada pasos cambia la presentación del código. Espero que le responda cómo y por qué el programa C compilado y el bytecode Java son diferentes.

ACTUALIZAR:JVM realizan todos los pasos que están después de "Bytecode" en tiempo de ejecución, dependiendo de su decisión de compilar ese código (esa es otra historia ... JVM está equilibrando entre la interpretación de bytecode y su compilación con el código dependiente de la plataforma nativa)

Finalmente encontré un buen ejemplo, tomado de Asignación de registro de escaneo lineal para el compilador de cliente Java HotSpot ™ (Por cierto, buena lectura para entender lo que está sucediendo dentro de JVM). Imagina que tenemos el 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 bytecode 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 toma 1 byte (JVM admite 256 comandos, pero de hecho tiene menos de ese número) + argumentos. Juntos se necesitan 27 bytes. Omito todas las etapas, y aquí está listo para ejecutar el código de la 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

Se necesitan 83 (52 en hex + 1 bytes) bytes en el resultado.

PD. No tengo en cuenta la vinculación (fue mencionado por otros), así como los encabezados de archivos compilados y bytecode (probablemente también son diferentes; no sé cómo está con C, pero en el archivo Bytecode se mueven todas las cadenas. grupo de encabezado especial, y en el programa se usa su "posición" en el encabezado, etc.)

Actualización2: Probablemente valga la pena mencionar que Java funciona con Stack (comandos Istore/ILOAD), aunque el código de la máquina basado en X86 y la mayoría de la otra plataforma funciona con registros. Como puede ver, el código de la máquina está "lleno" de los registros y eso le da un tamaño adicional al programa compilado para comparar con ByTecode basado en pila más simple.

Otros consejos

La principal causa de diferencia de tamaño en este caso es la diferencia en los formatos de archivo. Para un formato de programa tan pequeño del elfo (.o) El archivo presenta sobrecarga grave en términos de espacio.

Por ejemplo, mi muestra .o Archivo del programa "Hola, Mundial" toma 864 bytes. Consiste en (explorado con readelf dominio):

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

.class El archivo del programa similar toma solo 415 bytes, a pesar del hecho de que contiene más nombres de símbolos y estos nombres son largos. Consiste en (explorado con Visor de clase Java):

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

Ver también:

Los programas C, a pesar de que se compilan en un código de máquina nativo que se ejecuta en su procesador (enviado a través del sistema operativo, por supuesto), tienden a necesitar una gran configuración para el sistema operativo, cargando dinámicamente vinculado Bibliotecas como la biblioteca C, etc.

Java, por otro lado, se compila en Bytecode para una plataforma virtual (básicamente una computadora simulada dentro de una computadora), que se diseña específicamente junto con Java, por lo que mucha sobrecarga (si fuera necesario, ya que ambos serían necesarios, ya que ambos El código y la interfaz VM están bien definidos) se pueden mover a la VM en sí, dejando que el código del programa sea delgado.

Sin embargo, varía de compilador a compilador, y hay varias opciones para reducirlo o crear código de manera diferente, lo que tendrá diferentes efectos.

Dicho todo esto, no es realmente tan importante.

En resumen: los programas Java se compilan en el código Java Byte, que requiere que se ejecute un intérprete separado (máquina virtual Java).

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

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

Otra diferencia es que los archivos de clase Java a menudo representan piezas de funcionalidad relativamente pequeñas. Si bien es posible tener archivos de objetos C que se asignen a piezas aún más pequeñas, a menudo es más común poner más funcionalidad (relacionada) 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 a nivel de módulo, pero tiene un alcance a nivel de archivo; el alcance del paquete de Java funciona en múltiples archivos de clase). Obtiene una mejor métrica si compara 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 determinado 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 presentes LIBC, y los programas de Java tienen acceso a una gran biblioteca estándar. Recogiendo 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 la depuración en eso, IO, obtendrá mucha información sobre los tipos en la biblioteca estándar incluida, solo porque es demasiado incómodo para filtrarla. El código Java solo tendrá información de depuración sobre el código compilado real porque puede contar con la información relevante disponible en el archivo de objeto. ¿Cambia esto el tamaño real del código? No. Pero puede tener un gran impacto en los tamaños de archivo.

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

La mayoría (hasta el 90% para funciones simples) de un formato elfo .o El archivo es basura. Para .o Archivo que contiene un solo cuerpo de función vacía, puede esperar un desglose de tamaño como:

  • 1% de código
  • 9% de símbolo y tabla de reubicación (esencial para vincular)
  • 90% de sobrecarga de encabezado, notas inútiles de versión/proveedor almacenadas por el compilador y/o ensamblador, etc.

Si desea ver el tamaño real del código C compilado, use el size dominio.

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

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 de objeto que el compilador C ++ produce para finalmente hacer un binario ejecutable.

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

También en C cada símbolo (funciones, etc.), se trata de referencia desde una biblioteca externa al menos una vez en uno de los archivos de objetos. Si lo está utilizando en varios archivos de objetos, todavía se importa solo una vez. Hay dos formas en que puede ocurrir esta "importación". 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 dinámica vinculación, esto no sucede, pero como resultado su programa requiere bibliotecas adicionales para ejecutarse.

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

Java se compila en un lenguaje independiente de la máquina. Esto significa que después de que se compila, la máquina virtual Java (JVM) lo traduce en tiempo de ejecución. C se compila con 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.

Debido a que Java se compila en un lenguaje independiente de la máquina, los detalles específicos para una máquina en particular son manejados por el JVM. (es decir, C tiene una sobrecarga específica de la máquina)

Así es como lo pienso de todos modos :-)

Algunas razones potenciales:

  • El archivo de clase Java no incluye el código de inicialización en absoluto. Simplemente tiene su única clase y una función, muy pequeña. En comparación, el programa C tiene cierto grado de código de inicialización vinculado por estática, y posiblemente DLL Thunks.
  • 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 al igual que esa, para garantizar que el segmento de código comience en un límite de página.
Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top