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