Почему генерируемые двоичные файлы такие большие?

#c #size #binaries

#c #размер #двоичные файлы

Вопрос:

Почему двоичные файлы, которые генерируются при компиляции моих программ на C , такие большие (например, в 10 раз превышающие размер файлов исходного кода)? Какие преимущества это дает по сравнению с интерпретируемыми языками, для которых такая компиляция не требуется (и, следовательно, размер программы равен только размеру файлов кода)?

Комментарии:

1. и какой режим — debug / release и т.д.? Это может оказать существенное влияние на размер двоичного файла — также может повлиять и то, будете ли вы статически связывать библиотеки.

2. Какие заголовки вы включаете? Каждый бит включенного шаблонного кода компилируется в ваш исполняемый файл, а стандартные заголовки заполняются шаблонами.

3. однако интерпретируемым языкам нужно кое-что еще: им нужен интерпретатор. Теперь вы можете написать «hello world» на C, который занимает пару килобайт, или вы можете написать его на Python, который занимает, возможно, 100 байт… плюс интерпретатор размером 3 МБ. Бесплатного обеда не бывает 🙂

Ответ №1:

Современные интерпретируемые языки обычно компилируют код в некоторый способ представления для более быстрого выполнения… возможно, они не будут записаны на диск, но, конечно, нет гарантии, что программа будет представлена в более компактной форме. Некоторые интерпретаторы идут напролом и все равно генерируют машинный код (например, Java JIT). Кроме того, сам интерпретатор находится в памяти, которая может быть большой.

Несколько моментов:

  • Чем сложнее команды в исходном коде, тем больше операций машинного кода может потребоваться для их выполнения. Таким образом, функции языка более высокого уровня, как правило, имеют более высокое соотношение скомпилированного кода к исходному коду. Это не обязательно плохо: думайте об этом как «мне нужно только немного рассказать о том, что я хочу сделать, и из этого вытекают все необходимые шаги». Задача в программировании заключается в обеспечении их необходимости — для этого требуется хорошая библиотека и программный дизайн.
  • Компилятор часто намеренно решает обменять некоторый размер исполняемого файла на более высокую ожидаемую скорость выполнения: встроенный код против встроенного кода является частью этого компромисса, хотя для небольших функций ни одна из них не может быть последовательно более компактной.
  • Более сложные среды выполнения (например, добавление поддержки исключений C ) могут включать немного дополнительного кода, который выполняется, когда программа впервые начинает создавать необходимую среду для этой языковой функции.
  • Возможности библиотек могут быть несопоставимы. Помимо того, что вам, скорее всего, пришлось самостоятельно искать дополнительные библиотеки и быть очень осведомленным в их использовании (например, XML, синтаксический анализ PDF, OpenGL), языки часто незаметно используют вспомогательные библиотеки для того, что кажется языковыми особенностями и функциями. Любой из них может быть удивительно большим.
    • Например, многие интерпретаторы просто предоставляют printf() инструкцию библиотеки C или что-то подобное, в то время как для форматирования вывода C имеет ostream более сложную, расширяемую и типобезопасную систему с (к лучшему или к худшему) постоянным состоянием при вызовах функций, процедуры для запроса и установки этого состояния, дополнительный уровень настраиваемой буферизации, настраиваемые типы символов и локализацию и, как правило, множество небольших встроенных функций, которые могут привести к созданию программ меньшего или размера в зависимости от точного использования и настроек компилятора. Что лучше, зависит от вашего приложения и памяти в сравнении с целями производительности.
  • Инструкции встроенного языка могут быть скомпилированы по-разному: a switch для целочисленного выражения и иметь 100 регистровых меток, случайным образом распределенных между 1 и 1000: один компилятор / языки могут решить «упаковать» 100 регистров и выполнить бинарный поиск соответствия, другой — использовать малонаселенный массив из 1000 элементов и выполнить прямое индексирование (что приводит к потере места в исполняемом файле, но обычно ускоряет код). Итак, трудно делать выводы на основе размера исполняемого файла.

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

Ответ №2:

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

Комментарии:

1. В дальнейшем это означает, что интерпретируемая «программа» ожидает, что все библиотеки и т.д. Уже существуют в системе, следовательно, ваша «программа» будет иметь только ваш код. Такие языки, как C , будут хранить код, на который вы ссылаетесь, в вашем двоичном коде, так что это автономная единица.

2. @William: скомпилированный код часто зависит от совместно используемых библиотек / DLL во время выполнения, так что это не совсем понятно.

3. @Tony: то же самое делают интерпретируемые языки. Интерпретатор часто зависит во многом от одних и тех же общих библиотек / DLL, поэтому, если вы добавите все зависимости, интерпретируемый язык, как правило, будет зависеть от большего количества кода

4. @Tony: Да, ты прав. Прошло некоторое время с тех пор, как я работал с неуправляемым кодом, и я думал о статически связанных библиотеках.

5. @jalf: определенно, это общая тенденция — просто говорю, что это не совсем черно-белое. @William: не беспокойтесь. Приветствия.

Ответ №3:

Возьмем тривиальный случай: предположим, у вас есть однострочная программа

 print("hello world")
  

что делает эта «печать»? Конечно, ясно, что вы просите какой-то другой код выполнить какую-то работу? И этот код не бесплатный, общая сумма того, что нужно выполнить, намного больше, чем строки кода, которые вы пишете. В более реалистичных программах вы используете множество сложных библиотек, управляющих Windows и другими функциями пользовательского интерфейса, сетями, базами данных и так далее. Теперь, независимо от того, включен ли этот код в ваше приложение, загружен ли он из DLL или присутствует в интерпретаторе, он должен где-то быть.

Существует множество компромиссов между компиляцией и интерпретацией, а также промежуточных решений, таких как подход Java к компиляции / интерпретации байтового кода. Например, вы могли бы рассмотреть

  • затраты времени выполнения на интерпретацию исходного кода при каждом запуске по сравнению с выполнением скомпилированного кода
  • преимущества интерпретаторов в переносимости — вам нужно компилировать отдельные версии приложения для разных платформ.

Ответ №4:

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

Компилятор выполняет преобразование только один раз, в то время как интерпретатор обычно преобразует его при каждом выполнении программы.

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

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