Подтвердить что ты не робот

Длительное время компиляции для программы со статическим распределением

Я был бы очень признателен, если бы кто-нибудь мог сказать мне, почему компиляция этой программы:

double data[123456789];  
int main() {}

занимает в 10 раз больше времени, чем компиляция этого:

int main() {
    double* data=new double[123456789];
}

когда оба скомпилированы с помощью:

$ g++ -O0

а исполняемые файлы имеют почти одинаковый размер.

Я использую gcc 4.4.3 на Ubuntu 10.04.

Спасибо.

4b9b3361

Ответ 1

Динамическое распределение

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

double *data = new double[123456789];
double *data = malloc(123456789);
double data  = sqrt(123456789);

Все они делают разные вещи, но все, что требуется компилятору, это генерировать вызов внешней функции с фиксированным аргументом. Вы можете увидеть это, если вы используете g++ -S для сборки сборки:

.text
main:
    subq    $8, %rsp          /* Allocate stack space. */
    movl    $987654312, %edi  /* Load value "123456789 * 8" as argument. */
    call    _Znam             /* Call the allocation function. */
    xorl    %eax, %eax        /* Return 0. */
    addq    $8, %rsp          /* Deallocate stack space. */
    ret

Это просто для любого компилятора для генерации и для любого компоновщика.

Статическое размещение

Ваша первая программа немного запутанна, хотя, как вы заметили. Если мы посмотрим на сборку для этого, мы увидим что-то другое:

.text
main:
    xorl    %eax, %eax        /* Return 0. */
    ret

.bss
data:
    .zero   987654312         /* Reserve "123456789 * 8" bytes of space. */

Сгенерированная сборка запрашивает 123456789 * sizeof(double) байты пространства для резервирования при первом запуске программы. Когда это будет собрано и позже связано (что происходит за кулисами, вы просто запустите g++ foo.c), компоновщик ld фактически выделит все это зарезервированное пространство в памяти. Здесь время идет. Если вы запустите top, а g++ запустится, вы увидите ld всасываете большую часть вашей системной памяти.

Сокращенные исполняемые размеры

Разумным может быть вопрос: "Почему мой исполняемый файл не очень большой, если память зарезервирована во время ссылки?". Ответ скрыт в марке .bss в сборке. Это сообщает компоновщику, что данные, определенные ниже, не должны быть сохранены в конечном исполняемом файле, а вместо этого назначены нулю во время выполнения.

Это оставляет нам последовательность шагов:

  • Ассемблер сообщает компоновщику, что ему необходимо создать секцию памяти длиной 1 ГБ.

  • Компонент идет вперед и выделяет эту память, готовясь к ее размещению в финальном исполняемом файле.

  • Линкер понимает, что эта память находится в разделе .bss и помечена NOBITS, что означает, что данные равны 0 и не обязательно должны быть физически помещены в окончательный исполняемый файл. Это позволяет избежать записи 1 ГБ данных, вместо этого просто отбрасывая выделенную память.

  • Компилятор записывает в окончательный файл ELF только скомпилированный код, создавая небольшой исполняемый файл.

Более умный компоновщик может избежать шагов 2 и 3 выше, что значительно ускорит время компиляции. На самом деле, такие сценарии, как ваши, на практике не выходят достаточно часто, чтобы сделать такую ​​оптимизацию стоящей.

Динамическое и статическое распределение

Если вы пытаетесь решить, какой из вышеперечисленных (динамическое или статическое распределение) фактически использовать в вашей программе, вот несколько соображений:

  • Линкером необходимо будет использовать столько же памяти, сколько и ваша окончательная программа (плюс бит). Если вы хотите статически распределить 4 ГБ ОЗУ, вам понадобится 4 ГБ ОЗУ для вашего компоновщика. Это не подразумевается в том, как работают линкеры, а скорее просто похоже на то, как они реализованы.

  • При распределении больших объемов памяти динамическое выполнение позволяет вам улучшить обработку ошибок (отображая на экране удобное для пользователя сообщение, объясняющее, что у вас недостаточно памяти, вместо того, чтобы просто не загружать исполняемый файл с помощью сообщение от ОС, идущее к пользователю);

  • Динамическое выделение памяти позволяет вам выбрать, сколько памяти будет выделяться на основе ваших реальных потребностей. (Если data - это кеш, пользователь может выбрать размер кеша, если он хранит промежуточные результаты, вы можете изменить его на основе проблемы и т.д.)

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

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