Эффективное использование
   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 операций на такт
• Инструкции/такт                –    2013: 32 операции на такт
   после 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
  – C++-объекты – «целостны», нельзя иметь
    shared (a.foo) private (a.bar)
• Не все компиляторы поддерживают
• Бывают ошибки в рантайме (или
  компиляторе)
• Альтернатива: Cilk Plus (Intel C++, gcc 4.8
  branch)
Параллельные библиотеки
Intel TBB: суммирование вектора
Итого
1. Это работает !
2. Эффективность:
  –   Обычно 75-85% (3-3.5x speedup на 4-core)
  –   >90% для «гигабайт» данных
3. Дополнительный код:
  –   1-20 строк на каждую точку оптимизации
4. Проблемы:
  –   CPU overbooking
  –   NUMA
  –   Синхронизация (или ее отсутствие)
  –   Неудачные структуры данных => плохое кэширование
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
  – И это не считая AMD 
• Чем новее версия
  – Тем больше вкусных команд
  – И тем меньше процессоров поддерживает
SSE/AVX: что умеет
Что есть:                 Что есть:
• Команды                 • 16 (8) регистров
  – Load/store            • Типы данных
  – Арифметика                – Целые: 8, 16, 32, 64 бит
  – Сравнения, min, max       – Плавающие: 32, 64
  – Конверсия типов
                           Чего нет:
  – Битовые операции
                           • Сложных функций
  – Перестановка
                             (sin/cos/exp/pow/log)
    элементов вектора
  – Dot                    • Flow control
    Product, CRC32, AES, S • Scatter/gather
    TRCMP
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)
  – Ассемблер
3. «Параллельные языки»
  – OpenCL
  – ISPC
  – Много вариантов domain specific
Векторные расширения
• gcc и совместимые (Intel C++/Linux, Clang)
typedef float __v4sf
__attribute__ ((__vector_size__ (16)));
 __v4sf x0,x1,x2;
x0 = x1+x2; // поэлементное сложение
• Результат
  – Нормальный в очень простых случаях
  – Печальный в нетривиальных случаях
Векторизация С/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
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. Нет лишних затрат ресурсов
  Векторные блоки – есть в CPU, если вы их не
  используете, то они простаивают
4. Трудный старт
5. Где брать информацию:
  – 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
• Но лучше – сразу писать эффективно, помня
  о грядущей паралельности
Математические функции
             (sin,cos,pow,exp,log)
• Медленные «стандартные реализации»
• Медленные в FP87 (sin: до 100 тактов, log: 724)
• Быстрые реализации: через SSE-библиотеки,
  полиномами:
   – В поставке компилятора (Intel, MSVC, gcc builtins)
   – Встроенные библиотеки в ISPC
   – Есть очень быстрые opensource-варианты
      • http://web.archive.org/web/20080531151036/http://www.arithmex.com/
      • http://gruntthepeon.free.fr/ssemath/
• В частных случаях
   – можно заменять своими вариантами,
     оптимизированными под диапазон или точность
• Аппаратно реализованы на GPU: выигрыш >30x
Bandwidth vs Latency
              Bandwidth           Latency
RAM,1980           13 MB/s           225 ns       • Случайный доступ -
RAM, 2000       1.6 GB/s ~125x     52 ns ~4.5x      медленный
RAM, 2012      12.8 GB/s ~1000x    50ns ~4.5x        – «Память – это новый
4 chan, OC     ~50Gb/s ~4000x      35ns ~6.5x          диск»
L1 Cache       160Gb/s ~12300x     0.8ns 280x        – «Диск – это новая
HDD, 1980         0.6 MB/s           48 ms             лента»
HDD, 2000       86 MB/s ~150x     5.7 ms ~8.5x    • У CPU – то же самое!!
HDD,2012
SSD, 2012
               300 MB/s ~500x
               500Mb/s ~1000x
                                   ~5 ms ~10x
                                  0.1ms ~500x
                                                  • Кэши – немного
10 Mbit Eth        1 MB/s           3000 µs
                                                    спасают
