Ответ 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
- это кеш, пользователь может выбрать размер кеша, если он хранит промежуточные результаты, вы можете изменить его на основе проблемы и т.д.) -
Динамическое выделение памяти позволяет вам освободить ее позже, если ваша программа должна продолжить работу и сделать больше работы после ее завершения с памятью.
В конце концов, если приведенные выше пункты не имеют значения, и вы можете иметь дело с более длительным временем компиляции, это, вероятно, не имеет большого значения. Статическое распределение памяти может быть намного проще, и часто это правильный подход для небольших объемов памяти или отбрасываемых приложений.