﻿# Причины, по которым 64\-битные программы требуют больше стековой памяти

В форумах люди часто упоминают, что 64\-битные версии программ поглощают больший объем памяти и стека\. При этом обычно ссылаются на то, что размеры данных стали в 2 раза больше\. Однако это необоснованное утверждение, так как размер большинства типов \(char, short, int, float\) в языке Си/Си\+\+ остался прежним на 64\-битных системах\. Конечно, например, увеличился размер указателей, но ведь не все данные в программе состоят из указателей\. Причины роста потребляемой памяти и стека более сложны\. Я решил подробнее исследовать данный вопрос\.

В данной заметке я поговорю о стеке, а в будущем планирую обсудить выделение памяти и размер двоичного кода\. И еще хочу сразу заметить, что статья посвящена языку Си/Си\+\+ и среде разработки Microsoft Visual Studio\.

До недавнего времени я считал, что код 64\-битной программы может поглощать стек не быстрее чем в два раза по сравнению с 32\-битным кодом\. Основываясь на этом предположении, я рекомендовал в статьях на всякий случай увеличивать стек программы в два раза\. Однако теперь я выяснил неприятный факт\. Поглощение стека может вырасти существенно больше чем в два раза\. Я был удивлен, поскольку ранее считал рост стека в два раза самым пессимистическим вариантом развития событий\. Причина моих необоснованных надежд станет понятна чуть позже\. Рассмотрим теперь, как в 64\-битной программе передаются параметры при вызове функций\.

