7.8 Использование векторных инструкций через встроенные функции
На некоторых целевых платформах набор инструкций содержит векторные инструкции SIMD, которые одновременно обрабатывают несколько значений, хранящихся в одном большом регистре. Например, на x86 таким образом можно использовать расширения MMX, 3DNow! и SSE.
Первый шаг при использовании этих расширений — объявить необходимые типы данных. Для этого следует использовать соответствующий typedef:
typedef int v4si __attribute__ ((vector_size (16)));
Тип int задаёт базовый тип (которым может быть typedef), а атрибут задаёт размер вектора для переменной в байтах. Например, приведённое выше объявление заставляет компилятор задать для типа v4si режим шириной 16 байт, разделённый на единицы размером int. Для 32-разрядного int это означает вектор из 4 единиц по 4 байта, а соответствующий режим foo — V4SI.
Атрибут vector_size применим только к целочисленным скалярным типам и типам с плавающей точкой, хотя в сочетании с этой конструкцией допускаются массивы, указатели и возвращаемые функциями значения. В настоящее время допускаются только размеры, являющиеся положительными степенями двойки, кратными размеру базового типа.
В качестве базовых типов можно использовать все основные целочисленные типы, как знаковые, так и беззнаковые: char, short, int, long, long long. Кроме того, float и double можно использовать для создания векторных типов с плавающей точкой.
Если указать сочетание, недопустимое для текущей архитектуры, GCC синтезирует инструкции, используя более узкий режим. Например, если указать переменную типа V4SI, а архитектура не поддерживает этот конкретный тип SIMD, GCC создаст код, использующий 4 SIs.
Определённые таким образом типы можно использовать с подмножеством обычных операций C. В настоящее время GCC позволяет применять к этим типам следующие операторы: +, -, *, /, unary minus, ^, |, &, ~, %.
Операции работают подобно valarrays в C++. Сложение определяется как сложение соответствующих элементов операндов. Например, в приведённом ниже коде каждый из 4 элементов a складывается с соответствующим элементом b, а результирующий вектор сохраняется в c.
typedef int v4si __attribute__ ((vector_size (16))); v4si a, b, c; c = a + b;
Вычитание, умножение, деление и логические операции работают аналогичным образом. Так же результат применения унарного минуса или побитового дополнения к векторному типу — это вектор, элементы которого являются отрицательными или побитово дополненными значениями соответствующих элементов операнда.
К векторам целочисленного типа можно применять операторы сдвига <<, >>. Операция определяется следующим образом: {a0,
a1, …, an} >> {b0, b1, …, bn} == {a0 >> b0, a1 >> b1,
…, an >> bn}. В отличие от OpenCL, значения b не приводятся неявно по модулю разрядности базового типа B, и поведение не определено, если любое bi больше или равно B.
В отличие от скалярных операций в C и C++, операнды целочисленных векторных операций не подвергаются целочисленным преобразованиям.
Операнды бинарных векторных операций должны иметь одинаковое число элементов.
Для удобства допускается бинарная векторная операция, в которой один из операндов — скаляр. В этом случае компилятор преобразует скалярный операнд в вектор, каждый элемент которого равен этому скаляру. Такое преобразование выполняется только в том случае, если скаляр можно безопасно преобразовать к типу элемента вектора. Рассмотрим следующий код.
typedef int v4si __attribute__ ((vector_size (16)));
v4si a, b, c;
long l;
a = b + 1; /* a = b + {1,1,1,1}; */
a = 2 * b; /* a = {2,2,2,2} * b; */
a = l + a; /* Error, cannot convert long to int. */
К элементам вектора можно обращаться по индексу, как если бы вектор был массивом с тем же числом элементов и базовым типом. Выход за границы массива приводит к неопределённому поведению во время выполнения. Предупреждения о выходе за границы при индексировании вектора можно включить с помощью -Warray-bounds.
Сравнение векторов поддерживается стандартными операторами сравнения: ==, !=, <, <=, >, >=. Операндами сравнения могут быть векторные выражения целочисленного типа или типа с плавающей точкой. Сравнение векторов целочисленного типа с векторами типа с плавающей точкой не поддерживается. Результатом сравнения является вектор той же ширины и с тем же числом элементов, что и операнды сравнения, со знаковым целочисленным типом элементов.
Сравнение выполняется поэлементно: результат равен 0, если сравнение ложно, и -1 (константе соответствующего типа, в которой установлены все биты) в противном случае. Рассмотрим следующий пример.
typedef int v4si __attribute__ ((vector_size (16)));
v4si a = {1,2,3,4};
v4si b = {3,2,1,4};
v4si c;
c = a > b; /* The result would be {0, 0,-1, 0} */
c = a == b; /* The result would be {0,-1, 0,-1} */
В C++ доступен тернарный оператор ?:. a?b:c, где b и c — векторы одного типа, а a — целочисленный вектор с тем же числом элементов того же размера, что и у b и c, вычисляет все три аргумента и создаёт вектор {a[0]?b[0]:c[0], a[1]?b[1]:c[1], …}. Обратите внимание: в отличие от OpenCL, a интерпретируется как a != 0, а не как a < 0. Как и в случае бинарных операций, этот синтаксис допускается, если один из операндов b или c является скаляром, который затем преобразуется в вектор. Если и b, и c являются скалярами, а тип true?b:c имеет тот же размер, что и тип элемента a, то b и c преобразуются в векторный тип, элементы которого имеют этот тип, а число элементов совпадает с числом элементов a.
В C++ для векторов доступны логические операторы !, &&, ||. !v эквивалентно v == 0, a && b эквивалентно a!=0 & b!=0, а a || b эквивалентно a!=0 | b!=0. Для смешанных операций скаляра s и вектора v, s && v эквивалентно s?v!=0:0 (вычисление выполняется с коротким замыканием), а v && s эквивалентно v!=0 & (s?-1:0).
Перестановку элементов вектора можно выполнять с помощью функций __builtin_shuffle (vec, mask) и __builtin_shuffle (vec0, vec1, mask). Обе функции создают перестановку элементов одного или двух векторов и возвращают вектор того же типа, что и входной вектор или векторы. mask — целочисленный вектор той же ширины (W) и с тем же числом элементов (N), что и выходной вектор.
Элементы входных векторов нумеруются в порядке расположения в памяти: элементы vec0 начинаются с 0, а элементы vec1 — с N. Элементы mask рассматриваются по модулю N в случае одного операнда и по модулю 2*N в случае двух операндов.
Рассмотрим следующий пример:
typedef int v4si __attribute__ ((vector_size (16)));
v4si a = {1,2,3,4};
v4si b = {5,6,7,8};
v4si mask1 = {0,1,1,3};
v4si mask2 = {0,4,2,5};
v4si res;
res = __builtin_shuffle (a, mask1); /* res is {1,2,2,4} */
res = __builtin_shuffle (a, b, mask2); /* res is {1,5,3,6} */
Обратите внимание: __builtin_shuffle намеренно семантически совместима с функциями OpenCL shuffle и shuffle2.
Можно объявлять переменные и использовать их в вызовах функций и возвращаемых значениях, а также при присваиваниях и некоторых преобразованиях типов. Векторный тип можно указать в качестве возвращаемого типа функции. Векторные типы также можно использовать в качестве аргументов функций. Допускается преобразование одного векторного типа в другой, если их размеры совпадают (на самом деле векторы также можно преобразовывать в другие типы данных того же размера и обратно).
Выполнять операции с векторами разной длины или разной знаковости без преобразования типов нельзя.
Перестановку элементов вектора можно выполнять с помощью функции __builtin_shufflevector (vec1, vec2, index...). vec1 и vec2 должны быть выражениями векторного типа с совместимым типом элементов. Результат __builtin_shufflevector — это вектор с тем же типом элементов, что и у vec1 и vec2, но с числом элементов, равным числу указанных индексов.
Аргументы index — это список целых чисел, задающих индексы элементов первых двух векторов, которые следует извлечь и вернуть в новом векторе. Индексы элементов нумеруются последовательно, начиная с первого вектора и продолжаясь во втором. Индекс -1 можно использовать, чтобы указать, что соответствующий элемент возвращаемого вектора не имеет значения и может быть выбран произвольно для оптимизации генерируемой последовательности кода, выполняющей операцию перестановки.
Рассмотрим следующий пример:
typedef int v4si __attribute__ ((vector_size (16)));
typedef int v8si __attribute__ ((vector_size (32)));
v8si a = {1,-2,3,-4,5,-6,7,-8};
v4si b = __builtin_shufflevector (a, a, 0, 2, 4, 6); /* b is {1,3,5,7} */
v4si c = {-2,-4,-6,-8};
v8si d = __builtin_shufflevector (c, b, 4, 0, 5, 1, 6, 2, 7, 3); /* d is a */
Преобразование вектора можно выполнить с помощью функции __builtin_convertvector (vec, vectype). vec должно быть выражением целочисленного векторного типа или векторного типа с плавающей точкой, а vectype — целочисленным векторным типом или векторным типом с плавающей точкой с тем же числом элементов. Результат имеет тип vectype и значение, полученное приведением каждого элемента vec к типу элемента vectype согласно правилам приведения в C.
Рассмотрим следующий пример:
typedef int v4si __attribute__ ((vector_size (16)));
typedef float v4sf __attribute__ ((vector_size (16)));
typedef double v4df __attribute__ ((vector_size (32)));
typedef unsigned long long v4di __attribute__ ((vector_size (32)));
v4si a = {1,-2,3,-4};
v4sf b = {1.5f,-2.5f,3.f,7.f};
v4di c = {1ULL,5ULL,0ULL,10ULL};
v4sf d = __builtin_convertvector (a, v4sf); /* d is {1.f,-2.f,3.f,-4.f} */
/* Equivalent of:
v4sf d = { (float)a[0], (float)a[1], (float)a[2], (float)a[3] }; */
v4df e = __builtin_convertvector (a, v4df); /* e is {1.,-2.,3.,-4.} */
v4df f = __builtin_convertvector (b, v4df); /* f is {1.5,-2.5,3.,7.} */
v4si g = __builtin_convertvector (f, v4si); /* g is {1,-2,3,7} */
v4si h = __builtin_convertvector (c, v4si); /* h is {1,5,0,10} */
Иногда удобно писать код, сочетающий универсальные векторные операции (для ясности) и машинно-зависимые векторные встроенные функции (для доступа к векторным инструкциям, не предоставляемым универсальными встроенными функциями). На x86 встроенные функции для целочисленных векторов обычно используют один и тот же векторный тип __m128i независимо от того, как интерпретируется вектор, поэтому их аргументы и возвращаемые значения необходимо преобразовывать из других векторных типов и в них. В C можно использовать тип union:
#include <immintrin.h>
typedef unsigned char u8x16 __attribute__ ((vector_size (16)));
typedef unsigned int u32x4 __attribute__ ((vector_size (16)));
typedef union {
__m128i mm;
u8x16 u8;
u32x4 u32;
} v128;
для переменных, которые можно использовать как со встроенными операторами, так и со встроенными функциями x86:
v128 x, y = { 0 };
memcpy (&x, ptr, sizeof x);
y.u8 += 0x80;
x.mm = _mm_adds_epu8 (x.mm, y.mm);
x.u32 &= 0xffffff;
/* Instead of a variable, a compound literal may be used to pass the
return value of an intrinsic call to a function expecting the union: */
v128 foo (v128);
x = foo ((v128) {_mm_adds_epu8 (x.mm, y.mm)});
© Free Software Foundation
Licensed under the GNU Free Documentation License, Version 1.3.
https://gcc.gnu.org/onlinedocs/gcc-15.3.0/gcc/Vector-Extensions.html