thedeemon: (Default)
[personal profile] thedeemon
Есть в программистском фольклоре два поверья:
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 не зря пишут на ассемблере.

Date: 2012-06-05 06:08 pm (UTC)
From: [identity profile] mr-st.livejournal.com
Пару лет назад баловался с подобными тестами. Вывод был такой же: кроме интела никто толком векторизовать не умеет.

Date: 2012-06-05 06:40 pm (UTC)
From: [identity profile] stepancheg.livejournal.com
Выложи куда-нибудь код тестов.

Date: 2012-06-05 06:53 pm (UTC)
From: [identity profile] thedeemon.livejournal.com
Как-то так:
http://stuff.thedeemon.com/lj/vectest.zip
С++ варианты для GCC, там вместо declspec attribute.

(no subject)

From: [identity profile] soonts.livejournal.com - Date: 2012-06-05 11:46 pm (UTC) - Expand

(no subject)

From: [identity profile] thedeemon.livejournal.com - Date: 2012-06-06 03:03 am (UTC) - Expand

(no subject)

From: [identity profile] thedeemon.livejournal.com - Date: 2012-06-06 03:05 am (UTC) - Expand

Date: 2012-06-05 07:02 pm (UTC)
From: [identity profile] xeno-by.livejournal.com
В дотнетовском джите вроде бы SSE и нету. Попробуй на моно.

Date: 2012-06-05 08:59 pm (UTC)
wizzard: (Default)
From: [personal profile] wizzard
Дотнетовский джит, ЕМНИП, вообще на memory access optimization заточен, особенно х64.

Но SSE там действительно нету, увы.

Моно щас попробую, мне интересно.

2 [livejournal.com profile] thedeemon: а бенчмарк на memory access patterns или что-то такое сделать можешь?

(no subject)

From: [identity profile] thedeemon.livejournal.com - Date: 2012-06-06 02:58 am (UTC) - Expand

(no subject)

From: [identity profile] stdray.livejournal.com - Date: 2012-07-12 07:34 pm (UTC) - Expand

(no subject)

From: [personal profile] wizzard - Date: 2012-07-12 10:35 pm (UTC) - Expand

(no subject)

From: [identity profile] stdray.livejournal.com - Date: 2012-07-13 12:27 pm (UTC) - Expand

Date: 2012-06-05 08:07 pm (UTC)
From: [identity profile] potan.livejournal.com
А такая штука с распараллеливанием на sse не справится?

Date: 2012-06-06 02:56 am (UTC)
From: [identity profile] thedeemon.livejournal.com
Не знаю, надо пробовать. Кроме нее есть еще такая:
http://ispc.github.com/

(no subject)

From: [identity profile] helvegr.livejournal.com - Date: 2012-06-06 05:42 am (UTC) - Expand

(no subject)

From: [identity profile] thedeemon.livejournal.com - Date: 2012-06-06 07:10 am (UTC) - Expand

(no subject)

From: [identity profile] helvegr.livejournal.com - Date: 2012-06-06 07:13 am (UTC) - Expand

(no subject)

From: [identity profile] udpn.livejournal.com - Date: 2012-06-06 07:20 am (UTC) - Expand

(no subject)

From: [identity profile] thedeemon.livejournal.com - Date: 2012-06-06 08:23 am (UTC) - Expand

(no subject)

From: [identity profile] helvegr.livejournal.com - Date: 2012-06-07 12:43 pm (UTC) - Expand

(no subject)

From: [identity profile] izard.livejournal.com - Date: 2012-06-06 06:43 pm (UTC) - Expand

(no subject)

From: [identity profile] thedeemon.livejournal.com - Date: 2012-06-07 06:02 am (UTC) - Expand

Date: 2012-06-05 08:38 pm (UTC)
From: [identity profile] mr-aleph.livejournal.com
это же все голимый паттерн-матчинг по существу, достаточно посмотреть на реализацию vect_recog_dot_prod_pattern в gcc и прослезится.

Date: 2012-06-05 09:32 pm (UTC)
From: [identity profile] 109.livejournal.com
> Данные в задаче таковы, что разность помещается в short

а компилятор-то это откуда знает?

Date: 2012-06-06 02:52 am (UTC)
From: [identity profile] thedeemon.livejournal.com
Отсюда:
short v = a[j] - b[j];

Date: 2012-06-05 09:59 pm (UTC)
From: [identity profile] vadim kantorov (from livejournal.com)
Практически интересный вопрос: MATLAB за сколько такую операцию выполнит?

Date: 2012-06-06 02:53 am (UTC)
From: [identity profile] thedeemon.livejournal.com
У меня его нет. Попробуйте, мне тоже интересно.

