1. Эффективное использование
x86-совместимых CPU
Многопоточность, векторные расширения
Алексей Тутубалин
lexa@lexa.ru
CTO @LibRaw LLC
2. Производительность CPU 2005-2012:
Практически постоянна? Выросла в 32-64 раза?
Производительность CPU = • Многоядерность
частота * (число инструкций – С 2005 по 2012: рост в 4-8
на такт) раз
• Тактовая частота • SSE, AVX
2005 – 2.5-3.6 Ghz – 2005: 2 операции на такт
2012 – 1.8-3.5 Ghz – 2012: 16 операций на такт
• Инструкции/такт
после Intel Core2 => роста
очень медленный
3. Локальная параллельность
• Только в хотспотах
– Замена обычных циклов - параллельными
– Замена обычных команд – векторными (SIMD:
SSE, AVX)
• Сопутствующие изменения
– Редизайн/рефакторинг структур данных
– Изменения в алгоритмах
5. Схема работы
• Fork/join на месте
длинных циклов
• Накладные расходы
– n*1-10µsec на каждый поток
• Методы реализации
– Полуавтоматически: OpenMP
– Вручную: threading-библиотеки
6. OpenMP
• Простой синтаксис: #pragma в
последовательном коде
#pragma omp parallel for
for(i=0;i<N;i++)
a[i] = b[i]+c[i];
• Возможности: reduction, atomics, critical
sections, режим доступа к переменным:
sum=0.0;
#pragma omp parallel for reduction(+:sum)
for(i=0;i<N;i++)
sum+=vector[i];
7. OpenMP
• Чужеродность для С++
– Нет поддержки exceptions
• Не все компиляторы поддерживают
• Бывают ошибки в рантайме (или
компиляторе)
• Альтернатива: Cilk Plus (Intel C++, gcc 4.8
branch)
9. Итого
1. Это работает !
2. Эффективность:
– Обычно 75-85% (3-3.5x speedup на 4-core)
– >90% для «гигабайт» данных
3. Дополнительный код:
– 1-20 строк на каждую точку оптимизации
11. SSE/AVX: что это такое?
• SSE: одновременные операции
над 128-битными векторами
addps xmm0,xmm1
означает:
for(i=0;i<4;i++)
xmm0[i]+=xmm1[i];
• AVX: вектор 128 или 256 бит, 3-адресность:
vaddps ymm0,ymm1,ymm2
означает:
for(i=0;i<8;i++)
ymm0[i]=ymm1[i]+ymm2[i];
12. X86: много сортов SIMD
• Под что код делать будем?
– 6 версий SSE
– 2 версии AVX
• Чем новее версия
– Тем больше вкусных команд
– И тем уже поддержка
13. SSE/AVX: что умеет
Что есть: Что есть:
• Команды • 16 (8) регистров
– Load/store • Типы данных
– Арифметика – Целые: 8, 16, 32, 64 бит
– Сравнения, min, max – Плавающие: 32, 64
– Конверсия типов
Чего нет:
– Битовые операции
• Flow control
– Перестановка
элементов вектора • Scatter/gather
– Dot Product, CRC32,
AES, STRCMP
14. SSE/AVX: упаковка данных
Стандартный ООП-вариант Structure of Arrays (SoA)
Array Of Structures (AoS)
class foo_vector {
class foo {
vector<float> x,y,z;
float x,y,z;
};
void calc_z(){z=A*x+B*y;}
foo_vector::calc_z()
};
for(..) z[i]=A*x[i]+B*y[i];
vector<foo> fv(..);
for(…..) fv[i].calc_z();
calc_z() – хорошо векторизуется:
• for(..)calc_z() – плохо – 2 чтения
векторизуется: – 3 оп. арифметики
– 4-8 чтений – 1 запись
– 3 оп. арифметики
– 4 записи
15. SSE: выравнивание
• До Intel Core-i7:
– aligned (16 байт) load/store – сильно быстрее
(~4 раза для Core2Duo)
• Начиная с Core-i7:
– Почти нет разницы (<10%)
16. С/C++: выравнивание
• Переменные и поля в структурах:
Gcc: int bar[64] __attribute_((aligned(16)));
MSVC:__declspec(align(16)) float baz[32];
• Простые аллокации:
– MSVC: _aligned_malloc(size, align)/_aligned_free()
– Gcc: _mm_malloc(size, align)/_mm_free()
• STL: выравнивающий аллокатор:
std::vector<T, AlignmentAllocator<T, 16> > foo;
Все элементы вектора выровнены на 16:
class __attribute__ ((aligned (16))) Foo {
__attribute__ ((aligned (16))) float bar[4];
};
std::vector<Foo, AlignmentAllocator<Foo, 16> > baz;
У последнего примера – проблемы с Microsoft STL (починено в VS2012)
18. Compiler intrinsics: нудную работу
должен делать компилятор
Стандартные типы данных Макросы для команд
__m128
__m128 => SSE-регистр _mm_add_ps(__m128,__m128)
(float[4] или int16[8] или еще 6
вариантов) => addps (или vaddps)
Компилятор:
__m128d => double[2] • Распределяет регистры
• Подставляет адреса C-
Типы AVX-256: переменных
__m256 => float[8] • Генерирует код под целевую
архитектуру (SSE, AVX)
__m256d => double[4]
• Может переупорядочить
команды
20. SIMD: Итого
1. Это не больно!
2. Выигрыш в 2-4 раза - запросто
3. Трудный старт
4. Где брать информацию:
– Intel Software Developers Manual, Vol 2A & 2B
– MSDN: руководство по Visual Studio, много
мелких примеров по _mm-макросам
– Собирать по крупицам…
22. Intel SPMD Program Compiler (ISPC)
• SPMD – Single Program, Multiple Data
• Метафора: «пишем программу для одного SIMD-
элемента, компилятор расширит на весь вектор»
• x86: SSE2, SSE4, AVX, AVX2, Xeon Phi
• Поддерживается Intel: BSD-license, opensource,
http://ispc.github.com
23. ISPC: пример и синтаксис
Сложение векторов Расширение C:
• uniform – разделяемая копия
переменной на все
параллельные/SIMD потоки
varying – приватная копия
• foreach/foreach_tiled – SIMD-
цикл (тело цикла векторизуется)
запускается
• launch - запуск задачи
• Встроенные переменные:
programIndex – номер SIMD-
lane
programCount – ширина SIMD
taskIndex – номер задачи
• Далее – см. документацию
26. OpenCL
• Стандарт для «гетерогенных вычислений»
• Платформы: CPU/GPU
• Особенности
– Компиляция на лету
– Двойная буферизация
– Отдельный внешний рантайм
– Громоздкий API
• Применение
– Для больших объемов вычислений
– Если планируется переход на GPU-расчеты
27. Позитив
• Ускорение в 10-15 раз – достижимо
• Вероятно, потребуется рефакторинг
Но только один раз:
– Параллельный код неплохо масштабируется
– Рефакторинг для CPU => годится для GPU
• Но лучше – сразу писать эффективно, помня
о грядущей паралельности
28. Презентация (со ссылками) доступна:
www.slideshare.net/AlexTutubalin
Я иногда пишу на эти темы:
blog.lexa.ru/tags/sse
СПАСИБО ЗА ВНИМАНИЕ!
ВОПРОСЫ?
30. Bandwidth vs Latency
Bandwidth Latency
RAM,1980 13 MB/s 225 ns • Случайный доступ -
RAM, 2000 1600 MB/s 52 ns
(~125x) (~4.5x)
медленный
RAM, 2012 12800 MB/s 35ns – «Память – это новый диск»
(~1000x) (~6.5x) – «Диск – это новая лента»
HDD, 1980 0.6 MB/s 48 ms
HDD, 2000 86 MB/s 5.7 ms
• У CPU – то же самое!!
(~150x) (~8.5x) • Кэши – немного спасают
HDD,2012 300 MB/s ~5 ms
(~500x) (~10x) • LAN – приятное
10 Mbit 1 MB/s 3000 µsec исключение из общего
Ethernet
правила
56Gbit ~5000 MB/s 0.7 µsec
Infiniband (5000x) (~4300x)
31. BW/Latency: решения
• Нужно не зависеть от Latency:
– Последовательный код
– Нет зависимости по данным
– Последовательный доступ к памяти (и диску)
– Prefetch в кэши
• А любая случайность – зло:
– Обращение по указателю
– Синхронизация (!)
– Не предсказанный условный переход
– Случайный disk seek
32. Векторизация С/C++: алиасинг
Сложение двух векторов:
void sum_vec(float *s1,float *s2, float *d, int N){
for(int i=0;i<N;i++)
d[i] = s1[i]+ s2[i];
}
Вызов:
float A[SZ]; sum_vec(A,A+1,A+2,SZ-2);
Решение: restrict (C99), __restrict__(gcc C++), __restrict (MSVC):
void sum_vec(float * __restrict__ s1,
float* __restrict__ s2, float* __restrict__ d….
Но:
• Алиасинг бывает и на this
• В библиотечных классах – может не быть указан restrict
33. Векторизация в C++ компиляторах
• «Обычный код» (после базовых правок выравнивания и
алиасинга):
– Личный опыт: ускорения «в разы» компилятор не дает,
нужно вручную.
– Чужой опыт: «An Evaluation of Vectorizing Compilers» (2011)
http://polaris.cs.uiuc.edu/~garzaran/doc/pact11.pdf
Для кода Media Bench II получили:
• Intel C++/GCC: среднее ускорение 1.2 раза.
• Ручной ассемблерный код: ускорение 2.7 раза.
• Нужны серьезные правки кода, см. например
Program Optimization Trough Loop Vectorization
https://wiki.engr.illinois.edu/download/attachments/114
688007/9-Vectorization.pdf
34. Модификация кода для успешной
векторизации
Не векторизуется Векторизуется
for(i=0;i<LEN;i++){ for(i=0;i<LEN;i++){
sum = 0.0; sum[i] = 0.0;
for(j=0;j<LEN;j++) for(j=0;j<LEN;j++)
sum+=A[j][i]; sum[i]+=A[j][i];
B[i] = sum; B[i] = sum[i];
} }
36. OpenCL: корни в GPGPU
• Много отдельных
потоков (Work Items)
• Объединенных в блоки
с быстрой общей
памятью (Work Groups)
• Блоков много, порядок
исполнения не
определен
• Общая глобальная
(медленная) память