SSE избыточен или не рекомендуется?
Просматривая здесь и в интернете, я могу найти много сообщений о современных компиляторах, опережающих SSE во многих реальных ситуациях, и я только что столкнулся с некоторым кодом, который унаследовал, когда я отключал некоторый код SSE, написанный в 2006 году, для обработки изображений на основе целых чисел и заставить код вниз стандартной ветви C, он работает быстрее.
На современных процессорах с несколькими ядрами и передовой конвейерной обработкой и т. Д. Старый SSE-код работает хуже gcc -O2
?
5 ответов
Вы должны быть осторожны с микробенчмарками. Это действительно легко измерить что-то другое, чем вы думали. Микробенчмарки также обычно вообще не учитывают размер кода с точки зрения давления на записи L-I-cache / uop-cache и предсказателя ветвлений.
В большинстве случаев микробенчмарки обычно предсказывают все ответвления настолько хорошо, насколько это возможно, в то время как подпрограмма, которая вызывается часто, но не в узком цикле, может не сработать на практике.
За эти годы было много дополнений к SSE. Разумным базовым уровнем для нового кода является SSSE3 (встречается в Intel Core2 и более поздних версиях, а также AMD Bulldozer и более поздних версиях), если существует скалярный запасной вариант. Добавление быстрого байтового перемешивания (pshufb
) меняет правила игры для некоторых вещей. SSE4.1 добавляет довольно много полезных вещей и для целочисленного кода. Если старый код не использует его, вывод компилятора или новый рукописный код могут работать намного лучше.
В настоящее время мы работаем с AVX2, который обрабатывает две 128-битные линии одновременно в 256-битных регистрах. Есть несколько инструкций 256b тасования. AVX/AVX2 предоставляет 3-операндные (неразрушающие dest, src1, src2) версии всех предыдущих инструкций SSE, что помогает улучшить плотность кода, даже когда двухполосный аспект использования операций 256b является недостатком (или при ориентации на AVX1 без AVX2 для целочисленного кода).
Через год или два, вероятно, появится первое настольное оборудование AVX512. Это добавляет огромное количество мощных функций (маска регистров и заполнение большего количества пробелов в сильно неортогональном наборе команд SSE / AVX), а также просто более широкие регистры и исполнительные блоки.
Если старый код SSE давал лишь предельное ускорение по сравнению со скалярным кодом в тот момент, когда он был написан, или никто никогда не тестировал его, это может быть проблемой. Достижения компилятора могут привести к тому, что сгенерированный код для скалярного C превзойдет старый SSE, что потребует много усилий. Иногда стоимость перетаскивания данных в векторные регистры съедает все ускорение, как только они появляются.
Или, в зависимости от параметров вашего компилятора, компилятор может даже автоматически векторизоваться. IIRC, gcc -O2
не включает -ftree-vectorize
так что тебе нужно -O3
для авто-vec.
Еще одна вещь, которая может сдерживать старый код SSE, это то, что он может предполагать, что невыровненные загрузки / хранилища выполняются медленно и используются palignr
или аналогичные методы для перехода между невыровненными данными в регистрах и выровненными загрузками / хранилищами. Таким образом, старый код может быть настроен для старого микроарха таким способом, который на самом деле медленнее для последних.
Поэтому даже без использования каких-либо инструкций, которые ранее не были доступны, настройка для другой микроархитектуры имеет значение.
Выход компилятора редко бывает оптимальным, особенно если вы не сказали об указателях, которые не имеют псевдонимов (restrict
) или выравнивание. Но часто удается бежать довольно быстро. Вы часто можете немного улучшить его (особенно для того, чтобы быть более дружественным к гиперпоточности, имея меньше мопов /insns для выполнения той же работы), но вы должны знать микроархитектуру, на которую вы ориентируетесь. Например, Intel Sandybridge и более поздние версии могут использовать только операнды памяти с микроплавким предохранителем с режимом адресации одного регистра. Другие ссылки на вики x86.
Таким образом, чтобы ответить на заголовок, ни один набор инструкций SSE никоим образом не является избыточным или обескураженным. Использование его напрямую, с asm, не рекомендуется для случайного использования (используйте вместо этого встроенные функции). Использование встроенных функций не рекомендуется, если вы не можете получить ускорение по сравнению с выходом компилятора. Если они связаны сейчас, будущему компилятору будет проще работать с вашим скалярным кодом еще лучше, чем с вашими встроенными векторами.
Чтобы добавить к уже превосходному ответу Peter Cordes, необходимо учитывать один фундаментальный момент: компилятор не знает всего, что программист знает о проблемной области, и у программиста вообще нет простого способа выразить полезные ограничения и другую соответствующую информацию. что действительно умный компилятор может быть в состоянии использовать для облегчения векторизации. Это может дать программисту огромное преимущество во многих случаях.
Например, для простого случая, такого как:
// add two arrays of floats
float a[N], b[N], c[N];
for (int i = 0; i < N; ++i)
a[i] = b[i] + c[i];
любой достойный компилятор должен быть в состоянии сделать достаточно хорошую работу по векторизации этого с помощью SSE/AVX/ что угодно, и было бы мало смысла реализовывать это с помощью встроенных SIMD. Помимо относительно незначительных проблем, таких как выравнивание данных или вероятный диапазон значений для N, сгенерированный компилятором код должен быть близок к оптимальному.
Но если у вас есть что-то менее простое, например,
// map array of 4 bit values to 8 bit values using a LUT
const uint8_t LUT[16] = { 0, 1, 3, 7, 11, 15, 20, 27, ..., 255 };
uint8_t in[N]; // 4 bit input values
uint8_t out[N]; // 8 bit output values
for (int i = 0; i < N; ++i)
out[i] = LUT[in[i]];
вы не увидите никакой авто-векторизации из вашего компилятора, потому что (а) он не знает, что вы можете использовать PSHUFB
реализовать небольшую LUT, и (b), даже если бы она это сделала, она не может знать, что входные данные ограничены 4-битным диапазоном. Таким образом, программист может написать простую реализацию SSE, которая, скорее всего, будет на порядок быстрее:
__m128i vLUT = _mm_loadu_si128((__m128i *)LUT);
for (int i = 0; i < N; i += 16)
{
__m128i va = _mm_loadu_si128((__m128i *)&b[i]);
__m128i vb = _mm_shuffle_epi8(va, vLUT);
_mm_storeu_si128((__m128i *)&a[i], vb);
}
Возможно, еще через 10 лет компиляторы будут достаточно умны, чтобы делать подобные вещи, и языки программирования будут иметь методы для выражения всего, что программист знает о проблеме, данных и других соответствующих ограничениях, и в этот момент, вероятно, наступит время люди, как я, чтобы рассмотреть новую карьеру. Но до тех пор будет оставаться большое проблемное пространство, где человек все еще может легко победить компилятор с ручной оптимизацией SIMD.
Это были два отдельных и строго говоря не связанных вопроса:
1) Стали ли SSE в целом и кодированные базы SSE в частности устаревшими / "обескураженными" / выбывшими?
Ответ вкратце: пока нет и не совсем. Причина высокого уровня: потому что вокруг все еще достаточно оборудования (даже в домене HPC, где можно легко найти Nehalem), у которого есть только SSE * на борту, но нет AVX *. Если вы посмотрите за пределы HPC, рассмотрите, например, процессор Intel Atom, который в настоящее время поддерживает только до SSE4.
2) Почему gcc -O2 (то есть автоматически векторизованный, работающий на оборудовании только для SSE) быстрее, чем какая-то старая (предположительно внутренняя) реализация SSE, написанная 9 лет назад.
Ответ: это зависит, но в первую очередь очень активно совершенствуются на стороне компиляторов. За последние 9 лет лучшие команды разработчиков компиляторов x86 в AFAIK за последние 9 лет сделали огромные и огромные инвестиции в области автоматической векторизации или явной векторизации. И причина, по которой они это сделали, также ясна: потенциал SIMD "FLOP" в аппаратном обеспечении x86 был увеличен (формально) "в 8 раз" (то есть в 8 раз пиковых провалов SSE4) в течение последних 9 лет.
Позвольте мне задать еще один вопрос:
3) ОК, SSE не устарел. Но устареет ли он через X лет?
Ответ: кто знает, но, по крайней мере, в HPC, с более широким внедрением аппаратного обеспечения, совместимого с AVX-2 и AVX-512, встроенные кодовые базы SSE с большой вероятностью скоро уйдут в отставку, хотя это опять-таки зависит от того, что вы разрабатываете. Некоторые оптимизированные низкоуровневые библиотеки мультимедиа HPC/HPC+, вероятно, будут долго сохранять хорошо настроенные паттерны кода SSE.
Вы могли бы очень хорошо видеть, что современные компиляторы используют SSE4. Но даже если они придерживаются того же ISA, они часто намного лучше в планировании. Занятость блоков SSE означает тщательное управление потоковой передачей данных.
Ядра не имеют значения, так как каждый поток инструкций (поток) работает на одном ядре.
Да, но в основном в том смысле, что написание встроенной сборки не рекомендуется.
Инструкции SSE (и другие векторные инструкции) существовали достаточно долго, и теперь компиляторы хорошо понимают, как их использовать для генерации эффективного кода.
Вы не будете выполнять работу лучше, чем компилятор, если у вас нет четкого представления о том, что вы делаете. И даже тогда это часто не будет стоить усилий, потраченных на попытки обойти компилятор. И даже тогда наши усилия по оптимизации для одного конкретного процессора могут не привести к хорошему коду для других процессоров.