Date: 2012-06-05 11:56 pm (UTC)
From: [identity profile] helvegr.livejournal.com
> Ибо внутри внезапно часть вычислений делаются с плавающей точкой (если я правильно их нашел).

Я посмотрел 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)).

Date: 2012-06-06 04:50 am (UTC)
From: [identity profile] nponeccop.livejournal.com
> Поддержки SSE в GHC нет

Кстати она вроде бы есть в ATS и thedeemon вроде как гуру ATS. Реквестирую версию с интринсиками на ATS!

(no subject)

From: [identity profile] thedeemon.livejournal.com - Date: 2012-06-06 05:44 am (UTC) - Expand

(no subject)

From: [identity profile] helvegr.livejournal.com - Date: 2012-06-06 05:55 am (UTC) - Expand

(no subject)

From: [identity profile] thedeemon.livejournal.com - Date: 2012-06-06 06:51 am (UTC) - Expand

(no subject)

From: [identity profile] helvegr.livejournal.com - Date: 2012-06-06 06:58 am (UTC) - Expand

(no subject)

From: [identity profile] thedeemon.livejournal.com - Date: 2012-06-06 07:06 am (UTC) - Expand

(no subject)

From: [identity profile] helvegr.livejournal.com - Date: 2012-06-06 07:20 am (UTC) - Expand

Date: 2012-06-06 05:40 am (UTC)
From: [identity profile] nivanych.livejournal.com
10 лет прошло, как я x86-оптимизацией закончил заниматьася.
С тех пор, почти ничего не изменилось. Печально.
Но вот напомнили про ATS.
Что там, как там? :-)

Date: 2012-06-06 05:46 am (UTC)
From: [identity profile] thedeemon.livejournal.com
Он же Си генерит, так что не лучше, чем в GCC.

(no subject)

From: [identity profile] thedeemon.livejournal.com - Date: 2012-06-06 05:49 am (UTC) - Expand

Date: 2012-06-06 06:10 am (UTC)
From: [identity profile] diam-2003.livejournal.com
В таких случаях ещё принято писать, с какими ключиками запускался компилятор.

Date: 2012-06-06 07:00 am (UTC)
From: [identity profile] thedeemon.livejournal.com
Для GCC
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: [identity profile] permea-kra.livejournal.com - Date: 2012-06-14 07:19 am (UTC) - Expand

Date: 2012-06-06 06:28 am (UTC)
From: [identity profile] kiryl.livejournal.com
На AVX ещё не смотрел? С 256-и битными регистрами будет веселее. Может и компилятору векторизовать будет проще (хотя я б не сильно надеялся).

Date: 2012-06-06 07:04 am (UTC)
From: [identity profile] thedeemon.livejournal.com
Не, у меня пока под рукой даже нет процессора, его поддерживающего. И у большинства пользователей тоже.

(no subject)

From: [identity profile] izard.livejournal.com - Date: 2012-06-06 06:44 pm (UTC) - Expand

Date: 2012-06-06 03:53 pm (UTC)
From: [identity profile] qehgt.livejournal.com
Непонятно, что происходит в последних пяти строчках SSE кода. Можно как-то подробнее расписать? (непонятно, куда квадраты делись)

Date: 2012-06-06 04:08 pm (UTC)
From: [identity profile] thedeemon.livejournal.com
В цикле в a16 и b16 загружается по 8 16-битных значений. Вычитаем:
__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: [identity profile] qehgt.livejournal.com - Date: 2012-06-06 05:58 pm (UTC) - Expand

Date: 2012-06-06 07:41 pm (UTC)
From: [identity profile] sleepy-drago.livejournal.com
ко намекает что вы хотите странного. Наверное единственный компилятор, который "должен по идее уметь колбасить двойные циклы" это intel fortran :D Но я проверять это не буду никогда.

Что касается с++ то альтернативы интринсикам никогда не было. Если за последние 3 года не случилось чуда то intel c++ умудряется просрать все что он выиграл на одиночных циклах msvc практически на любой программе разумного размера. И этому есть хорошее объяснение так как мс оптимизирует кучу уровней абстракции в очень сложном коде - это их конек.

вывод тоже не блещет - в то время как отдельные личности пишут на ассемблере чтобы сэкономить размер кода, разработчики процессоров ... оптимизируют под код который выдают компиляторы. И появляются большие штрафы за заходы в середину функций и тп. Соответственно в выборе между интринсики + собирать циклы компилятором, который знает о чем думали разработчики процессоров, практически без вариантов уже лет 10.

счастье всех в том что иерархия памяти сглаживает это все почти в 0 так как пара штрафов (кэш-промахов) и про то сколько там вычислялась та ерунда можно забыть.

