Почему скомпилированные файлы классов Java меньше, чем скомпилированные файлы C?
Я хотел бы знать, почему файл.o, который мы получаем при компиляции файла.c, который печатает "Hello, World!" больше, чем файл Java .class, который также печатает "Hello, World!"?
9 ответов
Java использует байт-код для независимости от платформы и "предварительно скомпилирован", но байт-код используется интерпретатором и служит достаточно компактным, поэтому он не совпадает с машинным кодом, который вы можете видеть в скомпилированной программе на Си. Достаточно взглянуть на весь процесс компиляции 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-программы в машинный код. Как видите, байт-код находится далеко от машинного кода. Я не могу найти в Интернете хороших вещей, чтобы показать вам этот путь в реальной программе (пример), все, что я нашел, - это презентация, здесь вы можете увидеть, как каждый шаг меняет представление кода. Я надеюсь, что это ответит вам, как и почему скомпилированная программа c и байт-код Java отличаются.
ОБНОВЛЕНИЕ: Все шаги, которые выполняются после "байт-кода", выполняются JVM во время выполнения в зависимости от его решения компилировать этот код (это другая история... JVM балансирует между интерпретацией байт-кода и его компиляцией в собственный код, зависящий от платформы)
Наконец нашел хороший пример, взятый из Распределения регистров линейного сканирования для клиентского компилятора HotSpot™ (кстати, хорошее чтение, чтобы понять, что происходит внутри 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. Я не принимаю во внимание ссылки (упоминавшиеся другими), а также заголовки файлов compiledc и bytecode (возможно, они тоже разные; я не знаю, как с c, но в файле bytecode все строки перемещаются в специальный пул заголовков, и в программе используется его "позиция" в заголовке и т. д.)
ОБНОВЛЕНИЕ 2: Вероятно, стоит упомянуть, что Java работает со стеком (команды istore/iload), хотя машинный код на базе x86 и большинства других платформ работает с регистрами. Как вы можете видеть, машинный код "полон" регистров, и это дает дополнительный размер скомпилированной программе по сравнению с более простым стековым байт-кодом.
Основной причиной различий в размерах в этом случае является различие в форматах файлов. Для такой маленькой программы формат ELF (.o
) файл вводит серьезные накладные расходы с точки зрения пространства.
Например, мой образец .o
Файл программы "Hello, world" занимает 864 байта. Он состоит из (исследовано с readelf
команды):
- 52 байта заголовка файла
- 440 байтов заголовков разделов (40 байтов x 11 разделов)
- 81 байт имен разделов
- 160 байтов таблицы символов
- 43 байта кода
- 14 байт данных (
Hello, world\n\0
) - так далее
.class
Файл аналогичной программы занимает всего 415 байт, несмотря на то, что он содержит больше имен символов и эти имена длинные. Он состоит из (исследуется с помощью Java Class Viewer):
- 289 байтов постоянного пула (включая константы, имена символов и т. Д.)
- 94 байта таблицы методов (код)
- 8 байтов таблицы атрибутов (ссылка на имя исходного файла)
- 24 байта заголовков фиксированного размера
Смотрите также:
Программы на C, даже если они скомпилированы с собственным машинным кодом, который выполняется на вашем процессоре (разумеется, отправляется через ОС), обычно требуют много настроек и демонтажа для операционной системы, загружая динамически связанные такие библиотеки, как библиотека C и т. д.
Java, с другой стороны, компилируется в байт-код для виртуальной платформы (в основном, имитируемого компьютера в компьютере), которая специально разработана вместе с самой Java, поэтому большая часть этих накладных расходов (если бы это было даже необходимо, так как код и интерфейс виртуальной машины четко определены) могут быть перемещены в саму виртуальную машину, оставляя программный код простым.
Тем не менее, он варьируется от компилятора к компилятору, и есть несколько вариантов уменьшить его или построить код по-разному, что будет иметь разные последствия.
Все это говорит, это не так уж важно.
Вкратце: Java-программы компилируются в байт-код Java, для которого требуется отдельный интерпретатор (виртуальная машина Java).
Нет 100% гарантии, что файл.o, созданный c-компилятором, меньше, чем файл.class, созданный компилятором Java. Все зависит от реализации компилятора.
Одна из ключевых причин различий в размерах .o
а также .class
Файлы в том, что байт-коды Java немного более высокого уровня, чем машинные инструкции. Конечно, не очень высокий уровень - это все же довольно низкоуровневый материал - но это будет иметь значение, потому что он эффективно действует на сжатие всей программы. (Как код C, так и код Java могут содержать код запуска.)
Другое отличие состоит в том, что файлы классов Java часто представляют относительно небольшие функциональные возможности. В то время как можно иметь объектные файлы C, которые отображаются на еще более мелкие фрагменты, часто более распространено помещать больше (связанных) функциональных возможностей в один файл. Различия в правилах области видимости также могут подчеркнуть это (на самом деле C не имеет ничего, что соответствует области действия на уровне модуля, но вместо этого у него есть область действия на уровне файла; область действия пакета Java работает с несколькими файлами классов). Вы получите лучший показатель, если вы сравните размер всей программы.
С точки зрения "связанных" размеров исполняемые файлы JAR Java имеют тенденцию быть меньше (для данного уровня функциональности), потому что они поставляются сжатыми. Поставлять программы на С в сжатом виде относительно редко. (Существуют также различия в размерах стандартной библиотеки, но они также могут быть отмывкой, потому что программы на C могут рассчитывать на библиотеки, отличные от присутствия libc, а программы на Java имеют доступ к огромной стандартной библиотеке. Отбирая, у кого есть преимущество неловко.)
Затем возникает вопрос отладки информации. В частности, если вы скомпилируете программу на C с отладкой, которая выполняет IO, вы получите много информации о типах в стандартной библиотеке, включенной только потому, что это слишком неудобно для ее фильтрации. Код Java будет иметь только отладочную информацию о фактическом скомпилированном коде, поскольку он может рассчитывать на соответствующую информацию, доступную в объектном файле. Меняет ли это реальный размер кода? Нет. Но это может оказать большое влияние на размеры файлов.
В целом, я думаю, что трудно сравнивать размеры программ на C и Java. Или, скорее, вы можете сравнить их и легко узнать ничего полезного.
Большая часть (до 90% для простых функций) формата ELF .o
Файл мусорный. Для .o
файл, содержащий одно пустое тело функции, вы можете ожидать разбивки размера, как:
- 1% код
- 9% символ и таблица перемещения (необходимо для связи)
- 90% заголовка, бесполезные заметки о версии / вендоре, сохраненные компилятором и / или ассемблером и т. Д.
Если вы хотите увидеть реальный размер скомпилированного кода на C, используйте size
команда.
Файл класса - это байт-код Java.
Скорее всего, он меньше, поскольку библиотеки C/C++ и библиотеки операционной системы связаны с объектным кодом, который компилятор C++ создает для окончательного создания исполняемого двоичного файла.
Проще говоря, это все равно, что сравнивать байт-код Java с объектным кодом, созданным компилятором C, прежде чем он будет связан для создания двоичного файла. Разница заключается в том, что JVM интерпретирует байт-код Java, чтобы правильно делать то, для чего предназначена программа, тогда как C требует информацию из операционной системы, поскольку операционная система выполняет функции интерпретатора.
Также в C Каждый символ (функции и т. Д.), На который вы ссылаетесь из внешней библиотеки, хотя бы один раз импортируется в один из объектных файлов. Если вы используете его в нескольких объектных файлах, он все равно будет импортирован только один раз. Есть два способа, которыми этот "импорт" может произойти. При статическом связывании фактический код функции копируется в исполняемый файл. Это увеличивает размер файла, но имеет то преимущество, что внешние библиотеки (файлы.dll /.so) не нужны. При динамическом компоновке этого не происходит, но в результате вашей программе требуются дополнительные библиотеки для запуска.
В Java все, так сказать, динамически "связано".
Java компилируется в машинно-независимый язык. Это означает, что после компиляции он транслируется во время выполнения виртуальной машиной Java (JVM). C скомпилирован с машинными инструкциями и поэтому является двоичным для программы, выполняемой на целевой машине.
Поскольку Java скомпилирована на машинно-независимый язык, JVM обрабатывает конкретные детали для конкретной машины. (то есть C имеет машинно-зависимые накладные расходы)
Вот так я все равно думаю об этом:-)
Несколько потенциальных причин:
- Файл класса Java вообще не включает код инициализации. Он просто содержит один класс и одну функцию - очень маленький. Для сравнения, C-программа имеет некоторую степень статически связанного кода инициализации и, возможно, DLL-кодов.
- Программа на Си также может иметь разделы, выровненные по границам страницы - это добавит минимум 4 КБ к размеру программы, чтобы гарантировать, что сегмент кода начинается на границе страницы.