Объективная разница между регистром и указателем в инструкциях AVX - PullRequest
0 голосов
/ 09 октября 2018

Сценарий: вы пишете сложный алгоритм с использованием SIMD.Используется несколько констант и / или редко меняющихся значений.В конечном итоге алгоритм использует более 16 ymm, что приводит к использованию указателей стека (например, код операции содержит vaddps ymm0,ymm1,ymmword ptr [...] вместо vaddps ymm0,ymm1,ymm7).

Для того, чтобы алгоритм вписался вВ доступных регистрах константы могут быть «встроенными».Например:

const auto pi256{ _mm256_set1_ps(PI) };
for (outer condition)
{
    ...
    const auto radius_squared{ _mm256_mul_ps(radius, radius) };
    ...
    for (inner condition)
    {
        ...
        const auto area{ _mm256_mul_ps(radius_squared, pi256) };
        ...
    }
}

... становится ...

for (outer condition)
{
    ...
    for (inner condition)
    {
        ...
        const auto area{ _mm256_mul_ps(_mm256_mul_ps(radius, radius), _mm256_set1_ps(PI)) };
        ...
    }
}

Является ли рассматриваемая доступная переменная константой или нечасто вычисляется (вычисляется внешним циклом), как это сделать?определить, какой подход обеспечивает лучшую пропускную способность?Это вопрос какой-то концепции, такой как «ptr добавляет 2 дополнительные задержки»?Или он является недетерминированным, так что он отличается в каждом конкретном случае и может быть полностью оптимизирован только методом проб и ошибок + профилирования?

1 Ответ

0 голосов
/ 09 октября 2018

Хороший оптимизирующий компилятор должен генерировать одинаковый машинный код для обеих версий.Просто определите ваши векторные константы как локальные или используйте их анонимно для максимальной читабельности;позвольте компилятору позаботиться о распределении регистров и выберите самый дешевый способ справиться с исчерпанием регистров, если это произойдет.

Лучше всего помогать компилятору, если это возможно, использовать меньше различных констант.например, вместо _mm_and_si128 с set1_epi16(0x00FF) и 0xFF00, используйте _mm_andn_si128, чтобы замаскировать другой способ.Обычно вы ничего не можете сделать, чтобы повлиять на то, что он решает хранить в регистрах, а не на, но, к счастью, компиляторы довольно хороши в этом, потому что это также важно для скалярного кода.


Компилятор подниметконстанты вне цикла (даже встраивая вспомогательную функцию, содержащую константы) или, если они используются только в одной стороне ветви, перенести установку в эту сторону ветви.

Исходный код вычисляет точно то же самоебез каких-либо различий в видимых побочных эффектах, поэтому правило «как будто» дает компилятору свободу делать это.


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

Когда он обнаруживает, что не имеет достаточного количества регистров для хранения всех переменных и констант вregs внутри цикла, первый выбор для чего-то, чтобы не keep в регистре обычно является вектором, инвариантным к циклу, либо константой времени компиляции, либо чем-то, вычисляемым до цикла.

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

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

Это вопрос некоторыхКонцепция типа «ptr добавляет 2 дополнительные задержки»?

Микросинтезирование операнда источника памяти не удлиняет критический путь от непостоянного входа к выходу.Load uop может начаться, как только адрес будет готов, а для векторных констант это обычно либо режим относительной RIP, либо [rsp+constant] адресации.Поэтому обычно загрузка готова к выполнению, как только она поступает в вышедшую из строя часть ядра.Предполагая попадание в кэш L1d (поскольку он будет оставаться горячим в кэше, если загружается при каждой итерации цикла), это всего ~ 5 циклов, поэтому он легко будет готов во времени, если на входе векторного регистра будет узкое место в цепочке зависимостей.

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

...