Влияет ли стековое пространство, требуемое функцией, на встроенные решения в C / C ++? - PullRequest
0 голосов
/ 14 февраля 2019

Может ли большой объем стекового пространства, требуемого функцией, помешать ее встроению?Например, если бы у меня в стеке был автоматический буфер размером 10 Кбайт, это уменьшило бы вероятность того, что функция была встроена?знаю.

Я знаю, что это не идеально, но мне очень любопытно.Код, вероятно, тоже довольно плохо работает с кешем.

Ответы [ 3 ]

0 голосов
/ 14 февраля 2019

Например, если бы у меня в стеке был автоматический буфер 10 Кбайт, это уменьшило бы вероятность встроенной функции?

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

Расширение "широкого" вызова в один кадр, который вызывает другие "широкие" функции, может быть проблемойхотя и если оптимизатор не защитит от этого отдельно, ему, возможно, придется избегать расширения «широких» функций в целом.

В случае рекурсии: Скорее всего, да.

Пример Источник LLVM :

if (IsCallerRecursive &&
         AllocatedSize > InlineConstants::TotalAllocaSizeRecursiveCaller) {
       InlineResult IR = "recursive and allocates too much stack space";

Из Источник GCC :

Для пределов роста стека мы всегда основываемрост использования стека абонентов.Мы хотим предотвратить сбои в приложениях при переполнении стека, когда функции с огромными кадрами стека встраиваются.

Управление пределом из GCC manual :

- имя параметра = значение

large-function-growth

  • Определяет максимальный рост большой функции, вызванный встраиванием в процентах.Например, значение параметра 100 ограничивает рост большой функции в 2,0 раза по сравнению с исходным размером.

кадр большого стека

  • Предел, задающий кадры большого стека.В то время как встраивание алгоритма пытается не превышать этот предел слишком много.

large-stack-frame-growth

  • Определяет максимальный рост кадров большого стека, вызванный встраиваниемв процентах.Например, значение параметра 1000 ограничивает рост кадра большого стека в 11 раз по сравнению с исходным размером.
0 голосов
/ 15 февраля 2019

Да, отчасти потому, что компиляторы выполняют выделение стека для всей функции один раз в прологе / эпилоге, не перемещая указатель стека при входе / выходе из областей блока.


и каждому встроенному вызову inlineme () потребуется свой собственный буфер.

Нет, я уверен, что компиляторы достаточно умны, чтобы повторно использовать одно и то же пространство стека для разных экземпляровта же функция, потому что только один экземпляр этой переменной C может быть одновременно включен в область действия.

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

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

Реальный пример проблемы, которую это может создать (чего в основном избегают эвристики компилятора):

Встраивание if (rare_special_case) use_much_stack() в рекурсивную функцию, которая в противном случае не использует много стека, было бы очевидной проблемой для производительности (больше пропусков кеша и TLB), и даже для правильности, если вы рекурсировали достаточно глубоко, чтобы фактически переполнитьсястек.

(Особенно в стесненной среде, такой как стеки ядра Linux, обычно 8 КБ или 16 КБ на поток, по сравнению с 4 КБ на 32-разрядных платформах в старых версиях Linux. https://elinux.org/Kernel_Small_Stacks содержит некоторую информацию иисторические цитаты о попытке избавиться от стеков 4k, чтобы ядру не приходилось находить 2 смежные физические страницы на задачу).

Компиляторы обычно заставляют функции выделять все пространство стека, которое им когда-либо понадобитсявпереди (кроме VLA и alloca) .Включение функции обработки ошибок или обработки особых случаев вместо ее вызова в редком случае, когда это необходимо , приведет к выделению большого стека (и часто к сохранению / восстановлению большего количества регистров с сохранением вызовов) в главномпролог / эпилог, , где он также влияет на быстрый путь .Особенно, если быстрый путь не вызывал никаких других вызовов функций.

Если вы не включите обработчик, то это пространство стека никогда не будет использовано, если нет ошибок (или особый случай не сделалбывает).Таким образом, быстрый путь может быть более быстрым, с меньшим количеством инструкций push / pop и без выделения больших буферов перед продолжением вызова другой функции.(Даже если сама функция на самом деле не является рекурсивной, выполнение этого в нескольких функциях в глубоком дереве вызовов может привести к потере большого количества стека.)

Я читал, что ядро ​​Linux делает вручнуювыполните эту оптимизацию в нескольких ключевых местах, где встроенная эвристика gcc принимает нежелательное решение для встроенного: разбивает функцию на быстрый путь с вызовом медленного пути и использует __attribute__((noinline)) набольшая функция медленного пути, позволяющая убедиться, что она не встроена.


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

Если вы выполняли сохранение и / или выделение внутри условного блока перед запуском какого-либо общего кода, который был достигнут любым способом (с другой ветвью, чтобы решить, какие регистры восстановить в эпилоге), тогда не было бы никакого способа длямеханизм обработки исключений, чтобы знать, загружать ли R12 или R13 (например) из того места, где эта функция их сохранила, без какого-либо безумно сложного формата метаданных, который мог бы сигнализировать о регистре или ячейке памяти, которые необходимо проверить для какого-либо условия.Раздел .eh_frame в исполняемых файлах / библиотеках ELF достаточно раздут!(Это не является обязательным, BTW. X86-64 System V ABI (например) требует его даже в коде, который не поддерживает исключения, или в C. В некотором смысле это хорошо, потому что это означает, что обратные трассировки обычно работают, даже передаваярезервное копирование исключительной ситуации через функцию может привести к сбою.)

Вы можете точно настроить указатель стека внутри условного блока.Код, скомпилированный для 32-битного x86 (с дрянными соглашениями о вызовах стековых аргументов), может использовать push даже внутри условных ветвей.Так что, пока вы очищаете стек перед тем, как покинуть блок, который выделил пространство, это выполнимо.Это не сохранение / восстановление регистров, а перемещение указателя стека.(В функциях, созданных без указателя кадра, метаданные размотки должны записывать все такие изменения, поскольку указатель стека является единственной ссылкой для поиска сохраненных регистров и адреса возврата.)

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


Похожие: Рэймонд Чен опубликовал блог о соглашении о вызовах PowerPC и о том, как существуют конкретные требования к прологам / эпилогам функций, которые заставляют работать разматывание стека.(И правила подразумевают / требуют наличия красной зоны под указателем стека, которая защищена от асинхронного клоббера. В некоторых других соглашениях о вызовах используются красные зоны, такие как x86-64 System V, но в Windows x64 нет. Raymond posts еще один блог о красных зонах )

0 голосов
/ 14 февраля 2019

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

Сравните эту версию с массивом 10000 символов не , если он встроен (GCC 8.2, x64, -O2):

inline int inlineme(int args) {
  char svar[10000];

  return stringyfunc(args, svar);
}

int test(int x) {
    return inlineme(x);
}

Генерируемая сборка:

inlineme(int):
        sub     rsp, 10008
        mov     rsi, rsp
        call    stringyfunc(int, char*)
        add     rsp, 10008
        ret
test(int):
        jmp     inlineme(int)

с , этот с гораздо меньшим массивом из 10 символов, который равен :

inline int inlineme(int args) {
  char svar[10];

  return stringyfunc(args, svar);
}

int test(int x) {
    return inlineme(x);
}

Генерируемая сборка:

test(int):
        sub     rsp, 24
        lea     rsi, [rsp+6]
        call    stringyfunc(int, char*)
        add     rsp, 24
        ret
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...