При разработке соглашений по вызовам \([calling conventions](https://en.wikipedia.org/wiki/X86_calling_conventions)\) для архитектуры [x86\-64](https://pvs-studio.ru/ru/blog/terms/0006/) решили положить конец существованию различных вариантов вызова функций\. В [Win32](https://pvs-studio.ru/ru/blog/terms/0054/) существовал целый ряд соглашений о вызове: stdcall, cdecl, fastcall, thiscall и так далее\. В [Win64](https://pvs-studio.ru/ru/blog/terms/0055/) только одно "родное" соглашение по вызовам\. Модификаторы подобные \_\_cdecl компилятором игнорируются\. Думаю, что все согласятся в благородстве такого резкого сокращение числа соглашений\.

Соглашение по вызовам на платформе x86\-64 похоже на соглашение fastcall, существующее в x86\. В x64\-соглашении первые четыре целочисленных аргумента \(слева направо\) передаются в 64\-битных регистрах, выбранных специально для этой цели:

RCX: 1\-й целочисленный аргумент

RDX: 2\-й целочисленный аргумент

R8: 3\-й целочисленный аргумент

R9: 4\-й целочисленный аргумент

Остальные целочисленные аргументы передаются через стек\. Указатель "this" считается целочисленным аргументом, поэтому он всегда помещается в регистр RCX\. Если передаются значения с плавающей точкой, то первые четыре из них передаются в регистрах XMM0\-XMM3, а последующие \- через стек\.

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

Хотя аргументы могут быть переданы в регистрах, компилятор все равно резервирует для них место в стеке, уменьшая значение регистра RSP \(указателя стека\)\. Как минимум, каждая функция должна резервировать в стеке 32 байта \(четыре 64\-битных значения, соответствующие регистрам RCX, RDX, R8, R9\)\. Это пространство в стеке позволяет легко сохранить содержимое переданных в функцию регистров в стеке\. От вызываемой функции не требуется сбрасывать в стек входные параметры, переданные через регистры, но резервирование места в стеке при необходимости позволяет это сделать\. Если передается более четырех целочисленных параметров, в стеке нужно зарезервировать соответствующее дополнительное пространство\.

Рассмотрим пример\. Некая функция передает два целочисленных параметра дочерней функции\. Компилятор положит значения аргументов в регистры RCX и RDX и при этом вычтет 32 байта из регистра RSP\. Вызываемая функция может обратиться к параметрам через регистры RCX и RDX\. Если же коду этой функции данные регистры понадобятся для какой\-то иной цели, он сможет скопировать их содержимое в зарезервированное пространство стека размером 32 байта\.

Описанная особенность приводит к существенному возрастанию скорости поглощения стека\. Даже если функция не имеет параметров, то от стека все равно будет "откушено" 32 байта, которые затем никак не используются\. Смысл использования такого неэкономного механизма я не уловил\. Что\-то говорится про унификацию и упрощение отладки, но как\-то расплывчато\.

Обратим внимание еще на один момент\. Указатель стека RSP должен перед очередным вызовом функции быть выровнен по границе 16 байт\. Таким образом, суммарный размер используемого стека при вызове в 64\-битном коде функции **без параметров** составляет: 8 \(адрес возврата\) \+ 8 \(выравнивание\) \+ 32 \(резерв для аргументов\) \= 48 байт\!

Рассмотрим, к чему это может приводить на практике\. Здесь и далее для экспериментов я буду использовать Visual Studio 2010\. Составим рекурсивную функцию вида:

```cpp
void StackUse(size_t *depth)
{
  volatile size_t *ptr = 0;
  if (depth != NULL)
    ptr = depth;
  cout << *ptr << endl;
  (*ptr)++;
  StackUse(depth);
  (*ptr)--;
}
```

Функция немного запутанна, чтобы оптимизатор не превратил ее в "ничто"\. Основное здесь следующее: функция имеет аргумент типа указатель и одну локальную переменную, также типа указатель\. Посмотрим, сколько стека потребляет функция в 32\-битном и 64\-битном варианте и сколько раз она может быть рекурсивно вызвана при стеке размером 1 мегабайт \(размер по умолчанию\)\.

Release 32\-bit: последнее выведенное число \(глубина стека\) \- 51331

Компилятор использует при вызове данной функции 20 байт\.

Release 64\-bit: последнее выведенное число \- 21288

Компилятор использует при вызове данной функции 48 байт\.

Таким образом, 64\-битный вариант функции StackUse оказывается прожорливее 32\-битного в более чем в 2 раза\.

Замечу, что изменение правил выравнивания данных также может оказывать влияние на размер поглощаемого стека\. Предположим, что функция принимает в качестве аргумента структуру:

```cpp
struct S
{
  char a;
  size_t b;
  char c;
};
void StackUse(S s) { ... }
```

Размер структуры 'S' из\-за изменений правил выравнивания и изменения размера члена 'b' вырастет с 12 до 24 байт при перекомпиляции в 64\-битном режиме\. Структура передается в функцию по значению\. А, следовательно, структура в стеке также займет в два раза больше памяти\.

Неужели все так плохо? Нет\. Не следует забывать про большее количество регистров, имеющихся в распоряжении 64\-битного компилятора\. Усложним код экспериментальной функции:

```cpp
void StackUse(size_t *depth, char a, int b)
{
  volatile size_t *ptr = 0;
  int c = 1;
  int d = -1;
  for (int i = 0; i < b; i++)
    for (char j = 0; j < a; j++)
      for (char k = 0; k < 5; k++)
        if (*depth > 10 && k > 2)
        {
          c += j * k - i;
          d -= (i - j) * c;
        }
  if (depth != NULL)
    ptr = depth;
  cout << c << " " << d << " " << *ptr << endl;
  (*ptr)++;
  StackUse(depth, a, b);
  (*ptr)--;
}
```

Результаты запуска:

Release 32\-bit: последнее выведенное число \- 16060

Компилятор использует при вызове данной функции уже 64 байта\.

Release 64\-bit: последнее выведенное число \- 21310

Компилятор использует при вызове данной функции по\-прежнему 48 байт\.

Для данного примера 64\-битному компилятору удалось использовать дополнительные регистры и построить более эффективный код, что позволило сократить количество используемой стековой памяти\!

## Выводы

1. Невозможно предсказать, сколько стековой памяти будет использовать 64\-битный вариант программы по сравнению с 32\-битным\. Размер может быть как меньше \(что маловероятно\), так и значительно больше\.
1. Для 64\-битной программы на всякий случай стоит увеличить объем зарезервированного стека в 2\-3 раза\. Лучше в 3 раза для спокойствия\. Для этого в настройках проекта имеется параметр Stack Reserve Size \(ключ /STACK:reserve\)\. По умолчанию размер стека составляет 1 мегабайт\.
1. Не следует беспокоиться, что 64\-битная программа потребляет больше стековой памяти\. Физической памяти в 64\-битных системах значительно больше\. Стек размером 2 мегабайта на 64\-битной системе с 8 гигабайтами памяти, занимает меньший процент памяти, чем 1 мегабайт стека в 32\-битной системе с 2 гигабайтами памяти\.

## Дополнительные ссылки

1. Raymond Chen\. The history of calling conventions, part 5: amd64\. [http://blogs\.msdn\.com/b/oldnewthing/archive/2004/01/14/58579\.aspx](https://devblogs.microsoft.com/oldnewthing/2004/01/14)
1. Wikipedia\. x86 calling conventions\. [https://en\.wikipedia\.org/wiki/X86\_calling\_conventions](https://en.wikipedia.org/wiki/X86_calling_conventions)