Интервью с Анатолием Кузнецовым, автором библиотеки BitMagic C++ Library
Simd
1. Производительность
Вычислительная мощность
компьютера (производительность
компьютера) — это количественная
характеристика скорости выполнения
определённых операций на компьютере.
Технически современный микропроцессор
выполнен в виде одной сверхбольшой
интегральной схемы, состоящей из
нескольких миллиардов элементов — это
одна из самых сложных конструкций,
созданных человеком.
1
2. Закон Мура
Гордон Мур, один из основателей
компании Intel, сформулировал
получивший его имя закон в 1965
году. Описание закономерности
звучит так: «Количество
транзисторов, размещаемых на
кристалле интегральной схемы,
удваивается каждые 12 месяцев». В
1975 году Мур скорректировал свое
предсказание, увеличив этот срок до
двух лет.
2
6. Векторный VS Скалярный
Скалярный процессор
for i = 1 to 10 do
IF – Instruction Fetch(next)
ID – Instruction Decode
Load Operand1
Load Operand2
Add Operand1 Operand2
Store Result
end for
Векторный процессор
IF – Instruction Fetch(next)
ID – Instruction Decode
Load Operand1
Load Operand2
Add Operand1 Operand2
Store Result
6
Меньше преобразований адресов
Меньше IF, ID
Меньше конфликтов конвейера, ошибок предсказания
переходов
Эффективнее доступ к памяти (2 выборки vs 20)
Операция над операндами выполняется параллельно
Уменьшился размер кода
7. Что такое векторные инструкции
SIMD (single instruction, multiple data — одиночный поток команд,
множественный поток данных) — принцип компьютерных
вычислений, позволяющий обеспечить параллелизм на уровне
данных.
7
Одна инструкция, много данных
Каждая операция применяется к
N значений в одном (большом)
регистре фиксированного
размера (128, 256, 512 бит)
Может быть до N раз быстрее
обычных АЛУ
11. SSE2
2001, Pentium 4, IA32, x86-64 (Intel 64, 2004)
16 векторных регистров шириной 128 бит: xmm0 – xmm15
Добавлено 144 инструкции к 70 инструкциям SSE
Продолжение SSE работает с вещественными числами
SSE2 включает в себя ряд команд управления кэшем
Типы векторов:
16 x char
8 x short int
4 x float или int
2 x double
1 x 128-bit int
11
12. SSE3/4
Intel SSE3: 2003, Pentium 4 Prescott, IA32, x86-64 (Intel 64, 2004)
Добавлено 13 новых инструкции к инструкциям SSE2
Возможность горизонтальной работы с регистрами – команды
сложения и вычитания нескольких значений, хранящихся в одном
регистре
12
Intel SSE4: 2006, Intel Core, AMD
Bulldozer
Добавлено 54 новых инструкций
SSE 4.1 – 47
SSE 4.2 – 7
13. AVX
2008, Intel Sandy Bridge (2011), AMD Bulldozer (2011)
Добавлено 13 новых инструкции к инструкциям SSE2
Размер векторов увеличен до 256 бит
Векторные регистры переименованы: ymm0 – ymm15
Регистры xmm# – это младшие 128 бит регистров ymm#
Трехоперандный синтаксис AVX-инструкций: С = A + B
Использование ymm регистров требует поддержки со стороны операционной системы (для
сохранения регистров при переключении контекстов)
Linux ядра >= 2.6.30
Apple OS X 10.6.8
Windows 7 SP 1
Поддержка компиляторами:
GCC 4.6
Intel C++ Compiler 11.1
Microsoft Visual Studio 2010
Open64 4.5.1
13
19. Инструкции
19
Операции копирования данных (mem-reg/reg-mem/reg-reg)
Scalar: MOVSS
Packed: MOVAPS, MOVUPS, MOVLPS, MOVHPS,MOVLHPS, MOVHLPS
Арифметические операции
Scalar: ADDSS, SUBSS, MULSS, DIVSS, RCPSS,
SQRTSS, MAXSS, MINSS, RSQRTSS
Packed: ADDPS, SUBPS, MULPS, DIVPS, RCPPS,
SQRTPS, MAXPS, MINPS, RSQRTPS
Операции сравнения
Scalar: CMPSS, COMISS, UCOMISS
Pacled: CMPPS
Поразрядные логические операции
Packed: ANDPS, ORPS, XORPS, ANDNPS
......
Математика Скалярный
оператор
Векторный
оператор
X = X + Y addss addps
X = X – Y subss subps
X = X × Y mulss mulps
X = X / Y divss divps
X = rcpss rcpps
X = sqrtss sqrtps
X = rsqrtss rsqrtps
X = max(X, Y) maxss maxps
X = min(X, Y) minss minps
Математика Скалярный
оператор
Векторный
оператор
X = X + Y addss addps
X = X – Y subss subps
X = X × Y mulss mulps
X = X / Y divss divps
rcpss rcpps
sqrtss sqrtps
rsqrtss rsqrtps
X = max(X, Y) maxss maxps
X = min(X, Y) minss minps
26. Intrinsic в C++
26
Intrinsics – набор встроенных функций и типов данных, поддерживаемых
компилятором, для предоставления высокоуровневого доступа к SSE-инструкциям
Компилятор самостоятельно распределяет XMM/YMM регистры, принимает решение
о способе загрузки данных из памяти (проверяет выравнен адрес или нет) и т.п.
Заголовочные файлы
#include <mmintrin.h> /* MMX */
#include <xmmintrin.h> /* SSE, нужен также mmintrin.h */
#include <emmintrin.h> /* SSE2, нужен также xmmintrin.h */
#include <pmmintrin.h> /* SSE3, нужен также emmintrin.h */
#include <smmintrin.h> /* SSE4.1 */
#include <nmmintrin.h> /* SSE4.2 */
#include <immintrin.h> /* AVX */
__m128 float[4]
__m128d double[2]
__m128i char[16], short int[8], int[4], uint64_t [2]
Типы встроенных данных
27. Intrinsic в деле
27
#include "iostream"
#include "xmmintrin.h"
int main()
{
const auto N = 8;
alignas(16) float a[] = { 41982.0, 81.5091, 3.14, 42.666,
54776.45, 342.4556, 6756.2344, 4563.789 };
alignas(16) float b[] = { 85989.111, 156.5091, 3.14, 42.666,
1006.45, 9999.4546, 0.2344, 7893.789 };
__m128* a_simd = reinterpret_cast<__m128*>(a);
__m128* b_simd = reinterpret_cast<__m128*>(b);
auto size = sizeof(float);
void *ptr = _aligned_malloc(N * size, 32);
float* c = reinterpret_cast<float*>(ptr);
for (size_t i = 0; i < N/2; i++, a_simd++, b_simd++, c += 4)
_mm_store_ps(c, _mm_add_ps(*a_simd, *b_simd));
c -= N;
std::cout.precision(10);
for (size_t i = 0; i < N; i++)
std::cout << c[i] << std::endl;
_aligned_free(ptr);
system("PAUSE");
return 0;
}
28. SIMD в C#
28
На данный момент поддержка этой технологии в .NET представлена в пространстве имен
System.Numerics.Vectors и представляет собой библиотеку векторных типов, которые могут
использовать преимущества аппаратного ускорения SIMD. Она содержит следующие типы:
Vector — коллекцию статических удобных методов для работы с универсальными
векторами
Matrix3x2 — представляет матрицу 3х2
Matrix4х4 — представляет матрицу 4х4
Plane — представляет трехмерную плоскость
Quaternion — представляет вектор, используемый для кодирования трехмерных
физических поворотов
Vector<(Of <(<'T>)>)> представляет вектор указанного числового типа, который подходит
для низкоуровневой оптимизации параллельных алгоритмов
Vector2 — представляет вектор с двумя значениями одинарной точности с плавающей
запятой
Vector3 — представляет вектор с тремя значениями одинарной точности с плавающей
запятой
Vector4 — представляет вектор с четырьмя значениями одинарной точности с плавающей
запятой
29. System.Numerics.Vectors
29
using System;
using System.Numerics;
static void Main(string[] args)
{
const Int32 N = 8;
Single[] a = { 41982.0F, 81.5091F, 3.14F, 42.666F, 54776.45F, 342.4556F, 6756.2344F, 4563.789F };
Single[] b = { 85989.111F, 156.5091F, 3.14F, 42.666F, 1006.45F, 9999.4546F, 0.2344F, 7893.789F };
Single[] c = new Single[N];
for (int i = 0; i < N; i += Vector<Single>.Count)
{
var aSimd = new Vector<Single>(a, i);
var bSimd = new Vector<Single>(b, i);
Vector<Single> cSimd = aSimd + bSimd;
cSimd.CopyTo(c, i);
}
for (int i = 0; i < a.Length; i++)
Console.WriteLine(c[i]);
Console.ReadKey();
}
30. Простой пример
30
[Benchmark(Description = "VectorSum")]
public int VectorSum()
{
var vSize = Vector<int>.Count;
for (int i = 0; i < c.Length; i += vSize)
{
var aa = new Vector<int>(a, i);
var bb = new Vector<int>(b, i);
aa += bb;
aa.CopyTo(c, i);
}
return c[0];
}
[Benchmark(Description = "SimpleSum", Baseline = true)]
public int SimpleSum()
{
for (int i = 0; i < c.Length; i++)
c[i] = a[i] + b[i];
return c[0];
}
Method Platform Jit Median StdDev Scaled
SimpleSum X64 LegacyJit 2.3991 us 0.0818 us 1
SimpleSum X64 RyuJit 4.9439 us 0.4746 us 2.06
SimpleSum X86 LegacyJit 4.0082 us 0.3158 us 1.66
VectorSum X64 LegacyJit 51.9921 us 4.5165 us 21.67https://github.com/PerfDotNet/BenchmarkDotNet/ - benchmark for .NET
32. А на практике ???
32
Свёртка (в случае изображения) — это операция вычисления нового значения
заданного пикселя, при которой учитываются значения окружающих его соседних
пикселей.
33. Пример использования свертки
33
Фильтрация
Свёртка — очень полезная и распространённая операция, лежащая в основе различных
фильтров (размытие, повышение резкости, нахождение краёв, подавление шумов).
Фильтр улучшения четкостиФильтр размытие
34. Пример использования свертки
34
Сверточные нейронные сети
Имеется матрица на входе
(картинка) и есть ядро свертки
которое состоит из весов (эти веса в
процессе обучения сверточной
нейронной сети настраиваются).
Ядро построено таким образом, что
графически кодирует какой-либо
один признак, например, наличие
наклонной линии под
определенным углом.
Тогда следующий слой, получившийся в результате операции свёртки такой матрицей
весов, показывает наличие данной наклонной линии в обрабатываемом слое и её
координаты, формируя так называемую карту признаков (англ. feature map).
35. Реализация операции свертки
35
unsafe public static int[][] Convolution(int[][] img, int[,]
filter)
{
....
var vSize = Vector<int>.Count;
for (int i = 0; i < resHeight; ++i) {
res[i] = new int[resWidth];
fixed (int* resP = res[i], filterP = filter) {
for (int j = 0; j < resWidth; j += vSize) {
var kernel = Vector<int>.Zero;
for (int fi = 0, imgI = i, filterI = 0;
fi < filterHeight; ++fi, ++imgI, filterI += filterWidth)
for (int fj = 0; fj < filterWidth; ++fj) {
var imgV = new Vector<int>(img[imgI], j + fj);
var filterV = new Vector<int>(filterP[filterI + fj]);
kernel += imgV * filterV;
}
kernel.CopyTo(res[i], j);
}
}
}
return res;
}
public static int[][] Convolution(int[][] img, int[,] filter)
{
....
for (int i = 0; i < resHeight; ++i) {
res[i] = new int[resWidth];
for (int j = 0; j < resWidth; ++j) {
var kernel = 0;
for (int fi = 0, imgI = i; fi < filterHeight; ++fi, ++imgI)
for (int fj = 0; fj < filterWidth; ++fj)
kernel += img[imgI][j+fj] * filter[fi, fj];
}
res[i][j] = kernel;
}
}
return res;
}
4 строки – unsafe оптимизация работы с массивом
6 строк – векторизация
36. Benchmark
36
Method Platform Jit Median Scaled
SimpleJagged X86 LegacyJit 93.9177 ms 1
VectorJagged X86 LegacyJit 251.0950 ms 2.67
SimpleJagged X64 LegacyJit 103.8635 ms 1
VectorJagged X64 LegacyJit 341.3388 ms 3.28
SimpleJagged X64 RyuJit 96.0608 ms 1
VectorJagged X64 RyuJit 16.5649 ms 0.17
37. В итоге
37
Что мы имеем:
Во многих случаях векторизация дает увеличение производительности (размер вектора,
реализация)
Сложные алгоритмы потребуют изобретательность, но без этого никуда
System.Numerics.Vectors в настоящее время обхватывает только часть simd-инструкций.
Для более серьезного подхода потребуется С++
Есть множество других способов помимо векторизации: правильное использование кэша,
многопоточность, грамотная работа с памятью(чтобы сборщик мусора не потел) и т.д.
SIMD инструкции это один из способов выжать производительность, но не единственный.
Источник
параллелизма
Ускорение Усилие программиста Популярность
Множество ядер 2х-128х Умеренное Высокая
Множество машин 1х-Бесконечность Умеренно-Высокое Высокая
Векторизация 2х-8х Умеренно-Высокое Низкая
Графические адаптеры 128х-2048х Высокое Низкая
Ключевыми элементами любого микропроцессора являются дискретные переключатели – транзисторы. Блокируя и пропуская электрический ток (включение-выключение), они дают возможность логическим схемам компьютера работать в двух состояниях, то есть в двоичной системе.
Размеры транзисторов измеряются в нанометрах. Один нанометр (нм) – это одна миллиардная (10−9) часть метра.
На срезе одного человеческого волоса можно разместить более 2000 транзисторных затворов, выполненных по 45-нм производственной технологии.
7
7
Мировые производители микроэлектроники при совершенствовании техпроцесса изготовления кристаллов все чаще сталкиваются с физическими ограничениями основного материала, используемого для выращивания чипов. Компании уже давно заняты поиском альтернатив кремнию и, по всей видимости, близки к тому, чтобы перейти на новый тип полупроводников.
Сложности возникли уже на этапе 14 нм. При уменьшении элементов и расстояния между ними производителям все сложнее получить устойчивые состояния транзисторов (открыт/закрыт) без которого невозможно использовать бинарную логику. Приближаясь к единицам нанометров, сказываются физические ограничения кремния, которые уже нельзя преодолеть, даже используя трехмерную структуру и другие технологические «присадки».
«Два последних технологических перехода помогли нам понять, что на сегодняшний день самый реалистичный срок — выпуск новых чипов раз в два с половиной года, а не в два», — заявил руководитель Intel Брайан Крзэнич.
7
Complex instruction set computer — вычисления со сложным набором команд. Процессорная архитектура, основанная на усложнённом наборе команд.
Reduced instruction set computer — вычисления с упрощённым набором команд. Архитектура процессоров, построенная на основе упрощённого набора команд, характеризуется наличием команд фиксированной длины, большого количества регистров, операций типа регистр-регистр, а также отсутствием косвенной адресации.
Minimum instruction set computer — вычисления с минимальным набором команд.
Very long instruction word — сверхдлинное командное слово. Архитектура процессоров с явно выраженным параллелизмом вычислений, заложенным в систему команд процессора.
Конве́йер — способ организации вычислений, используемый в современных процессорах и контроллерах с целью повышения их производительности (увеличения числа инструкций, выполняемых в единицу времени). Идея заключается в параллельном выполнении нескольких инструкций процессора.
Суперскалярность — архитектура вычислительного ядра, использующая несколько декодеров команд, которые могут загружать работой множество исполнительных блоков. Планирование исполнения потока команд является динамическим и осуществляется самим вычислительным ядром.
7
Отличается от скалярных процессоров, которые могут работать только с одним операндом в единицу времени. Абсолютное большинство процессоров являются скалярными или близкими к ним. Векторные процессоры были распространены в сфере научных вычислений, где они являлись основой большинства суперкомпьютеров начиная с 1980-х до 1990-х. Но резкое увеличение производительности и активная разработка новых процессоров привели к вытеснению векторных процессоров из сферы повседневных процессоров.
7
Факторы влияющие на производительность векторного процессора
Доля кода в векторной форме
Длина вектора (векторного регистра)
Количество векторных регистров
Количество векторных модулей доступа к памяти (load-store)
Таким образом, математические операции выполняются гораздо быстрее, основным ограничивающим фактором становится время, необходимое для извлечения данных из памяти.
7
В большинстве современных микропроцессоров имеются векторные расширения.
7
MMX (Multimedia Extensions — мультимедийные расширения) — коммерческое название дополнительного SIMD набора инструкций, разработанного компанией Intel и впервые представленного в 1997 году одновременно с линией процессоров Pentium MMX. Набор инструкций был предназначен для ускорения процессов кодирования/декодирования потоковых аудио и видеоданных.
&lt;number&gt;
SSE - набор инструкций, разработанный Intel и впервые представленный в процессорах серии Pentium III как ответ на аналогичный набор инструкций 3DNow! от AMD, который был представлен годом раньше.
Технология SSE позволяла преодолеть две основные проблемы MMX: при использовании MMX невозможно было одновременно использовать инструкции сопроцессора, так как его регистры были общими с регистрами MMX, и возможность MMX работать только с целыми числами.
SSE включает в архитектуру процессора восемь 128-битных регистров и набор инструкций, работающих со скалярными и упакованными типами данных.
Управляющий регистр MXCSR (SIMD Floating-Point Control/Status Register) предназначен для маскирования/демаскирования SIMD-исключений, управления режимом округления и тд.
&lt;number&gt;
SSE2 расширяет набор инструкций SSE с целью полностью вытеснить MMX. Набор SSE2 добавил 144 новые команды к SSE, в котором было только 70 команд.
Процессор, поддерживающий SSE2, требуется для установки Windows 8 и Microsoft Office 2013
&lt;number&gt;
SSE3
Если говорить более конкретно, добавлены команды сложения и вычитания нескольких значений, хранящихся в одном регистре. Эти команды упростили ряд DSP- и 3D-операций. Существует также новая команда для преобразования значений с плавающей точкой в целые без необходимости вносить изменения в глобальном режиме округления.
SSE4
Добавлены инструкции, ускоряющие компенсацию движения в видеокодеках, быстрое чтение из USWC памяти, множество инструкций для упрощения векторизации программ компиляторами.
Кроме того, в SSE4.2 добавлены инструкции обработки строк 8/16 битных символов, вычисления CRC32 (контрольная сумма), POPCNT(подсчет числа единичных битов).
&lt;number&gt;
AVX предоставляет различные улучшения, новые инструкции и новую схему кодирования машинных кодов.
Для большинства новых инструкций отсутствуют требования к выравниванию операндов в памяти. Однако рекомендуется следить за выравниванием на размер операнда, во избежание значительного снижения производительности.
Подходит для интенсивных вычислений с плавающей точкой в мультимедиа-программах и научных задачах. Там, где возможна более высокая степень параллелизма, увеличивает производительность с вещественными числами.
&lt;number&gt;
&lt;number&gt;
&lt;number&gt;
&lt;number&gt;
Результат помещается в младшее двойное слово (32-bit) операнда-назначения (xmm1). Три старших двойных слова из операнда-источника (xmm0) копируются в операнд-назначение (xmm1)
Результат помещается в младшие 64 бита операнда-назначения (xmm1). Старшие 64 бита из операнда-источника (xmm0) копируются в операнд-назначение (xmm1)
&lt;number&gt;
&lt;number&gt;
&lt;number&gt;
&lt;number&gt;
&lt;number&gt;
&lt;number&gt;
&lt;number&gt;
&lt;number&gt;
&lt;number&gt;
&lt;number&gt;
alignas(#) — стандартный для С++ переносимый способ задания настраиваемого выравнивания переменных и пользовательских типов. Используется в С++11 и поддерживается Visual Studio 2015. Можно использовать и другой вариант — __declspec( align( #)) declarator. Данные средства для управления выравниванием при статическом выделении памяти. Если необходимо выравнивание с динамическим выделением, необходимо использовать void* _aligned_malloc(size_t size, size_t alignment);
Затем преобразуем указатель на массив a и b к типу _m128* при помощи reinterpret_cast, который позволяет преобразовывать любой указатель в указатель любого другого типа.
После динамически выделим выравненную память при помощи уже упомянутой выше функции _aligned_malloc(N*sizeof(float), 16);
Количество необходимых байт выделяем исходя из количества элементов с учетом размерности типа, а 16 это значение выравнивания, которое должно быть степенью двойки. А затем указатель на этот участок памяти приводим к другому типу указателя, чтобы с ним можно было бы работать с учетом размерности типа float как с массивом.
&lt;number&gt;
Впервые упоминание o поддержке JIT технологии SIMD было объявлено в блоге .NET в апреле 2014 года. Тогда разработчики анонсировали новую превью-версию RyuJIT, которая обеспечивала SIMD функциональность. Причиной добавления стала довольно высокая популярность запроса на поддержку C# and SIMD. Изначальный набор поддерживаемых типов был не большим и были ограничения по функциональности. Изначально поддерживался набор SSE, а AVX обещали добавить в релизе. Позже были выпущены обновления и добавлены новые типы с поддержкой SIMD и новые методы для работы с ними, что в последних версиях представляет обширную и удобную библиотеку для аппаратной обработки данных.
Такой подход облегчает жизнь разработчика, который не должен писать CPU-зависимый код. Вместо этого CLR абстрагирует аппаратное обеспечение, предоставляя виртуальную исполняющую среду, которая переводит свой код в машинные команды либо во время выполнения. Оставляя генерацию кода CLR, вы можете использовать один и тот же MSIL код на разных компьютерах с разными процессорами, не отказываясь от оптимизаций, специфических для данного конкретного CPU.
Аппаратное ускорение может приводить к значительному повышению производительности при математическом и научном программировании, а также при программировании графики.
Класс Vector предоставляет методы для сложения, сравнения, поиска минимума и максимума и многих других преобразований над векторами. При этом операции работают с использованием технологии SIMD. Остальные типы также поддерживают аппаратное ускорение и содержат специфические для них преобразования. Для матриц это может быть перемножение, для векторов евклидово расстояние между точками и т.д.
&lt;number&gt;
Несомненно, вы можете сделать больше на С++, чем на C#. Но вы действительно получаете кросс-процессорную поддержку на С#. Например, автоматический выбор между SSE4 и AVX. В целом, это не может не радовать. Ценой малых усилий мы можем получать от системы как можно большей производительности, задействуя все возможные аппаратные ресурсы.
&lt;number&gt;
&lt;number&gt;
&lt;number&gt;
&lt;number&gt;
&lt;number&gt;
Закладывает в сеть априорное знание о том, что объект может встретиться в любой части изображения
&lt;number&gt;