Есть в программистском фольклоре два поверья:
1) хочешь скорости - пиши на Си
2) нет смысла писать на асме, компилятор и так умный
Столкнувшись в недавнем проекте с явным недостатком ума компилятора, решил по мотивам реального кода сделать микробенчмарк и посмотреть, как вообще разные компиляторы с ним справляются. Задачка очень простая: есть два массива 16-битных целых чисел, найти сумму квадратов разностей. В оригинале это были блоки 16х16, и проходить их надо было в двойном цикле. Такой вот код:
Данные в задаче таковы, что разность помещается в short, это не проблема. Цикл такой прогоняю 10 млн раз и смотрю время, а также смотрю, что за код сгенерировался.
MSVC6 (1998 год) делает все в лоб: два цикла по 16 итераций, внутри чтение слов из памяти с конвертацией в 32 бита, затем imul и сложение. 4 с лишним секунды.
Наступает 21 век, в 2001 году появляется набор инструкций SSE2, позволяющий ряд операций делать над 16 байтами сразу, в частности вычитать 8 short'ов за раз. С умножением сложнее: результат умножения short'ов - int, нельзя умножить 8 пар short'ов и получить 8 интов, т.к. в 128-битный регистр они не поместятся. Можно одной операцией либо перемножить и взять нижние 16 бит результатов, либо верхние 16 бит, либо 8 интов-результатов попарно сложить и получить 4 инта, уже помещающиеся в регистр.
Для использования команд SSE есть интринсики. Код с ними выглядит так:
Отрабатывает те же 10 млн повторов за полсекунды. Но ручного написания интринсиков хочется избежать, у нас же есть умные компиляторы, да?
MSVC8 (2006 год). В опциях компилятора есть гордый пункт про SSE2. Включаем, компиляем. 3 с лишним секунды. Двойной цикл, внутренний unroll'ен по 4 итерации, внутри те же imul. SSE? Не, не слышал.
MSVC10 (2010 год). У студии новый WPFный интерфейс, но только не у окна настроек С++ проекта (настораживает?). Включаем нужные опции, компиляем. Тот же двойной цикл с разворачиванием внутреннего по 4 итерации, те же imul. Те же 3 секунды. SSE? Не, не слышал.
Intel Compiler 7.1 (2003 год). Полностью разворачивает внутренний цикл. 2 секунды. SSE? А что это?
Даем подсказку: вместо двойного цикла по 16 итераций делаем один на 256 итераций. Тут вдруг интеловский компилятор оживает и сам векторизует код ровно так же, как в примере с интринсиками. Укладывается в полсекунды.
Аналогичная подсказка MSVC всех версий ничего не дает, они продолжают разворачивать максимум по 4 итерации, никакого SSE.
Intel Compiler 11 (2009 год). Одинарный цикл векторизует так же хорошо, как 7-й. В двойном цикле на этот раз догадывается хотя бы векторизовать внутренний, но тупит с выравниваниями, вставляя ненужный и неиспользуемый код по краям. 1 секунда.
GCC 4.7.0 (2012 год). В двойном цикле полностью разворачивает внутренний, внутри imul, все как у intel 7. Те же 2 секунды. SSE? Не, не слышал. Если сделать цикл одинарным, сдюживает его векторизовать, но делает это плохо: вместо попарного сложения результатов умножения отдельно вычисляет верхние и нижние биты результатов, потом возится с перестановками и перепаковками байтов в регистрах. 1 секунда, вдвое хуже интела и варианта с ручной векторизацией.
Может, другие языки лучше? Что у нас после С? D? DMD 2.059, 2012 год, двойной цикл по 16 итераций, imul, т.е. код на уровне vc6. Ожидаемые 4 секунды.
Ну это маргинальщина, на самом деле после С/С++ идет C#. Молва гласит, что якобы JIT позволяет генерить код под конкретный процессор, используя доступные наборы инструкций. В теории это действительно так. Переписываем код на C# собираем в VS10 под .NET4. 6 секунд двойной цикл, 5 секунд одинарный. SSE? Хз, но чота не похоже. О, сборка была для x86. Пробуем для x64: 4 секунды на двойной цикл, 2 с половиной на одинарный. Получше, но до нормального невекторизованного С++ кода не дотягивает.
Ладно, надоели си-подобные языки, вот на днях вышел Haskell Platform 2012.2. Там GHC 7.4.1, берем Data.Vector.Unboxed Int16. Если верить бенчмаркам criterion, 10 млн повторов будут работать 16 секунд.Ибо внутри внезапно часть вычислений делаются с плавающей точкой (если я правильно их нашел) (не правильно).
Мораль: со всей объективностью одного микробенчмарка :) можно заявить, что спустя 10 лет после появления SSE2 компиляторы - все еще говно, и авторы того же x264 не зря пишут на ассемблере.
1) хочешь скорости - пиши на Си
2) нет смысла писать на асме, компилятор и так умный
Столкнувшись в недавнем проекте с явным недостатком ума компилятора, решил по мотивам реального кода сделать микробенчмарк и посмотреть, как вообще разные компиляторы с ним справляются. Задачка очень простая: есть два массива 16-битных целых чисел, найти сумму квадратов разностей. В оригинале это были блоки 16х16, и проходить их надо было в двойном цикле. Такой вот код:
__declspec(align(16)) short a[256], b[256];
int sum = 0, j=0;
for(int y=0;y<16;y++)
for(int x=0;x<16;x++) {
short v = a[j] - b[j];
sum += v*v;
j++;
}Данные в задаче таковы, что разность помещается в short, это не проблема. Цикл такой прогоняю 10 млн раз и смотрю время, а также смотрю, что за код сгенерировался.
MSVC6 (1998 год) делает все в лоб: два цикла по 16 итераций, внутри чтение слов из памяти с конвертацией в 32 бита, затем imul и сложение. 4 с лишним секунды.
Наступает 21 век, в 2001 году появляется набор инструкций SSE2, позволяющий ряд операций делать над 16 байтами сразу, в частности вычитать 8 short'ов за раз. С умножением сложнее: результат умножения short'ов - int, нельзя умножить 8 пар short'ов и получить 8 интов, т.к. в 128-битный регистр они не поместятся. Можно одной операцией либо перемножить и взять нижние 16 бит результатов, либо верхние 16 бит, либо 8 интов-результатов попарно сложить и получить 4 инта, уже помещающиеся в регистр.
Для использования команд SSE есть интринсики. Код с ними выглядит так:
int j = 0;
__m128i xsum = _mm_setzero_si128();
while(j<256) {
__m128i a16 = _mm_load_si128((__m128i*)&a[j]);
__m128i b16 = _mm_load_si128((__m128i*)&b[j]);
__m128i diff16 = _mm_sub_epi16(a16, b16);
__m128i madd = _mm_madd_epi16(diff16, diff16);
xsum = _mm_add_epi32(xsum, madd);
j += 8;
}
__m128i shft = _mm_srli_si128(xsum, 8);
xsum = _mm_add_epi32(xsum, shft);
shft = _mm_srli_si128(xsum, 4);
xsum = _mm_add_epi32(xsum, shft);
sum = _mm_cvtsi128_si32(xsum);
Отрабатывает те же 10 млн повторов за полсекунды. Но ручного написания интринсиков хочется избежать, у нас же есть умные компиляторы, да?
MSVC8 (2006 год). В опциях компилятора есть гордый пункт про SSE2. Включаем, компиляем. 3 с лишним секунды. Двойной цикл, внутренний unroll'ен по 4 итерации, внутри те же imul. SSE? Не, не слышал.
MSVC10 (2010 год). У студии новый WPFный интерфейс, но только не у окна настроек С++ проекта (настораживает?). Включаем нужные опции, компиляем. Тот же двойной цикл с разворачиванием внутреннего по 4 итерации, те же imul. Те же 3 секунды. SSE? Не, не слышал.
Intel Compiler 7.1 (2003 год). Полностью разворачивает внутренний цикл. 2 секунды. SSE? А что это?
Даем подсказку: вместо двойного цикла по 16 итераций делаем один на 256 итераций. Тут вдруг интеловский компилятор оживает и сам векторизует код ровно так же, как в примере с интринсиками. Укладывается в полсекунды.
Аналогичная подсказка MSVC всех версий ничего не дает, они продолжают разворачивать максимум по 4 итерации, никакого SSE.
Intel Compiler 11 (2009 год). Одинарный цикл векторизует так же хорошо, как 7-й. В двойном цикле на этот раз догадывается хотя бы векторизовать внутренний, но тупит с выравниваниями, вставляя ненужный и неиспользуемый код по краям. 1 секунда.
GCC 4.7.0 (2012 год). В двойном цикле полностью разворачивает внутренний, внутри imul, все как у intel 7. Те же 2 секунды. SSE? Не, не слышал. Если сделать цикл одинарным, сдюживает его векторизовать, но делает это плохо: вместо попарного сложения результатов умножения отдельно вычисляет верхние и нижние биты результатов, потом возится с перестановками и перепаковками байтов в регистрах. 1 секунда, вдвое хуже интела и варианта с ручной векторизацией.
Может, другие языки лучше? Что у нас после С? D? DMD 2.059, 2012 год, двойной цикл по 16 итераций, imul, т.е. код на уровне vc6. Ожидаемые 4 секунды.
Ну это маргинальщина, на самом деле после С/С++ идет C#. Молва гласит, что якобы JIT позволяет генерить код под конкретный процессор, используя доступные наборы инструкций. В теории это действительно так. Переписываем код на C# собираем в VS10 под .NET4. 6 секунд двойной цикл, 5 секунд одинарный. SSE? Хз, но чота не похоже. О, сборка была для x86. Пробуем для x64: 4 секунды на двойной цикл, 2 с половиной на одинарный. Получше, но до нормального невекторизованного С++ кода не дотягивает.
Ладно, надоели си-подобные языки, вот на днях вышел Haskell Platform 2012.2. Там GHC 7.4.1, берем Data.Vector.Unboxed Int16. Если верить бенчмаркам criterion, 10 млн повторов будут работать 16 секунд.
Мораль: со всей объективностью одного микробенчмарка :) можно заявить, что спустя 10 лет после появления SSE2 компиляторы - все еще говно, и авторы того же x264 не зря пишут на ассемблере.
no subject
Date: 2012-06-05 06:08 pm (UTC)no subject
Date: 2012-06-05 06:40 pm (UTC)no subject
Date: 2012-06-05 06:53 pm (UTC)http://stuff.thedeemon.com/lj/vectest.zip
С++ варианты для GCC, там вместо declspec attribute.
(no subject)
From:(no subject)
From:(no subject)
From:no subject
Date: 2012-06-05 07:02 pm (UTC)no subject
Date: 2012-06-05 08:59 pm (UTC)Но SSE там действительно нету, увы.
Моно щас попробую, мне интересно.
2
(no subject)
From:(no subject)
From:(no subject)
From:(no subject)
From:no subject
Date: 2012-06-05 08:07 pm (UTC)no subject
Date: 2012-06-06 02:56 am (UTC)http://ispc.github.com/
(no subject)
From:(no subject)
From:(no subject)
From:(no subject)
From:(no subject)
From:(no subject)
From:(no subject)
From:(no subject)
From:no subject
Date: 2012-06-05 08:38 pm (UTC)no subject
Date: 2012-06-05 09:32 pm (UTC)а компилятор-то это откуда знает?
no subject
Date: 2012-06-06 02:52 am (UTC)short v = a[j] - b[j];
no subject
Date: 2012-06-05 09:59 pm (UTC)no subject
Date: 2012-06-06 02:53 am (UTC)no subject
Date: 2012-06-05 11:56 pm (UTC)Я посмотрел core, и таких вычислений там не увидел. Вот релевантный кусок (http://hpaste.org/69574).
Видно следующее: а) лишняя проверка на завершение, т.к. zipWith не знает, что векторы одного размера; б) проверка того, что результат разности влезает в 16 бит (narrow16Int#) внутри цикла (16-битные операции в GHC реализованы через 32-битные).
Вот оптимизированная версия:
calc_low_level :: V.Vector Int16 -> V.Vector Int16 -> Int calc_low_level a b = go 0 0 where end = V.length a go i acc | i >= end = acc | otherwise = let a_i = fromIntegral (V.unsafeIndex a i) b_i = fromIntegral (V.unsafeIndex b i) d :: Int d = a_i - b_i in go (i+1) (acc + d*d)Поддержки SSE в GHC нет даже на уровне интринсиков (хотя планируется добавить (http://hackage.haskell.org/trac/ghc/wiki/SIMD/Implementation/Status)).
no subject
Date: 2012-06-06 04:50 am (UTC)Кстати она вроде бы есть в ATS и thedeemon вроде как гуру ATS. Реквестирую версию с интринсиками на ATS!
(no subject)
From:(no subject)
From:(no subject)
From:(no subject)
From:(no subject)
From:(no subject)
From:no subject
Date: 2012-06-06 05:40 am (UTC)С тех пор, почти ничего не изменилось. Печально.
Но вот напомнили про ATS.
Что там, как там? :-)
no subject
Date: 2012-06-06 05:46 am (UTC)(no subject)
From:no subject
Date: 2012-06-06 06:10 am (UTC)no subject
Date: 2012-06-06 07:00 am (UTC)g++ madd.cpp -O3 -msse2 -ftree-vectorize -o madd.exe
Для msvc там ключей дофига, но в опциях были maximize speed, inline any suitable, use SSE2, no RTTI, favor fast code over size.
Для интела еще добавляются -QaxW -Qip
Для DMD: -release -O
В шарпе вроде всего одна галка "optimize".
(no subject)
From:no subject
Date: 2012-06-06 06:28 am (UTC)no subject
Date: 2012-06-06 07:04 am (UTC)(no subject)
From:no subject
Date: 2012-06-06 03:53 pm (UTC)no subject
Date: 2012-06-06 04:08 pm (UTC)__m128i diff16 = _mm_sub_epi16(a16, b16);
в diff16 - 8 16-битных разностей. Дальше
__m128i madd = _mm_madd_epi16(diff16, diff16);
вот тут разности умножаются поэлементно на себя, а затем первый результат складывается со вторым, третий с четвертым, пятый с шестым и седьмой с восьмым, в переменной madd теперь 4 32-битных инта. Они поэлементно добавляются в xsum - вектор из 4 интов.
После цикла 4 инта в xsum нужно сложить и получить окончательный ответ. Для этого сдвигаем их на 8 байт вправо и прибавляем, получаем 2 инта-суммы (первый + третий, второй + четвертый) в нижних 8 байтах xsum. Потом аналогично сдвигаем на 4 байта вправо и прибавляем, получаем искомую сумму в одном нижнем инте xsum. Ее и записываем в интовую переменную.
(no subject)
From:no subject
Date: 2012-06-06 07:41 pm (UTC)Что касается с++ то альтернативы интринсикам никогда не было. Если за последние 3 года не случилось чуда то intel c++ умудряется просрать все что он выиграл на одиночных циклах msvc практически на любой программе разумного размера. И этому есть хорошее объяснение так как мс оптимизирует кучу уровней абстракции в очень сложном коде - это их конек.
вывод тоже не блещет - в то время как отдельные личности пишут на ассемблере чтобы сэкономить размер кода, разработчики процессоров ... оптимизируют под код который выдают компиляторы. И появляются большие штрафы за заходы в середину функций и тп. Соответственно в выборе между интринсики + собирать циклы компилятором, который знает о чем думали разработчики процессоров, практически без вариантов уже лет 10.
счастье всех в том что иерархия памяти сглаживает это все почти в 0 так как пара штрафов (кэш-промахов) и про то сколько там вычислялась та ерунда можно забыть.
имхо^2 мечтать о новых языках на которых можно будет специфицировать до железа можно но практически бессмысленно. Коробочные продукты на них не появятся при нашей жизни. увы. А то чем занимаются авторы golang/llvm/... и тп это печаль./имхо^2
no subject
Date: 2012-06-07 02:29 am (UTC)(no subject)
From:(no subject)
From:Re: компиляторы и SSE, или веселые микробенчмарки
Date: 2012-06-07 05:19 am (UTC)http://alextutubalin.livejournal.com/tag/sse
Re: компиляторы и SSE, или веселые микробенчмарки
Date: 2012-06-07 06:05 am (UTC)Re: компиляторы и SSE, или веселые микробенчмарки
From:no subject
Date: 2012-06-14 08:17 pm (UTC)no subject
Date: 2012-06-15 10:58 am (UTC)(no subject)
From:(no subject)
From:(no subject)
From:no subject
Date: 2012-06-16 09:49 am (UTC)no subject
Date: 2012-06-16 10:21 am (UTC)no subject
Date: 2012-07-01 11:08 pm (UTC)Но правы Вы в том, что компиляторам следовало бы уметь оптимизировать получше. Жаль, что они так пока не умеют
no subject
Date: 2012-07-02 05:44 am (UTC)no subject
Date: 2012-07-05 09:16 pm (UTC)определить как
int a[256], b[256];
или даже
long long a[256], b[256];
??
думаю что разницы совсем не будет
я анализировал арифметику над 64-битными целыми, и Visual C, и C# и GCC используют инструкции SSE2
вот такой тести прогонял - вычисление числа Pi
unsigned long long denom=3;
double ourPi=4.0f;
int addFlop=1;
unsigned long long i=1;
clock_t t0 = clock();
for (i=1;i<=1000000000LL;i++)
{
if (addFlop)
{
ourPi-=(4.0/denom);
addFlop=0;
denom+=2;
}
else
{
ourPi+=(4.0/denom);
addFlop=1;
denom+=2;
}
}
no subject
Date: 2014-05-26 06:24 pm (UTC)Ну и ещё хотелось бы дополнить, что подобные микротесты не отражают потенциал компилятора. Реальная жизнь даёт гораздо более сложные случаи и смотреть нужно по ним (хотя в конкретном случае сомневаюсь что обычные компиляторы что-нибудь смогут).
Векторизация (разворачивание циклов) и применение SSE
Date: 2016-09-10 06:39 pm (UTC)