У меня есть критичный к производительности код, и есть огромная функция, которая выделяет около 40 массивов разного размера в стеке в начале функции. Большинство из этих массивов должны иметь определенное выравнивание (потому что к этим массивам обращаются где-то еще по цепочке, используя инструкции процессора, которые требуют выравнивания памяти (для процессоров Intel и arm).
Так как некоторые версии gcc просто не в состоянии правильно выровнять переменные стека (особенно для кода руки), или даже иногда он говорит, что максимальное выравнивание для целевой архитектуры меньше, чем то, что фактически запрашивает мой код, у меня просто нет выбора, кроме как выделить эти массивы в стеке и выровнять их вручную.
Итак, для каждого массива мне нужно сделать что-то подобное, чтобы правильно выровнять его:
short history_[HIST_SIZE + 32];
short * history = (short*)((((uintptr_t)history_) + 31) & (~31));
Сюда, history
теперь выровнен по 32-байтовой границе. Делать то же самое утомительно для всех 40 массивов, плюс эта часть кода действительно интенсивно использует процессор, и я просто не могу сделать одну и ту же технику выравнивания для каждого из массивов (этот беспорядок выравнивания сбивает с толку оптимизатор, и различное распределение регистров сильно замедляет работу функции , для лучшего объяснения см. объяснение в конце вопроса).
Итак … очевидно, я хочу выполнить это ручное выравнивание только один раз и предположить, что эти массивы расположены один за другим. Я также добавил дополнительные отступы к этим массивам, чтобы они всегда были кратны 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.
Итак, как я могу это сделать? Пожалуйста, не тратьте время на попытки объяснить, что это не стандартное, не переносимое, неопределенное и т. Д. (Я читал много статей об этом). Кроме того, я не могу изменить код (возможно, я бы подумал об изменении GCC, чтобы исправить проблему, но не о рефакторинге кода) … в принципе, все, что я хочу, — это применить какое-то заклинание черной магии, чтобы более новый GCC производит функционально такой же код для этого типа кода без отключения оптимизации?
Редактировать:
Короче говоря, суть вопроса … как я могу выделить случайное количество стекового пространства (используя массивы символов или 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, которая просто приводит пустой указатель на эту структуру tmp *:
struct tmp * to_struct_tmp(void * buffer32)
{
return (struct tmp *)buffer32;
}
Первоначально я думал, что если я приведу распределенную память с помощью to_struct_tmp, это обманет gcc, чтобы получить результаты, которые я ожидал получить, но все равно он выдаст недопустимый вывод. Если я попытаюсь изменить рабочий код таким образом:
tmp buf;
tmp * X = to_struct_tmp(&buf);
тогда я получаю то же самое плохой результат! ВАУ, что еще я могу сказать? Возможно, на основе строгого правила псевдонимов gcc предполагает, что tmp * X
не связано с tmp buf
и удалил tmp buf
как неиспользованная переменная сразу после возврата из to_struct_tmp? Или делает что-то странное, что дает неожиданный результат. Я также пытался проверить сгенерированную сборку, однако, изменяя tmp * X = &buf;
в tmp * X = to_struct_tmp(&buf);
создает совершенно другой код для функции, так что каким-то образом это правило псевдонимов влияет на генерацию кода.
Заключение:
После всех видов тестирования у меня есть идея, почему, возможно, я не могу заставить его работать независимо от того, что я пытаюсь. Основываясь на строгом совмещении типов, GCC считает, что статический массив не используется, и поэтому не выделяет для него стек. Затем локальные переменные, которые также используют стек, записываются в то же место, где мой tmp
структура хранится; другими словами, моя структура jumbo разделяет ту же стековую память, что и другие переменные функции. Только это может объяснить, почему это всегда приводит к одному и тому же плохому результату. -fno-strict-aliasing исправляет проблему, как и ожидалось в этом случае.
Если ваши проблемы на самом деле вызваны оптимизацией, связанной со строгим псевдонимом, то -fno-strict-aliasing
решит проблему. Кроме того, в этом случае вам не нужно беспокоиться о потере оптимизации, потому что, по определению, эти оптимизации небезопасны для вашего кода, и вы не может используй их.
Хорошая мысль преторианец. Я вспоминаю истерию одного разработчика, вызванную введением анализа псевдонимов в gcc. Определенный автор ядра Linux хотел (A) использовать псевдонимы, и (B) все же получить эту оптимизацию. (Это упрощение, но, похоже, -fno-strict-aliasing
решит проблему, не будет стоить дорого, и все они должны были жарить другую рыбу.)
Во-первых, я хотел бы сказать, что я определенно с вами, когда вы просите не жужжать о «стандартных нарушениях», «зависящих от реализации» и т. Д. Ваш вопрос абсолютно правомерен, ИМХО.
Ваш подход, чтобы упаковать все массивы в один struct
также имеет смысл, это то, что я бы сделал.
Из формулировки вопроса неясно, какие «артефакты» вы наблюдаете. Сгенерирован ли какой-либо ненужный код? Или смещение данных? Если последний случай — вы можете (надеюсь) использовать такие вещи, как STATIC_ASSERT
во время компиляции убедиться, что все выровнено правильно. Или, по крайней мере, иметь некоторое время выполнения ASSERT
при отладочной сборке.
Как предложил Эрик Постпишил, вы можете рассмотреть возможность объявления этой структуры как глобальной (если это применимо для данного случая, я имею в виду многопоточность и рекурсию не вариант).
Еще один момент, на который я хотел бы обратить внимание, это так называемые стековые тесты. Когда вы выделяете много памяти из стека в одной функции (точнее, более 1 страницы) — на некоторых платформах (таких как Win32) компилятор добавляет дополнительный код инициализации, известный как стековые зонды. Это также может оказать некоторое влияние на производительность (хотя, вероятно, будет незначительным).
Кроме того, если вам не нужны все 40 массивов одновременно, вы можете расположить некоторые из них в union
, То есть у вас будет один большой struct
внутри которого некоторыеstructs
будет сгруппирован в union
,
Здесь есть ряд вопросов.
Выравнивание: Мало что требует 32-байтового выравнивания. 16-байтовое выравнивание выгодно для типов SIMD на современных процессорах Intel и ARM. При использовании AVX на современных процессорах Intel затраты на производительность при использовании адресов, которые выровнены по 16 байтам, но не выровнены по 32 байтам, обычно незначительны. Может быть большой штраф для 32-байтовых хранилищ, которые пересекают строку кэша, поэтому здесь может быть полезно 32-байтовое выравнивание. В противном случае 16-байтовое выравнивание может подойти. (На OS X и iOS, malloc
возвращает 16-байтовую выровненную память.)
Распределение в критическом коде: Вы должны избегать выделения памяти в критичном для производительности коде. Как правило, память должна выделяться в начале программы или до начала работы, критичной к производительности, и повторно использоваться во время кода, критичного к производительности. Если вы выделяете память до того, как начнется критичный для производительности код, то время, которое требуется для выделения и подготовки памяти, по существу не имеет значения.
Большие, многочисленные массивы в стеке: Стек не предназначен для больших выделений памяти, и существуют ограничения для его использования. Даже если вы не столкнетесь с проблемами сейчас, очевидно, что несвязанные изменения в вашем коде в будущем могут взаимодействовать с использованием большого количества памяти в стеке и вызывать переполнение стека.
Многочисленные массивы: 40 массивов это много. Если все они не используются одновременно для разных данных и обязательно так, вам следует повторно использовать одно и то же пространство для разных данных и целей. Излишнее использование разных массивов может привести к большему количеству кэш-памяти, чем необходимо.
Оптимизация: Непонятно, что вы имеете в виду, говоря, что «беспорядок выравнивания сбивает с толку оптимизатор, а разное распределение регистров замедляет работу функции». Если у вас есть несколько автоматических массивов внутри функции, я бы ожидал, что оптимизатор узнает, что они различаются, даже если вы извлекаете указатели из массивов по адресной арифметике. Например, данный код, такой как a[i] = 3; b[i] = c[i]; a[i] = 4;
Я ожидаю, что оптимизатор будет знать, что a
, b
, а также c
разные массивы, и, следовательно, c[i]
не может быть таким же, как a[i]
, так что это нормально, чтобы устранить a[i] = 3;
, Возможно, у вас проблема в том, что с 40 массивами у вас есть 40 указателей на массивы, поэтому компилятор заканчивает тем, что перемещает указатели в регистры и из них?
В этом случае повторное использование меньшего количества массивов для разных целей может помочь уменьшить это. Если у вас есть алгоритм, который на самом деле использует 40 массивов одновременно, то вы можете посмотреть на реструктуризацию алгоритма, чтобы он использовал меньше массивов за раз. Если алгоритм должен указывать на 40 различных мест в памяти, то вам, по сути, нужно 40 указателей, независимо от того, где или как они расположены, и 40 указателей больше, чем доступные регистры.
Если у вас есть другие вопросы по поводу оптимизации и использования регистра, вы должны быть более конкретными о них.
Псевдонимы и артефакты: Вы сообщаете о некоторых проблемах с псевдонимами и артефактах, но не предоставляете достаточно подробных сведений для их понимания. Если у вас есть один большой char
массив, который вы интерпретируете как структуру, содержащую все ваши массивы, тогда в структуре нет псевдонимов. Так что не ясно, с какими проблемами вы сталкиваетесь.
32-байтовое выравнивание звучит так, как будто вы нажимаете кнопку слишком далеко. Никакая инструкция ЦП не должна требовать такого выравнивания. По сути, должно быть достаточно выравнивания по ширине как самый большой тип данных вашей архитектуры.
С11 имеет концепцию для maxalign_t
, который является фиктивным типом максимального выравнивания для архитектуры. Если у вашего компилятора его еще нет, вы можете легко смоделировать его
union maxalign0 {
long double a;
long long b;
... perhaps a 128 integer type here ...
};
typedef union maxalign1 maxalign1;
union maxalign1 {
unsigned char bytes[sizeof(union maxalign0)];
union maxalign0;
}
Теперь у вас есть тип данных, который имеет максимальное выравнивание вашей платформы и по умолчанию инициализируется со всеми байтами, установленными в 0
,
maxalign1 history_[someSize];
short * history = history_.bytes;
Это позволяет избежать ужасных вычислений адресов, которые вы делаете в настоящее время, вам нужно будет только принять некоторые someSize
принять во внимание, что вы всегда выделяете кратные sizeof(maxalign1)
,
Также убедитесь, что у этого нет проблем с наложением. Прежде всего unions
в C, сделанном для этого, и тогда указателям символов (любой версии) всегда разрешено псевдоним любого другого указателя.