Comments 15
Вообще — откуда такой упор на -О0? Хотя бы -Og использовали…
+3
Хотелось максимально полного контроля: что написал, то и получил.
0
Для этого надо asm использовать. А то интринсики — это такое. Например, в произвольный момент времени (при использовании больше 6-8 SIMD-переменных) компилятор может напихать левых сохранений-восстановлений регистров через память.
Кстати, для последнего случая (с накоплением промежуточной суммы в SIMD-регистре) имеет смысл делать сравнение-накопление в двух регистрах по очереди, т.к. современные процессоры могут выполнять команды vpcmpeqw/vpand/vpaddw в количестве двух штук на такт (в отличии от vmovmskb, где только одну команду на такт), но с latency 1 такт. Соответственно, при исполнении через один регистр в теории теряется половина производительности. Правда на практике узким местом может быть уже чтение из памяти, но это надо на тесте проверять (и заодно и убедиться что компилятор не выкинул второй регистр, если тестовый код на интринсиках будет).
Кстати-2: можно заменить vpand с маской на vpsrlw на 15 разрядов. По производительности будет то же самое, но не нужно маску в отдельном регистре хранить. Пустячок, а приятно (и полезно в случае сложного кода, когда каждый регистр на счету).
Кстати, для последнего случая (с накоплением промежуточной суммы в SIMD-регистре) имеет смысл делать сравнение-накопление в двух регистрах по очереди, т.к. современные процессоры могут выполнять команды vpcmpeqw/vpand/vpaddw в количестве двух штук на такт (в отличии от vmovmskb, где только одну команду на такт), но с latency 1 такт. Соответственно, при исполнении через один регистр в теории теряется половина производительности. Правда на практике узким местом может быть уже чтение из памяти, но это надо на тесте проверять (и заодно и убедиться что компилятор не выкинул второй регистр, если тестовый код на интринсиках будет).
Кстати-2: можно заменить vpand с маской на vpsrlw на 15 разрядов. По производительности будет то же самое, но не нужно маску в отдельном регистре хранить. Пустячок, а приятно (и полезно в случае сложного кода, когда каждый регистр на счету).
0
когда каждый регистр на счету
Есть такие вещи как Register renaming, shadow register и "micro operations".
В AMD Zen сериях 1000 и 2000 инструкции AVX вроде как работают на 128 битных регистрах, т.ч. нужно делать два прохода.
Процессоры Intel снижают частоту при исполнении AVX.
Т.ч. всё сложно.
0
Есть такие вещи как Register renaming, shadow register
Вещи конечно есть, но их невозможно контролировать и они разные в разных сериях процессоров. А вот явно заявленные регистры есть всегда.
В AMD Zen сериях 1000 и 2000 инструкции AVX вроде как работают на 128 битных регистрах, т.ч. нужно делать два прохода.
Я имел мало опыта с АМД, но, судя по тестам Фога (некий известный в узких кругах Agner Fog) для Ryzen 7 1800X, немалое количество типовых операций делается с производительностью больше чем 1 регистр за такт, но с latency 1 такт, и для 256-разрядных регистров тоже. Т.е. 256-разрядные регистры имеет смысл использовать в любом случае, чтобы не терять производительность.
Процессоры Intel снижают частоту при исполнении AVX.
Т.ч. всё сложно.
Для этого и нужны тесты. Но по моему опыту, весьма немаленькому, на Интеле AVX2 даёт выигрыш всегда (хотя в ядрах с архитектурой Haswell этот выигрыш может быть совсем незначительным). Всё сложно — это с AVX512. Вот там действительно частота нехило снижается и другие важные тонкости есть.
Кстати, я что-то ступил в предыдущем предложении по модификации алгоритма с vpand с маской на vpsrlw. В итоге ни одна из этих операций вообще не нужна. Достаточно сделать vpcmpeqw, а затем vpsubw, т.к. по результатам сравнения мы получаем либо 0, либо -1 (0xFFFF).
0
А какая разница — vsubw или vandw? Всё равно если данных много переполнение будет…
0
Разница в одну инструкцию: первом случае vpcmpeqw -> vpand -> vpaddw, а во втором только vpcmpeqw -> vpsubw. По идее, это само по себе может 30% выигрыша дать на больших массивах.
А переполнение легко контролируется дополнительной вложенностью цикла. Т.е. делается цикл продолжительностью ARR_SIZE >> 20 (для случая 16 аккумуляторов в SIMD-регистре, по 16 разрядов каждый), внутри него цикл на 0x100000. И «хвост» продолжительностью ARR_SIZE & 0xFFFFF (на который также идёт переход, если ARR_SIZE < 0x100000). После каждого «малого» цикла значения аккумуляторов добавляются в регистр общего назначения нужной разрядности.
А переполнение легко контролируется дополнительной вложенностью цикла. Т.е. делается цикл продолжительностью ARR_SIZE >> 20 (для случая 16 аккумуляторов в SIMD-регистре, по 16 разрядов каждый), внутри него цикл на 0x100000. И «хвост» продолжительностью ARR_SIZE & 0xFFFFF (на который также идёт переход, если ARR_SIZE < 0x100000). После каждого «малого» цикла значения аккумуляторов добавляются в регистр общего назначения нужной разрядности.
0
Разница в одну инструкцию: первом случае vpcmpeqw -> vpand -> vpaddw, а во втором только vpcmpeqw -> vpsubw. По идее, это само по себе может 30% выигрыша дать на больших массивах.Не обратил внимания, что вы предлагаете vpand вставлять в цикл. Я думал там просто сложение… и в конце уже только, перед выдачей результата пользователю, вернуть минус сумму.
0
Не обратил внимания, что вы предлагаете vpand вставлять в цикл.
Так то не я, а автор статьи предлагает. Я как раз предлагаю лишнюю маску убрать.
и в конце уже только, перед выдачей результата пользователю, вернуть минус сумму
Можно и так, но один разряд аккумуляторов на знак уйдёт. Да и зачем лишние сложности в алгоритме, если можно просто поменять команду и сразу получать сумму с нужным знаком.
0
Из моей практики «что написал, то получил» — это примерно -O2. На -O3 начинается разворот циклов и прочие чудеса, которые, действительно, делают программу мало похожей на исходник, но вот -O0 сравнивать по скорости уж как-то совсем бессмысленно: бесконечные пересылки данных занимают куда больше времени, чем осмысленная деятельность.
Минимум, который имеет смысл сравнивать по скорости — это -Og, как я сказал: пересылки данных убиваются, по возможности, но программа остаётся линейной и инструкции не переставляются…
Минимум, который имеет смысл сравнивать по скорости — это -Og, как я сказал: пересылки данных убиваются, по возможности, но программа остаётся линейной и инструкции не переставляются…
+3
Что то мне подсказывает, что если просто включить -O2 или -O3 результат будет не сильно хуже. А если запустить профилирование то компилятор сгенерирует для выборки на N (где N явно большое) элементов даже более оптимальную реализацию чем можно написать руками.
Почему я так думаю, да потому что у вас тут ноль кода отвечающего за работу с кешем, загрузку из памяти, выделение памяти, переключение контекста и т. д. и т. п. Т.е. суть в том, что компилятор способен эффективно оптимизировать не только пользовательский код но и свою библиотеку и системные вызовы.
Собственно я не исключаю, что могу быть не прав. Но как то слабо верится, что до сих пор всё так плохо особенно если использовать профилирование.
Почему я так думаю, да потому что у вас тут ноль кода отвечающего за работу с кешем, загрузку из памяти, выделение памяти, переключение контекста и т. д. и т. п. Т.е. суть в том, что компилятор способен эффективно оптимизировать не только пользовательский код но и свою библиотеку и системные вызовы.
Собственно я не исключаю, что могу быть не прав. Но как то слабо верится, что до сих пор всё так плохо особенно если использовать профилирование.
0
Один программист по имени Wojciech Muła публикует статьи по практическому применению SIMD: http://0x80.pl/articles/index.html
Мне нравится его подход со сравнением разных реализаций для одной конкретной задачи.
+3
а можно ссылку на бенчмарки?
0
Sign up to leave a comment.
Ускоряем неускоряемое или знакомимся с SIMD, часть 2 — AVX