Строгое сглаживание и выравнивание памяти
У меня есть критический код производительности, и есть огромная функция, которая выделяет в начале функции 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:
Я использовал этот код для нескольких ОС/компиляторов, но начал возникать проблемы при переключении на новый NDK, основанный на GCC 4.6. Я получаю тот же плохой результат с GCC 4.7 (от NDK r8d)
Я упоминаю 32-байтовое выравнивание. Если у вас болят глаза, замените его любым другим номером, который вам нравится, например, 666, если это поможет. Нет абсолютно никакого смысла упоминать, что большинство архитектур не нуждаются в этом выравнивании. Если я выровняю 8 Кбайт локальных массивов в стеке, я потеряю 15 байтов для выравнивания по 16 байт, и я потеряю 31 для 32-байтового выравнивания. Надеюсь, это ясно, что я имею в виду.
Я говорю, что в критическом коде производительности есть 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 исправляет проблему, как и ожидалось в этом случае.
Ответы
Ответ 1
Просто отключите оптимизацию на основе псевдонимов и назовите его днем
Если ваши проблемы на самом деле вызваны оптимизациями, связанными с строгим псевдонимом, то -fno-strict-aliasing
решит проблему. Кроме того, в этом случае вам не нужно беспокоиться о потере оптимизации, потому что по определению эти оптимизации небезопасны для вашего кода, и вы не можете их использовать.
Хорошая точка Praetorian. Я вспоминаю одну истерику разработчика, вызванную введением анализа псевдонимов в gcc. Некоторое ядро Linux-автору хотелось (A) описать вещи, и (B) все еще получает эту оптимизацию. (Это упрощение, но, похоже, -fno-strict-aliasing
решило бы проблему, а не стоило бы дорого, и все они должны были поджарить другую рыбу.)
Ответ 2
Сначала я хотел бы сказать, что я определенно с вами, когда вы просите не гудеть о "стандартном нарушении", "зависящем от реализации" и т.д. Ваш вопрос абсолютно законный ИМХО.
Ваш подход к пакету всех массивов в пределах одного struct
также имеет смысл, что бы я сделал.
Неясно, из формулировки вопроса, которую вы видите "артефакты". Есть ли какой-либо ненужный код? Или несогласованность данных? Если это так, вы можете (надеюсь) использовать такие вещи, как STATIC_ASSERT
, чтобы во время компиляции обеспечить правильное выравнивание. Или, по крайней мере, иметь некоторое время выполнения ASSERT
при сборке отладки.
Как предложил Эрик Постшишил, вы можете считать эту структуру глобальной (если это применимо к делу, я имею в виду, что многопоточность и рекурсия не являются опцией).
Еще один момент, который я хотел бы заметить, - это так называемые стековые зонды. Когда вы выделяете много памяти из стека в одной функции (более 1 страницы, если быть точным), на некоторых платформах (таких как Win32) компилятор добавляет дополнительный код инициализации, известный как стековые пробники. Это может также иметь некоторое влияние на производительность (хотя, вероятно, и незначительное).
Кроме того, если вам не нужны все 40 массивов одновременно, вы можете разместить некоторые из них в union
. То есть у вас будет один большой struct
, внутри которого некоторый sub structs
будет сгруппирован в union
.
Ответ 3
Здесь есть несколько проблем.
Выравнивание: Мало что требует 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
, который вы интерпретируете как структуру, содержащую все ваши массивы, тогда в структуре нет сглаживания. Поэтому неясно, с какими проблемами вы сталкиваетесь.
Ответ 4
32-битное выравнивание звучит так, как если бы вы слишком сильно нажимали кнопку. Никакая инструкция ЦП не требует такого выравнивания. В принципе, достаточно приблизиться к самому большому типу данных вашей архитектуры.
C11 имеет концепцию fo 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, сделанный для этого, а затем указатели на символы (любой версии) всегда разрешены для псевдонимов любого другого указателя.