ݺߣ

ݺߣShare a Scribd company logo
Эффективное использование
   x86-совместимых CPU
   Многопоточность, векторные расширения




         Алексей Тутубалин
            lexa@lexa.ru
          CTO @LibRaw LLC
Производительность 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 => роста
   очень медленный
Локальная параллельность
• Только в хотспотах
  – Замена обычных циклов - параллельными
  – Замена обычных команд – векторными (SIMD:
    SSE, AVX)
• Сопутствующие изменения
  – Редизайн/рефакторинг структур данных
  – Изменения в алгоритмах
ЛОКАЛЬНАЯ МНОГОПОТОЧНОСТЬ
Схема работы
• Fork/join на месте
  длинных циклов
• Накладные расходы
  – n*1-10µsec на каждый поток
• Методы реализации
  – Полуавтоматически: OpenMP
  – Вручную: threading-библиотеки
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];
OpenMP
• Чужеродность для С++
  – Нет поддержки exceptions
• Не все компиляторы поддерживают
• Бывают ошибки в рантайме (или
  компиляторе)
• Альтернатива: Cilk Plus (Intel C++, gcc 4.8
  branch)
Параллельные библиотеки
Intel TBB: суммирование вектора
Итого
1. Это работает !
2. Эффективность:
  – Обычно 75-85% (3-3.5x speedup на 4-core)
  – >90% для «гигабайт» данных
3. Дополнительный код:
  – 1-20 строк на каждую точку оптимизации
SIMD-КОМАНДЫ (SSE, AVX)
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];
X86: много сортов SIMD
• Под что код делать будем?
  – 6 версий SSE
  – 2 версии AVX
• Чем новее версия
  – Тем больше вкусных команд
  – И тем уже поддержка
SSE/AVX: что умеет
Что есть:                Что есть:
• Команды                • 16 (8) регистров
  – Load/store           • Типы данных
  – Арифметика             – Целые: 8, 16, 32, 64 бит
  – Сравнения, min, max    – Плавающие: 32, 64
  – Конверсия типов
                        Чего нет:
  – Битовые операции
                        • Flow control
  – Перестановка
    элементов вектора • Scatter/gather
  – Dot Product, CRC32,
    AES, STRCMP
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 записи
SSE: выравнивание
• До Intel Core-i7:
  – aligned (16 байт) load/store – сильно быстрее
    (~4 раза для Core2Duo)
• Начиная с Core-i7:
  – Почти нет разницы (<10%)
С/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)
Откуда берется SSE/AVX-код?
1. Генерирует C/C++ компилятор
   (автоматическая векторизация)
2. Пишется руками
  – макросы компилятора (compiler intrinsics)
  – ассемблер
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]
                                   • Может переупорядочить
                                     команды
Ручной код: compiler intrinsics
C: 277 Mpix/sec (8.8 Gb/sec)       SSE/AVX: 584 Mpix/sec (18.6 Gb/s)
компилятор частично векторизовал
SIMD: Итого
1.   Это не больно!
2.   Выигрыш в 2-4 раза - запросто
3.   Трудный старт
4.   Где брать информацию:
     – Intel Software Developers Manual, Vol 2A & 2B
     – MSDN: руководство по Visual Studio, много
       мелких примеров по _mm-макросам
     – Собирать по крупицам… 
ПАРАЛЛЕЛЬНЫЕ/SIMD ЯЗЫКИ
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
ISPC: пример и синтаксис
Сложение векторов   Расширение C:
                    • uniform – разделяемая копия
                      переменной на все
                      параллельные/SIMD потоки
                      varying – приватная копия
                    • foreach/foreach_tiled – SIMD-
                      цикл (тело цикла векторизуется)
                      запускается
                    • launch - запуск задачи
                    • Встроенные переменные:
                      programIndex – номер SIMD-
                      lane
                      programCount – ширина SIMD
                      taskIndex – номер задачи
                    • Далее – см. документацию
ISPC: многопоточность
ISPC: практика
OpenCL
• Стандарт для «гетерогенных вычислений»
• Платформы: CPU/GPU
• Особенности
  –   Компиляция на лету
  –   Двойная буферизация
  –   Отдельный внешний рантайм
  –   Громоздкий API
• Применение
  – Для больших объемов вычислений
  – Если планируется переход на GPU-расчеты
Позитив
• Ускорение в 10-15 раз – достижимо
• Вероятно, потребуется рефакторинг
  Но только один раз:
  – Параллельный код неплохо масштабируется
  – Рефакторинг для CPU => годится для GPU
• Но лучше – сразу писать эффективно, помня
  о грядущей паралельности
Презентация (со ссылками) доступна:
www.slideshare.net/AlexTutubalin

Я иногда пишу на эти темы:
blog.lexa.ru/tags/sse




СПАСИБО ЗА ВНИМАНИЕ!
ВОПРОСЫ?
ДОПОЛНИТЕЛЬНЫЕ МАТЕРИАЛЫ
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)
BW/Latency: решения
• Нужно не зависеть от Latency:
  – Последовательный код
  – Нет зависимости по данным
  – Последовательный доступ к памяти (и диску)
  – Prefetch в кэши
• А любая случайность – зло:
  – Обращение по указателю
  – Синхронизация (!)
  – Не предсказанный условный переход
  – Случайный disk seek
Векторизация С/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
Векторизация в 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
Модификация кода для успешной
        векторизации
Не векторизуется      Векторизуется
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];
}                     }
ISPC: ускорение
(для поставляемых примеров)
OpenCL: корни в GPGPU
• Много отдельных
  потоков (Work Items)
• Объединенных в блоки
  с быстрой общей
  памятью (Work Groups)
• Блоков много, порядок
  исполнения не
  определен
• Общая глобальная
  (медленная) память
OpenCL: сложение векторов

More Related Content

Доклад на Highload-2012

  • 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)
  • 8. Параллельные библиотеки Intel TBB: суммирование вектора
  • 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)
  • 17. Откуда берется SSE/AVX-код? 1. Генерирует C/C++ компилятор (автоматическая векторизация) 2. Пишется руками – макросы компилятора (compiler intrinsics) – ассемблер
  • 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] • Может переупорядочить команды
  • 19. Ручной код: compiler intrinsics C: 277 Mpix/sec (8.8 Gb/sec) SSE/AVX: 584 Mpix/sec (18.6 Gb/s) компилятор частично векторизовал
  • 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) • Блоков много, порядок исполнения не определен • Общая глобальная (медленная) память

Editor's Notes

  • #8: + альтернативы
  • #9: TBB,QtConcurrent, boost::mapreduce
  • #10: 20м надо 16
  • #14: 20m
  • #16: 9 минут
  • #18: 25м
  • #21: 20 минут?
  • #33: 25m