У меня есть критический код производительности, и есть огромная функция, которая выделяет в начале функции 40 массивов разного размера в стеке. Большинство этих массивов должны иметь определенное выравнивание (потому что эти массивы доступны в другом месте по цепочке с использованием инструкций процессора, для которых требуется выравнивание памяти (для процессоров Intel и плеч).
Так как некоторые версии gcc просто не могут правильно выровнять переменные стека (особенно для кода руки), или даже иногда говорят, что максимальное выравнивание для целевой архитектуры меньше, чем то, что действительно запрашивает мой код, у меня просто нет выбора, кроме как распределите эти массивы в стеке и выровняйте их вручную.
Итак, для каждого массива мне нужно сделать что-то подобное, чтобы правильно его выровнять:
short history_[HIST_SIZE + 32];
short * history = (short*)((((uintptr_t)history_) + 31) & (~31));
Таким образом, history
теперь выровнен по 32-байтовой границе. Выполнение этого же является утомительным для всех 40 массивов, плюс эта часть кода действительно интенсивна cpu, и я просто не могу сделать одну и ту же методику выравнивания для каждого из массивов (этот беспорядок выравнивания путает оптимизатор, а разное распределение регистров замедляет функцию большого времени, для лучшего объяснения см. объяснение в конце вопроса).
Итак... очевидно, что я хочу сделать это ручное выравнивание только один раз и предположить, что эти массивы расположены один за другим. Я также добавил дополнительное дополнение к этим массивам, так что они всегда кратно 32 байтам. Итак, тогда я просто создаю массив jumbo char в стеке и передаю его структуре, которая имеет все эти выровненные массивы:
struct tmp
{
short history[HIST_SIZE];
short history2[2*HIST_SIZE];
...
int energy[320];
...
};
char buf[sizeof(tmp) + 32];
tmp * X = (tmp*)((((uintptr_t)buf) + 31) & (~31));
Что-то вроде этого. Возможно, это не самый элегантный, но он дал действительно хороший результат, и ручной осмотр сгенерированной сборки доказывает, что сгенерированный код более или менее адекватен и приемлем. Система сборки была обновлена, чтобы использовать более новый GCC, и внезапно у нас появились некоторые артефакты в сгенерированных данных (например, выход из тестового набора проверки не является более точным даже в чистой сборке C с отключенным кодом asm). Потребовалось много времени, чтобы отладить проблему, и, похоже, она связана с правилами псевдонимов и новыми версиями GCC.
Итак, как я могу это сделать? Пожалуйста, не теряйте время, пытаясь объяснить, что он не стандартный, не портативный, undefined и т.д. (Я читал много статей об этом). Кроме того, я не могу изменить код (я бы, возможно, подумал о том, чтобы изменить GCC, чтобы исправить проблему, но не рефакторинг кода)... в основном, все, что я хочу, это применить какое-то черное магическое заклинание, чтобы новый GCC создает функционально такой же код для этого типа кода без отключения оптимизации?
Edit:
Я говорю, что в критическом коде производительности есть 40 массивов в стеке. Я, вероятно, также должен сказать, что это старый сторонний код, который работает хорошо, и я не хочу с ним связываться. Не нужно говорить, хорошо это или плохо, нечего делать.
Этот код/функция имеет хорошо протестированное и определенное поведение. У нас есть точные номера требований этого кода, например. он выделяет Xkb или RAM, использует Y kb статических таблиц и потребляет до Z kb пространства стека, и он не может измениться, так как код не будет изменен.
Говоря, что "выравнивание беспорядок путает оптимизатор", я имею в виду, что если я попытаюсь выровнять каждый массив отдельно, оптимизатор кода выделяет дополнительные регистры для кода выравнивания, а критически важные части кода производительности внезапно не имеют достаточного количества регистров и начинают обрабатывать стек, что приводит к замедлению кода. Такое поведение наблюдалось на процессорах ARM (кстати, я вообще не беспокоюсь об Intel).
По артефактам я имел в виду, что вывод становится не-битексным, добавляется некоторый шум. Либо из-за проблемы с псевдонимом типа, либо в компиляторе есть некоторая ошибка, которая в конечном итоге приводит к неправильному выводу функции.
Короче говоря, точка вопроса... как я могу выделить случайное количество пространства стека (используя char массивы или alloca
, а затем выровнять указатель на это пространство стека и переинтерпретировать этот кусок памяти как некоторая структура, которая имеет определенную правильную компоновку, которая гарантирует выравнивание определенных переменных до тех пор, пока сама структура будет правильно выровнена. Я пытаюсь использовать память, используя всевозможные подходы, я переношу выделение большого стека в отдельную функцию, но я все же получить плохую производительность и повреждение стека, я действительно начинаю все больше думать о том, что эта огромная функция попадает в какую-то ошибку в gcc. Это довольно странно, что, делая это бросок, я не могу получить эту вещь независимо от того, что я Кстати, я отключил все оптимизации, которые требуют какого-либо выравнивания, теперь это чистый код стиля C, но я получаю плохие результаты (сбой без битексакта и случайные сбои стека). Простое исправление, которое исправляет все это, я пишу вместо:
char buf[sizeof(tmp) + 32];
tmp * X = (tmp*)((((uintptr_t)buf) + 31) & (~31));
этот код:
tmp buf;
tmp * X = &buf;
тогда все ошибки исчезнут! Единственная проблема заключается в том, что этот код не выполняет правильное выравнивание для массивов и сбой при включении оптимизаций.
Интересное наблюдение:
Я упомянул, что этот подход хорошо работает и дает ожидаемый результат:
tmp buf;
tmp * X = &buf;
В каком-то другом файле я добавил автономную функцию noinline, которая просто выводит указатель void на эту структуру tmp *:
struct tmp * to_struct_tmp(void * buffer32)
{
return (struct tmp *)buffer32;
}
Изначально я думал, что если я брошу выделенную память с помощью to_struct_tmp, она обманет gcc для получения результатов, которые я ожидал получить, но все же выдает недопустимый вывод. Если я попытаюсь изменить рабочий код следующим образом:
tmp buf;
tmp * X = to_struct_tmp(&buf);
тогда я получаю тот же результат bad! WOW, что еще я могу сказать? Возможно, в соответствии с правилом строгого сглаживания gcc предполагает, что tmp * X
не относится к tmp buf
и удаляет tmp buf
как неиспользуемую переменную сразу после возврата из to_struct_tmp? Или делает что-то странное, что приводит к неожиданному результату. Я также попытался проверить сгенерированную сборку, однако изменение tmp * X = &buf;
до tmp * X = to_struct_tmp(&buf);
приводит к очень разному коду для функции, поэтому каким-то образом правило aliasing влияет на генерации кода большое время.
Вывод:
После всех видов тестирования у меня есть идея, почему я не могу заставить ее работать независимо от того, что я пытаюсь. Основываясь на строгом псевдониме типов, GCC считает, что статический массив не используется и поэтому не выделяет для него стек. Затем локальные переменные, которые также используют стек, записываются в то же место, где хранится моя структура tmp
; другими словами, моя jumbo-структура использует одну и ту же стек стека, как и другие переменные функции. Только это может объяснить, почему это всегда приводит к одному и тому же плохим результатам. -fno-strict-aliasing исправляет проблему, как и ожидалось в этом случае.