имхо^2 мечтать о новых языках на которых можно будет специфицировать до железа можно но практически бессмысленно. Коробочные продукты на них не появятся при нашей жизни. увы. А то чем занимаются авторы golang/llvm/... и тп это печаль./имхо^2

Date: 2012-06-07 02:29 am (UTC)
From: [identity profile] thedeemon.livejournal.com
В исходной задаче, по мотивам которой был сделан этот бенчмарк, брался один блок 16х16 и к нему примерялся десяток способов внутриблочного предсказания, с оценкой в виде как раз суммы квадратов ошибки. Там все очень хорошо в кэше сидело, и векторизация дала многократное ускорение. И то, что для этого приходится писать интринсики, удручает. И вообше, хочется поручить это машине, т.к. человек не всегда видит оптимальное решение.

(no subject)

From: [identity profile] sleepy-drago.livejournal.com - Date: 2012-06-07 09:16 am (UTC) - Expand

(no subject)

From: [identity profile] tramsm.livejournal.com - Date: 2012-07-01 11:11 pm (UTC) - Expand
dememax: (сонливость)
From: [personal profile] dememax
Кстати, может заинтересует, не увидел у вас в друзьях Алексея Тутубалина:
http://alextutubalin.livejournal.com/tag/sse

Date: 2012-06-14 08:17 pm (UTC)
From: [identity profile] permea-kra.livejournal.com
Не знаю, за счет чего, но если в строку опций для gcc -O3 -msse2 madd.cpp добавить -floop-strip-mine , появляется ~10% выигрышь времени. За счет чего?

Date: 2012-06-15 10:58 am (UTC)
From: [identity profile] thedeemon.livejournal.com
У меня наоборот на 5-10% медленнее получилось с этим ключом.

(no subject)

From: [identity profile] thedeemon.livejournal.com - Date: 2012-06-15 11:01 am (UTC) - Expand

(no subject)

From: [identity profile] permea-kra.livejournal.com - Date: 2012-06-15 05:13 pm (UTC) - Expand

(no subject)

From: [identity profile] thedeemon.livejournal.com - Date: 2012-06-15 05:54 pm (UTC) - Expand

Date: 2012-06-16 09:49 am (UTC)
From: [identity profile] wolf ramovsky (from livejournal.com)
Пост ни о чём, у автора ГСМ.

Date: 2012-06-16 10:21 am (UTC)
From: [identity profile] tivochkin.livejournal.com
Мне нравится ваш уровень аргументации.

Date: 2012-07-01 11:08 pm (UTC)
From: [identity profile] tramsm.livejournal.com
Знаете почему этот тест не отражает реальность? Потому, что се делать миллионов повторов делаются над одним участком памяти в 512 байт. Соответственно, весь участок памяти лежит в кеше. В реальности же операции загрузки из памяти в кеш больше чем время работы с загруженными данными. Пожтму интересно было бы посмотреть как работал бы этот тест, если бы 10 миллионов повторов было бы над разными данными.

Но правы Вы в том, что компиляторам следовало бы уметь оптимизировать получше. Жаль, что они так пока не умеют

Date: 2012-07-05 09:16 pm (UTC)
From: [identity profile] sobolevsergei.livejournal.com
а что если массивы short a[256], b[256];
определить как
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;
}
}
Edited Date: 2012-07-05 09:24 pm (UTC)

Date: 2014-05-26 06:24 pm (UTC)
From: [identity profile] alex markin (from livejournal.com)
Я из интереса попробовал (http://alexanius-blog.blogspot.ru/2014/05/blog-post_23.html) эти примеры на компиляторе процессора Эльбрус :) Всё отлично векторизуется :)

Ну и ещё хотелось бы дополнить, что подобные микротесты не отражают потенциал компилятора. Реальная жизнь даёт гораздо более сложные случаи и смотреть нужно по ним (хотя в конкретном случае сомневаюсь что обычные компиляторы что-нибудь смогут).
From: [identity profile] livejournal.livejournal.com
Пользователь [livejournal.com profile] ewoke сослался на вашу запись в своей записи «Векторизация (разворачивание циклов) и применение SSE (http://ewoke.livejournal.com/420839.html)» в контексте: [...] был анонсирован в pentium 3 сами знаете как давно. http://thedeemon.livejournal.com/49226.html [...]

Profile

thedeemon: (Default)
Dmitry Popov

December 2025

S M T W T F S
 12 3456
789101112 13
14151617181920
21222324252627
28293031   

Most Popular Tags

Style Credit

Expand Cut Tags

No cut tags
Page generated Jan. 30th, 2026 03:40 pm
Powered by Dreamwidth Studios