Для получения триального ключа
заполните форму ниже
Team License (базовая версия)
Enterprise License (расширенная версия)
* Нажимая на кнопку, вы даете согласие на обработку
своих персональных данных. См. Политику конфиденциальности

** На сайте установлена reCAPTCHA и применяются
Политика конфиденциальности и Условия использования Google.
Запросите информацию о ценах
Новая лицензия
Продление лицензии
--Выберите валюту--
USD
EUR
GBP
RUB
* Нажимая на кнопку, вы даете согласие на обработку
своих персональных данных. См. Политику конфиденциальности

** На сайте установлена reCAPTCHA и применяются
Политика конфиденциальности и Условия использования Google.
Для получения лицензии для вашего открытого
проекта заполните, пожалуйста, эту форму
* Нажимая на кнопку, вы даете согласие на обработку
своих персональных данных. См. Политику конфиденциальности

** На сайте установлена reCAPTCHA и применяются
Политика конфиденциальности и Условия использования Google.
Для получения лицензии для вашего открытого
проекта заполните, пожалуйста, эту форму
* Нажимая на кнопку, вы даете согласие на обработку
своих персональных данных. См. Политику конфиденциальности

** На сайте установлена reCAPTCHA и применяются
Политика конфиденциальности и Условия использования Google.
Мне интересно попробовать плагин на:
* Нажимая на кнопку, вы даете согласие на обработку
своих персональных данных. См. Политику конфиденциальности

** На сайте установлена reCAPTCHA и применяются
Политика конфиденциальности и Условия использования Google.
Ваше сообщение отправлено.

Мы ответим вам на


Если вы так и не получили ответ, пожалуйста, проверьте папку
Spam/Junk и нажмите на письме кнопку "Не спам".
Так Вы не пропустите ответы от нашей команды.

>
>
>
Причины, по которым 64-битные программы…

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

07 Июн 2010

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

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

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

При разработке соглашений по вызовам (calling conventions) для архитектуры x86-64 решили положить конец существованию различных вариантов вызова функций. В Win32 существовал целый ряд соглашений о вызове: stdcall, cdecl, fastcall, thiscall и так далее. В Win64 только одно "родное" соглашение по вызовам. Модификаторы подобные __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. Составим рекурсивную функцию вида:

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 раза.

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

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

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

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

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-битному компилятору удалось использовать дополнительные регистры и построить более эффективный код, что позволило сократить количество используемой стековой памяти!

Выводы

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

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

Популярные статьи по теме
Как и почему статические анализаторы борются с ложными срабатываниями

Дата: 20 Мар 2017

Автор: Андрей Карпов

В своей предыдущей статье я писал, что мне не нравится подход, при котором статические анализаторы кода оцениваются с помощью синтетических тестов. В статье приводился пример, воспринимаемый анализат…
Статический анализ как часть процесса разработки Unreal Engine

Дата: 27 Июн 2017

Автор: Андрей Карпов

Проект Unreal Engine развивается - добавляется новый код и изменятся уже написанный. Неизбежное следствие развития проекта - появление в коде новых ошибок, которые желательно выявлять как можно раньш…
Как PVS-Studio оказался внимательнее, чем три с половиной программиста

Дата: 22 Окт 2018

Автор: Андрей Карпов

PVS-Studio, как и другие статические анализаторы кода, часто выдаёт ложные срабатывания. Но не стоит спешить считать странные срабатывания ложными. Это короткая история о том, как PVS-Studio вновь ок…
Любите статический анализ кода!

Дата: 16 Окт 2017

Автор: Андрей Карпов

Я в шоке от возможностей статического анализа кода, хотя сам участвую в разработке инструмента PVS-Studio. На днях я был искренне удивлён тому, что анализатор оказался умнее и внимательнее меня.
Главный вопрос программирования, рефакторинга и всего такого

Дата: 14 Апр 2016

Автор: Андрей Карпов

Вы угадали, ответ - "42". Здесь приводится 42 рекомендации по программированию, которые помогут избежать множества ошибок, сэкономить время и нервы. Автором рекомендаций выступает Андрей Карпов - тех…
Технологии, используемые в анализаторе кода PVS-Studio для поиска ошибок и потенциальных уязвимостей

Дата: 21 Ноя 2018

Автор: Андрей Карпов

Краткое описание технологий, используемых в инструменте PVS-Studio, которые позволяют эффективно обнаруживать большое количество паттернов ошибок и потенциальных уязвимостей. Статья описывает реализа…
Характеристики анализатора PVS-Studio на примере EFL Core Libraries, 10-15% ложных срабатываний

Дата: 31 Июл 2017

Автор: Андрей Карпов

После большой статьи про проверку операционной системы Tizen мне было задано много вопросов о проценте ложных срабатываний и о плотности ошибок (сколько ошибок PVS-Studio выявляет на 1000 строк кода)…
Зло живёт в функциях сравнения

Дата: 19 Май 2017

Автор: Андрей Карпов

Возможно, читатели помнят мою статью под названием "Эффект последней строки". В ней идёт речь о замеченной мной закономерности: ошибка чаще всего допускается в последней строке однотипных блоков текс…
Бесплатный PVS-Studio для тех, кто развивает открытые проекты

Дата: 22 Дек 2018

Автор: Андрей Карпов

В канун празднования нового 2019 года команда PVS-Studio решила сделать приятный подарок всем контрибьюторам open-source проектов, хостящихся на GitHub, GitLab или Bitbucket. Им предоставляется возмо…
Эффект последней строки

Дата: 31 Май 2014

Автор: Андрей Карпов

Я изучил множество ошибок, возникающих в результате копирования кода. И утверждаю, что чаще всего ошибки допускают в последнем фрагменте однотипного кода. Ранее я не встречал в книгах описания этого …

Комментарии (0)

Следующие комментарии

На сайте установлена reCAPTCHA и применяются
Политика конфиденциальности и Условия использования Google.
Этот сайт использует куки и другие технологии, чтобы предоставить вам более персонализированный опыт. Продолжая просмотр страниц нашего веб-сайта, вы принимаете условия использования этих файлов. Если вы не хотите, чтобы ваши данные обрабатывались, пожалуйста, покиньте данный сайт. Подробнее →
Принять