10G Ethern.     1 Gb/s 1000x       3 µs 1000x     • LAN, SSD – приятное
                                                    исключение из
56Gbit IB       ~5 GB/s 5000x     0.7 µs ~4300x
                                                    общего правила
Как жить
• (Почти) не зависят от Latency:
  – Последовательный код
  – Нет зависимости по данным
  – Последовательный доступ к памяти (и диску)
    Единственная эффективная структура данных:
    большой массив с линейным доступом к нему
• Упираются в Latency
  –   Обращение по указателю
  –   Синхронизация (!)
  –   Не предсказанный условный переход
  –   Случайный disk seek
Презентация (со ссылками) доступна:
www.slideshare.net/AlexTutubalin

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




СПАСИБО ЗА ВНИМАНИЕ!
ВОПРОСЫ?
OpenCL: корни в GPGPU
• Много отдельных
  потоков (Work Items)
• Объединенных в блоки
  с быстрой общей
  памятью (Work Groups)
• Блоков много, порядок
  исполнения не
  определен
• Общая глобальная
  (медленная) память
OpenCL: сложение векторов

Доклад в Mail.ru 01.11.12

  • 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 операций на такт • Инструкции/такт – 2013: 32 операции на такт после Intel Core2 => рост очень медленный
  • 3.
    Локальная параллельность • Тольков хотспотах – Замена обычных циклов - параллельными – Замена обычных команд – векторными (SIMD: SSE, AVX) • Сопутствующие изменения – Редизайн/рефакторинг структур данных – Изменения в алгоритмах
  • 4.
  • 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 – C++-объекты – «целостны», нельзя иметь shared (a.foo) private (a.bar) • Не все компиляторы поддерживают • Бывают ошибки в рантайме (или компиляторе) • Альтернатива: 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 строк на каждую точку оптимизации 4. Проблемы: – CPU overbooking – NUMA – Синхронизация (или ее отсутствие) – Неудачные структуры данных => плохое кэширование
  • 10.
  • 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 – И это не считая AMD  • Чем новее версия – Тем больше вкусных команд – И тем меньше процессоров поддерживает
  • 13.
    SSE/AVX: что умеет Чтоесть: Что есть: • Команды • 16 (8) регистров – Load/store • Типы данных – Арифметика – Целые: 8, 16, 32, 64 бит – Сравнения, min, max – Плавающие: 32, 64 – Конверсия типов Чего нет: – Битовые операции • Сложных функций – Перестановка (sin/cos/exp/pow/log) элементов вектора – Dot • Flow control Product, CRC32, AES, S • Scatter/gather TRCMP
  • 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) – Ассемблер 3. «Параллельные языки» – OpenCL – ISPC – Много вариантов domain specific
  • 18.
    Векторные расширения • gccи совместимые (Intel C++/Linux, Clang) typedef float __v4sf __attribute__ ((__vector_size__ (16))); __v4sf x0,x1,x2; x0 = x1+x2; // поэлементное сложение • Результат – Нормальный в очень простых случаях – Печальный в нетривиальных случаях
  • 19.
    Векторизация С/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
  • 20.
    Векторизация в 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
  • 21.
    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] • Может переупорядочить команды
  • 22.
    Ручной код: compilerintrinsics C: 277 Mpix/sec (8.8 Gb/sec) SSE/AVX: 584 Mpix/sec (18.6 Gb/s) компилятор частично векторизовал
  • 23.
    SIMD: Итого 1. Этоне больно! 2. Выигрыш в 2-4 раза - запросто 3. Нет лишних затрат ресурсов Векторные блоки – есть в CPU, если вы их не используете, то они простаивают 4. Трудный старт 5. Где брать информацию: – Intel Software Developers Manual, Vol 2A & 2B – MSDN: руководство по Visual Studio, много мелких примеров по _mm-макросам – Собирать по крупицам… 
  • 24.
  • 25.
    Intel SPMD ProgramCompiler (ISPC) • SPMD – Single Program, Multiple Data • Метафора: «пишем программу для одного SIMD- элемента, компилятор расширит на весь вектор» • x86: SSE2, SSE4, AVX, AVX2 (и Xeon Phi) • Поддерживается Intel: BSD- license, opensource, http://ispc.github.com
  • 26.
    ISPC: пример исинтаксис Сложение векторов Расширение C: • uniform – разделяемая копия переменной на все параллельные/SIMD потоки varying – приватная копия • foreach/foreach_tiled – SIMD- цикл (тело цикла векторизуется) запускается • launch - запуск задачи • Встроенные переменные: programIndex – номер SIMD- lane programCount – ширина SIMD taskIndex – номер задачи • Далее – см. документацию
  • 27.
  • 28.
  • 29.
    OpenCL • Стандарт для«гетерогенных вычислений» • Платформы: CPU/GPU • Особенности – Компиляция на лету – Двойная буферизация – Отдельный внешний рантайм – Громоздкий API • Применение – Для больших объемов вычислений – Если планируется переход на GPU-расчеты
  • 30.
    Позитив • Ускорение в10-15 раз – достижимо • Вероятно, потребуется рефакторинг Но только один раз: – Параллельный код неплохо масштабируется – Рефакторинг для CPU => годится для GPU • Но лучше – сразу писать эффективно, помня о грядущей паралельности
  • 31.
    Математические функции (sin,cos,pow,exp,log) • Медленные «стандартные реализации» • Медленные в FP87 (sin: до 100 тактов, log: 724) • Быстрые реализации: через SSE-библиотеки, полиномами: – В поставке компилятора (Intel, MSVC, gcc builtins) – Встроенные библиотеки в ISPC – Есть очень быстрые opensource-варианты • http://web.archive.org/web/20080531151036/http://www.arithmex.com/ • http://gruntthepeon.free.fr/ssemath/ • В частных случаях – можно заменять своими вариантами, оптимизированными под диапазон или точность • Аппаратно реализованы на GPU: выигрыш >30x
  • 32.
    Bandwidth vs Latency Bandwidth Latency RAM,1980 13 MB/s 225 ns • Случайный доступ - RAM, 2000 1.6 GB/s ~125x 52 ns ~4.5x медленный RAM, 2012 12.8 GB/s ~1000x 50ns ~4.5x – «Память – это новый 4 chan, OC ~50Gb/s ~4000x 35ns ~6.5x диск» L1 Cache 160Gb/s ~12300x 0.8ns 280x – «Диск – это новая HDD, 1980 0.6 MB/s 48 ms лента» HDD, 2000 86 MB/s ~150x 5.7 ms ~8.5x • У CPU – то же самое!! HDD,2012 SSD, 2012 300 MB/s ~500x 500Mb/s ~1000x ~5 ms ~10x 0.1ms ~500x • Кэши – немного 10 Mbit Eth 1 MB/s 3000 µs спасают 10G Ethern. 1 Gb/s 1000x 3 µs 1000x • LAN, SSD – приятное исключение из 56Gbit IB ~5 GB/s 5000x 0.7 µs ~4300x общего правила
  • 33.
    Как жить • (Почти)не зависят от Latency: – Последовательный код – Нет зависимости по данным – Последовательный доступ к памяти (и диску) Единственная эффективная структура данных: большой массив с линейным доступом к нему • Упираются в Latency – Обращение по указателю – Синхронизация (!) – Не предсказанный условный переход – Случайный disk seek
  • 34.
    Презентация (со ссылками)доступна: www.slideshare.net/AlexTutubalin Я иногда пишу на эти темы: blog.lexa.ru/tags/sse СПАСИБО ЗА ВНИМАНИЕ! ВОПРОСЫ?
  • 35.
    OpenCL: корни вGPGPU • Много отдельных потоков (Work Items) • Объединенных в блоки с быстрой общей памятью (Work Groups) • Блоков много, порядок исполнения не определен • Общая глобальная (медленная) память
  • 36.

Editor's Notes