Как достичь теоретической пиковой производительности 4 операций с плавающей запятой (двойной точности) за такт на современном процессоре Intel x86-64?
Насколько я понимаю, для большинства современных процессоров Intel требуется три цикла для SSE add
и пять циклов для a mul
(см., Например , «Таблицы инструкций» Агнера Фога ). Благодаря конвейерной обработке можно получить пропускную способность по одному add
за цикл, если алгоритм имеет как минимум три независимых суммирования. Поскольку это верно как для упакованных, addpd
так и для скалярных addsd
версий и регистров SSE может содержать два double
, пропускная способность может достигать двух флопов за цикл.
Кроме того, кажется (хотя я не видел никакой надлежащей документации по этому вопросу) add
, и mul
могут выполняться параллельно, давая теоретическую максимальную пропускную способность четыре флопс за цикл.
Однако я не смог воспроизвести эту производительность с помощью простой программы на C / C ++. Моя лучшая попытка привела к примерно 2,7 флопс / цикл. Если кто-то может предложить простую C / C ++ или ассемблерную программу, которая демонстрирует пиковую производительность, это было бы очень признательно.
Моя попытка:
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <sys/time.h>
double stoptime(void) {
struct timeval t;
gettimeofday(&t,NULL);
return (double) t.tv_sec + t.tv_usec/1000000.0;
}
double addmul(double add, double mul, int ops){
// Need to initialise differently otherwise compiler might optimise away
double sum1=0.1, sum2=-0.1, sum3=0.2, sum4=-0.2, sum5=0.0;
double mul1=1.0, mul2= 1.1, mul3=1.2, mul4= 1.3, mul5=1.4;
int loops=ops/10; // We have 10 floating point operations inside the loop
double expected = 5.0*add*loops + (sum1+sum2+sum3+sum4+sum5)
+ pow(mul,loops)*(mul1+mul2+mul3+mul4+mul5);
for (int i=0; i<loops; i++) {
mul1*=mul; mul2*=mul; mul3*=mul; mul4*=mul; mul5*=mul;
sum1+=add; sum2+=add; sum3+=add; sum4+=add; sum5+=add;
}
return sum1+sum2+sum3+sum4+sum5+mul1+mul2+mul3+mul4+mul5 - expected;
}
int main(int argc, char** argv) {
if (argc != 2) {
printf("usage: %s <num>\n", argv[0]);
printf("number of operations: <num> millions\n");
exit(EXIT_FAILURE);
}
int n = atoi(argv[1]) * 1000000;
if (n<=0)
n=1000;
double x = M_PI;
double y = 1.0 + 1e-8;
double t = stoptime();
x = addmul(x, y, n);
t = stoptime() - t;
printf("addmul:\t %.3f s, %.3f Gflops, res=%f\n", t, (double)n/t/1e9, x);
return EXIT_SUCCESS;
}
Составлено с
g++ -O2 -march=native addmul.cpp ; ./a.out 1000
выдает следующий вывод на Intel Core i5-750, 2,66 ГГц.
addmul: 0.270 s, 3.707 Gflops, res=1.326463
То есть примерно 1,4 флопа за цикл. Глядя на ассемблерный код с
g++ -S -O2 -march=native -masm=intel addmul.cpp
основным циклом мне кажется оптимальным:
.L4:
inc eax
mulsd xmm8, xmm3
mulsd xmm7, xmm3
mulsd xmm6, xmm3
mulsd xmm5, xmm3
mulsd xmm1, xmm3
addsd xmm13, xmm2
addsd xmm12, xmm2
addsd xmm11, xmm2
addsd xmm10, xmm2
addsd xmm9, xmm2
cmp eax, ebx
jne .L4
Изменение скалярных версий с упакованными версиями ( addpd
и mulpd
) удвоило бы количество флопов без изменения времени выполнения, и поэтому мне хватило бы лишь 2,8 флопов за цикл. Есть ли простой пример, который достигает четырех флопов за цикл?
Хорошая маленькая программа от Mysticial; Вот мои результаты (хотя бы на несколько секунд):
gcc -O2 -march=nocona
: 5,6 Гфлоп из 10,66 Гфлоп (2,1 Флоп / цикл)cl /O2
openmp удалено: 10,1 Гфлоп из 10,66 Гфлоп (3,8 Флоп / цикл)
Все это кажется немного сложным, но мои выводы пока:
gcc -O2
изменяет порядок независимых операций с плавающей запятой с целью чередованияaddpd
иmulpd
по возможности. То же самое относится и кgcc-4.6.2 -O2 -march=core2
.gcc -O2 -march=nocona
похоже, сохраняет порядок операций с плавающей запятой, как определено в источнике C ++.cl /O2
64-разрядный компилятор из SDK для Windows 7 выполняет автоматическое развертывание циклов и, по-видимому, пытается упорядочить операции так, чтобы группы из трехaddpd
чередовались с тремяmulpd
(ну, по крайней мере, в моей системе и для моей простой программы) ,Мой Core i5 750 ( архитектура Nehalem ) не любит чередование надстроек и мул и, по-видимому, не может выполнять обе операции параллельно. Тем не менее, если сгруппированы в 3-х, это внезапно работает как магия.
Другие архитектуры (возможно, Sandy Bridge и другие), по-видимому, могут выполнять add / mul параллельно без проблем, если они чередуются в коде сборки.
Хотя это трудно признать, но в моей системе
cl /O2
гораздо лучше справляется с низкоуровневыми операциями оптимизации для моей системы и достигает почти максимальной производительности для небольшого примера C ++, описанного выше. Я измерял между 1,85-2,01 флопс / цикл (использовал clock () в Windows, что не так точно. Я думаю, нужно использовать лучший таймер - спасибо Mackie Messer).Лучшее, с чем мне удалось
gcc
справиться, - это вручную развернуть цикл и расставить сложения и умножения в группы по три. Сg++ -O2 -march=nocona addmul_unroll.cpp
я получаю в лучшем случае,0.207s, 4.825 Gflops
что соответствует 1,8 плюхается / цикл , который я очень доволен компанией.
В коде C ++ я заменил for
цикл
for (int i=0; i<loops/3; i++) {
mul1*=mul; mul2*=mul; mul3*=mul;
sum1+=add; sum2+=add; sum3+=add;
mul4*=mul; mul5*=mul; mul1*=mul;
sum4+=add; sum5+=add; sum1+=add;
mul2*=mul; mul3*=mul; mul4*=mul;
sum2+=add; sum3+=add; sum4+=add;
mul5*=mul; mul1*=mul; mul2*=mul;
sum5+=add; sum1+=add; sum2+=add;
mul3*=mul; mul4*=mul; mul5*=mul;
sum3+=add; sum4+=add; sum5+=add;
}
И сборка теперь выглядит так
.L4:
mulsd xmm8, xmm3
mulsd xmm7, xmm3
mulsd xmm6, xmm3
addsd xmm13, xmm2
addsd xmm12, xmm2
addsd xmm11, xmm2
mulsd xmm5, xmm3
mulsd xmm1, xmm3
mulsd xmm8, xmm3
addsd xmm10, xmm2
addsd xmm9, xmm2
addsd xmm13, xmm2
...
источник
-funroll-loops
). Пробовал с gcc версии 4.4.1 и 4.6.2, но вывод asm выглядит нормально?-O3
gcc, который позволяет-ftree-vectorize
? Может быть, в сочетании с тем,-funroll-loops
хотя я этого не делаю, если это действительно необходимо. В конце концов, сравнение кажется несправедливым, если один из компиляторов выполняет векторизацию / развёртывание, а другой - не потому, что не может, а потому, что об этом сказано не слишком.-funroll-loops
, наверное, что-то попробовать. Но я думаю,-ftree-vectorize
что дело не в этом. ОП пытается просто выдержать 1 муль + 1 инструкцию добавления / цикл. Инструкции могут быть скалярными или векторными - это не имеет значения, поскольку задержка и пропускная способность одинаковы. Так что если вы можете выдержать 2 / цикл со скалярным SSE, то вы можете заменить их векторным SSE, и вы получите 4 флопа / цикл. В своем ответе я поступил именно так из SSE -> AVX. Я заменил все SSE на AVX - те же задержки, те же пропускные способности, 2x флопс.Ответы:
Я выполнил эту задачу раньше. Но это было главным образом для измерения энергопотребления и температуры процессора. Следующий код (который довольно длинный) достигает почти оптимального на моем Core i7 2600K.
Ключевым моментом, который следует здесь отметить, является огромное количество ручного развертывания циклов, а также чередования умножений и добавлений ...
Полный проект можно найти на моем GitHub: https://github.com/Mysticial/Flops
Предупреждение:
Если вы решили скомпилировать и запустить это, обратите внимание на температуру вашего процессора !!!
Убедитесь, что вы не перегреваете его. И убедитесь, что удушение процессора не влияет на ваши результаты!
Кроме того, я не несу ответственности за любой ущерб, который может возникнуть в результате выполнения этого кода.
Ноты:
На удивление, ICC 11 (Intel Compiler 11) не может скомпилировать его.
Вывод (1 поток, 10000000 итераций) - скомпилировано с Visual Studio 2010 SP1 - выпуск x64:
Машина Core i7 2600K @ 4,4 ГГц. Теоретический пик SSE составляет 4 флопа * 4,4 ГГц = 17,6 Гфлопс . Этот код достигает 17,3 GFlops - неплохо.
Вывод (8 потоков, 10000000 итераций) - скомпилировано с Visual Studio 2010 SP1 - выпуск x64:
Теоретический пик SSE составляет 4 флопа * 4 ядра * 4,4 ГГц = 70,4 Гфлопса. Фактически это 65,5 GFlops .
Давайте сделаем еще один шаг вперед. AVX ...
Вывод (1 поток, 10000000 итераций) - скомпилировано с Visual Studio 2010 SP1 - выпуск x64:
Теоретический пик AVX составляет 8 флопов * 4,4 ГГц = 35,2 Гфлопс . Фактически это 33,4 GFlops .
Вывод (8 потоков, 10000000 итераций) - скомпилировано с Visual Studio 2010 SP1 - выпуск x64:
Теоретический пик AVX составляет 8 флопов * 4 ядра * 4,4 ГГц = 140,8 Гфлопс. Фактически это 138,2 GFlops .
Теперь несколько пояснений:
Критическая часть производительности - это, очевидно, 48 инструкций во внутреннем цикле. Вы заметите, что он разбит на 4 блока по 12 инструкций в каждом. Каждый из этих 12 блоков инструкций полностью независим друг от друга - для выполнения в среднем требуется 6 циклов.
Таким образом, существует 12 инструкций и 6 циклов между выпусками. Задержка умножения составляет 5 циклов, так что этого достаточно, чтобы избежать задержек задержки.
Шаг нормализации необходим, чтобы предотвратить переполнение / переполнение данных. Это необходимо, поскольку беспроигрышный код будет медленно увеличивать / уменьшать величину данных.
Так что на самом деле можно добиться большего, чем это, если вы просто используете все нули и избавляетесь от шага нормализации. Однако, поскольку я написал эталон для измерения энергопотребления и температуры, я должен был убедиться, что на флопах были «реальные» данные, а не нули - поскольку исполнительные блоки вполне могут иметь особую обработку случая для нулей, которые используют меньше энергии и производить меньше тепла.
Больше результатов:
Темы: 1
Теоретический пик SSE: 4 флопа * 3,5 ГГц = 14,0 гфлопс . Фактический 13,3 GFlops .
Темы: 8
Теоретический пик SSE: 4 флопа * 4 ядра * 3,5 ГГц = 56,0 Гфлопса . Фактический 51,3 GFlops .
В многопоточном режиме температура моего процессора достигла 76C! Если вы запускаете их, убедитесь, что на результаты не влияет регулирование процессора.
Темы: 1
Теоретический пик SSE: 4 флопа * 3,2 ГГц = 12,8 гфлопс . Фактически это 12,3 GFlops .
Темы: 8
Теоретический пик SSE: 4 флопа * 8 ядер * 3,2 ГГц = 102,4 Гфлопса . Фактически это 97,9 GFlops .
источник
1.814s, 5.292 Gflops, sum=0.448883
из пиковых 10,68 Гфлопс или просто не хватает 2,0 флопс за цикл. Кажетсяadd
/mul
не выполняются параллельно. Когда я изменяю ваш код и всегда добавляю / умножаю с одним и тем же регистром, скажемrC
, он внезапно достигает почти пика:0.953s, 10.068 Gflops, sum=0
или 3,8 флопс / цикл. Очень странно.cl /O2
(64-битный от Windows SDK), и даже мой пример работает там близко к пику для скалярных операций (1,9 флопс / цикл). Цикл развертывания и переупорядочения компилятора, но это, возможно, не является причиной, по которой нужно больше разбираться в этом. Дросселирование не проблема, я хорошо отношусь к своему процессору и держу итерации на 100k. :)В архитектуре Intel есть один момент, о котором люди часто забывают: порты диспетчеризации разделяются между Int и FP / SIMD. Это означает, что вы получите только определенное количество пакетов FP / SIMD, прежде чем логика цикла создаст пузырьки в потоке с плавающей запятой. Mystical получил больше провалов из своего кода, потому что он использовал более длинные шаги в своем развернутом цикле.
Если вы посмотрите на архитектуру Nehalem / Sandy Bridge здесь http://www.realworldtech.com/page.cfm?ArticleID=RWT091810191937&p=6, то совершенно ясно, что происходит.
Напротив, должно быть проще достичь пиковой производительности на AMD (Bulldozer), поскольку каналы INT и FP / SIMD имеют отдельные порты выдачи с собственным планировщиком.
Это только теоретически, поскольку у меня нет ни одного из этих процессоров для тестирования.
источник
inc
,cmp
иjl
. Все они могут идти в порт № 5 и не мешать ни векторизации,fadd
ниfmul
. Я скорее подозреваю, что декодер (иногда) встает на пути. Требуется выдержать от двух до трех инструкций за цикл. Я не помню точных ограничений, но длина команды, префиксы и выравнивание входят в игру.cmp
и,jl
конечно, перейдите к порту 5,inc
не так уверен, как это всегда происходит в группе с 2 другими. Но вы правы, трудно сказать, где находится узкое место, и декодеры также могут быть частью этого.Филиалы определенно могут удержать вас от поддержания максимальной теоретической производительности. Видите ли вы разницу, если вы выполняете ручное развертывание? Например, если вы добавили в 5 или 10 раз больше операций на цикл итерации:
источник
-funroll-loops
опции, которая даже не включена в-O3
. Смg++ -c -Q -O2 --help=optimizers | grep unroll
.Использование Intel ICC версии 11.1 на 2,4 ГГц Intel Core 2 Duo я получаю
Это очень близко к идеальным 9,6 Гфлопс.
РЕДАКТИРОВАТЬ:
Ой, глядя на код сборки, кажется, что icc не только векторизовал умножение, но и вытащил дополнения из цикла. При навязывании более строгой семантики fp код больше не векторизован:
EDIT2:
Как просили:
Внутренний цикл кода Clang выглядит следующим образом:
EDIT3:
Наконец, два предложения: во-первых, если вам нравится этот тип тестирования, подумайте об использовании
rdtsc
инструкции вместоgettimeofday(2)
. Это намного точнее и обеспечивает время в циклах, что обычно в любом случае вас интересует. Для gcc и друзей вы можете определить это так:Во-вторых, вам следует несколько раз запускать тестовую программу и использовать только наилучшую производительность . В современных операционных системах многие вещи происходят параллельно, процессор может находиться в режиме энергосбережения на низких частотах и т. Д. Повторное выполнение программы дает результат, который ближе к идеальному случаю.
источник
addsd
's' иmulsd
's' или они в группах, как в моем выводе сборки? Я также получаю примерно 1 флоп / цикл, когда компилятор смешивает их (без которых я получаю-march=native
). Как изменяется производительность, если вы добавляете строкуadd=mul;
в начале функцииaddmul(...)
?addsd
иsubsd
инструкции действительно смешаны в точной версии. Я тоже попробовал clang 3.0, он не смешивает инструкции и очень близок к 2 флопсам / такт на Core 2 Duo. Когда я запускаю тот же код на моем ноутбуке Core i5, смешивание кода не имеет значения. Я получаю около 3 флопов / цикл в любом случае.icc
ранее, можете ли вы дважды проверить сборку?