Почему скомпилированные файлы classов Java меньше C скомпилированных файлов?

Я хотел бы знать, почему файл .o, который мы получаем от компиляции файла .c, который печатает «Hello, World!» больше, чем файл Java .class, который также печатает «Hello, World!»?

    Java использует Bytecode для независимой от платформы и «предварительно скомпилированной», но байт-код используется интерпретатором и служит достаточно компактным, поэтому это не тот машинный код, который вы можете видеть в скомпилированной программе на языке C. Просто взгляните на полный процесс компиляции Java:

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

    это целая цепочка для преобразования кода Java Program to Machine. Как вы видите, байт-код находится далеко от машинного кода. Я не могу найти в Интернете хорошие вещи, чтобы показать вам эту дорогу на реальной программе (пример), все, что я нашел, это презентация , здесь вы можете увидеть, как каждый шаг изменяет представление кода. Надеюсь, он ответит вам, как и почему скомпилированные программы c и Java-байт-коды различны.

    UPDATE: все шаги, которые выполняются после «байт-кода», выполняются JVM во время выполнения в зависимости от его решения о компиляции этого кода (это еще одна история … JVM балансирует между интерпретацией байт-кода и компиляцией на собственный код, зависящий от платформы)

    Наконец, нашел хороший пример, взятый из линейного распределения регистра сканирования для Java HotSpot ™ Client Compiler (btw good reading, чтобы понять, что происходит внутри JVM). Представьте, что у нас есть Java-программа:

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

    то его байткод:

     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 

    каждая команда принимает 1 байт (JVM поддерживает 256 команд, но на самом деле имеет меньше этого числа) + аргументы. Вместе он занимает 27 байт. Я опускаю все этапы, и здесь готов выполнить машинный код:

     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 

    в результате получается 83 (52 в шестнадцатеричном + 1 байт) байта.

    PS. Я не принимаю во внимание ссылку (упоминается другими), а также компилируемые и байткодовые файлы заголовков (возможно, они тоже разные, я не знаю, как это происходит с c, но в файле байт-кода все строки перемещаются в специальный пул заголовков, а в программе используется его «позиция» в заголовке и т. д.),

    UPDATE2: Возможно, стоит упомянуть, что java работает со стеком (команды istore / iload), хотя машинный код на основе x86 и большинство других платформ работает с регистрами. Как видите, машинный код является «полным» регистров и дает дополнительный размер скомпилированной программе в сравнении с более простым байт-кодом на основе стека.

    Основной причиной разницы в размере в этом случае является различие в форматах файлов. Для такого небольшого программного формата файл ELF ( .o ) вводит серьезные накладные расходы с точки зрения пространства.

    Например, мой пример файла .o файла «Hello, world» занимает 864 байта . Он состоит из (с readelf команды readelf ):

    • 52 байта заголовка файла
    • 440 байтов заголовков разделов (40 байт x 11 разделов)
    • 81 байт названий разделов
    • 160 байт таблицы символов
    • 43 байта кода
    • 14 байтов данных ( Hello, world\n\0 )
    • так далее

    .class файл аналогичной программы занимает всего 415 байт , несмотря на то, что он содержит больше имен символов, и эти имена длинны. Он состоит из (с помощью Java Class Viewer ):

    • 289 байтов постоянного пула (включая константы, имена символов и т. Д.),
    • 94 байта таблицы методов (код)
    • 8 байтов таблицы атрибутов (ссылка на исходный файл)
    • 24 байта заголовков фиксированного размера

    Смотрите также:

    • Исполняемый и связанный формат
    • Файл classа Java
    • Java Class Viewer

    C, даже если они скомпилированы в собственный машинный код, который работает на вашем процессоре (естественно, отправляется через ОС), как правило, нужно много настраивать и разрывать для операционной системы, загружать динамически связанные библиотеки, такие как библиотека C и т. д.

    Java, с другой стороны, компилирует байт-код для виртуальной платформы (в основном имитируемый компьютер в составе компьютера), который специально разработан вместе с самой Java, поэтому многие из этих накладных расходов (если это даже необходимо, так как оба код и интерфейс VM хорошо определены) могут быть перемещены в самую виртуальную машину, оставив программный код более тощим.

    Однако это зависит от компилятора и компилятора, и есть несколько вариантов его уменьшения или создания кода по-разному, что будет иметь разные эффекты.

    Все это говорило, что это не так важно.

    Короче говоря, Java-программы скомпилированы в байт-код Java, для чего требуется отдельный интерпретатор (Java Virtual Machine).

    Существует 100% -ная гарантия того, что файл .o, созданный c-компилятором, меньше, чем файл .class, созданный компилятором Java. Все зависит от реализации компилятора.

    Одной из основных причин различий в размерах файлов .o и .class является то, что байт-коды Java немного выше, чем машинные инструкции. Разумеется, не слишком высокий уровень – это все еще довольно низкоуровневый материал, но это будет иметь значение, потому что оно эффективно действует на сжатие всей программы. (Оба кода C и Java могут иметь код запуска.)

    Другое отличие состоит в том, что файлы classов Java часто представляют собой относительно небольшие функциональные возможности. Хотя возможно, что объектные файлы C, которые отображаются на еще более мелкие fragmentы, часто чаще используют более (связанные) функциональные возможности в одном файле. Различия в правилах видимости также могут акцентировать внимание на этом (у C действительно нет ничего, что соответствует областью уровня модуля, но у него действительно есть область уровня файлов, область применения пакета Java работает с несколькими файлами classа). Если вы сравните размер всей программы, вы получите лучшую метрику.

    С точки зрения «связанных» размеров исполняемые JAR-файлы Java имеют тенденцию быть меньше (для определенного уровня функциональности), поскольку они поставляются сжатыми. Сравнительно редко поставлять программы C в сжатой форме. (Есть также различия в размере стандартной библиотеки, но они могут также быть стиранием, потому что программы на C могут рассчитывать на библиотеки, отличные от libc, и программы Java имеют доступ к огромной стандартной библиотеке. неудобно.)

    Затем возникает вопрос об отладочной информации. В частности, если вы скомпилируете C-программу с отладкой на IO, вы получите много информации о типах в стандартной библиотеке, только потому, что это слишком сложно, чтобы отфильтровать ее. Код Java будет иметь только отладочную информацию о фактическом скомпилированном коде, поскольку он может рассчитывать на доступную информацию в объектном файле. Изменяет ли этот размер фактический размер кода? Нет. Но это может сильно повлиять на размеры файлов.

    В целом, я бы предположил, что трудно сравнивать размеры программ на C и Java. Вернее, вы можете сравнить их и легко узнать ничего полезного.

    Большинство (до 90% для простых функций) файла ELF-формата .o являются нежелательными. Для файла .o содержащего единственное пустое тело функции, вы можете ожидать разбивку по размеру, например:

    • Код 1%
    • 9% таблица символов и перемещений (необходимая для связывания)
    • 90% заголовка заголовка, бесполезные версии / вендоры, хранящиеся компилятором и / или ассемблером, и т. Д.

    Если вы хотите увидеть реальный размер скомпилированного кода C, используйте команду size .

    Файл classа – это байтовый код Java.

    Это, скорее всего, меньше, поскольку библиотеки C / C ++ и библиотеки операционной системы связаны с объектным кодом, который производит компилятор C ++, чтобы, наконец, сделать исполняемый двоичный файл.

    Проще говоря, это похоже на сравнение Java-байтового кода с объектным кодом, созданным компилятором C, прежде чем он будет связан с созданием двоичного кода. Разница заключается в том, что JVM интерпретирует байт-код Java, чтобы правильно выполнять то, что программа предназначена для выполнения, тогда как C требует информации из операционной системы, поскольку операционная система функционирует как интерпретатор.

    Также в C Каждый символ (функции и т. Д.) Вы ссылаетесь на внешнюю библиотеку, по крайней мере, один раз в одном из объектных файлов. Если вы используете его в нескольких объектных файлах, он все равно импортируется только один раз. Это может быть два способа импорта. При статической привязке фактический код функции копируется в исполняемый файл. Это увеличивает размер файла, но имеет то преимущество, что внешние библиотеки (файлы .dll / .so) не нужны. С динамическим связыванием это не происходит, но в результате ваша программа требует запуска дополнительных библиотек.

    В Java все так «связано» динамически, так сказать.

    Java компилируется в машинный независимый язык. Это означает, что после его компиляции он затем транслируется во время выполнения виртуальной машиной Java (JVM). C скомпилирован для машинных инструкций и, следовательно, является всем двоичным кодом для запуска программы на целевой машине.

    Поскольку Java компилируется на независимый от машины язык, конкретные детали для конкретной машины обрабатываются JVM. (т. е. C имеет накладные расходы машины)

    Вот как я думаю об этом в любом случае 🙂

    Несколько потенциальных причин:

    • Файл classа Java не включает код инициализации вообще. У этого есть только один class и одна функция в нем – очень мало. Для сравнения, программа C имеет некоторую степень статически связанного кода инициализации и, возможно, DLL-thunks.
    • Программа C также может иметь разделы, выровненные по границам страниц – это добавит минимум 4kb к размеру программы точно так же, чтобы обеспечить, чтобы сегмент кода начинался на границе страницы.