Я пытаюсь написать библиотеку для AVX2 в Ada 2012, используя компилятор GNAT GCC. В настоящее время я определил тип данных Vec_256_Integer_32 следующим образом:
type Vector_256_Integer_32 is array (0 .. 7) of Integer_32;
pragma Pack(Vec_256_Integer_32);
Обратите внимание, что я выровнял массив в соответствии с границей в 32 байта, указанной в документации Intel для встроенной функции _mm256_load_si256
из immintrin.h
.
Я хотел бы реализовать операцию, которая объединяет два таких массива вместе с помощью AVX2. Прототип функции выглядит следующим образом.
function Vector_256_Integer_32_Add (Left, Right : Vector_256_Integer_32) return Vector_256_Integer_32
Моя идея реализации этой функции состоит в том, чтобы сделать это в три шага.
_mm256_load_si256
в локальную переменную._mm256_add_epi32
.Vec_256_Unsigned_32
, используя _mm256_store_si256
.Что меня смущает, так это то, как бы я создал тип данных __m256i в Аде для хранения промежуточных результатов. Может кто-нибудь пролить свет на это? Кроме того, если вы видите какие-либо проблемы с моим подходом, любые отзывы приветствуются.
Я нашел определение __m256i в GCC (находится в gcc/gcc/config/i386/avxintrin.h).
typedef long long __m256i __attribute__ ((__vector_size__ (32), __may_alias__));
Однако здесь я застрял, так как не уверен, как перенести это в код Ады.
Я обнаружил, что атрибут __vector_size__
задокументирован здесь.
Это второстепенный вопрос, который вам не поможет: ваш «пакет прагмы» имеет два аргумента. Ada RM говорит, что у него должен быть только один аргумент — тип, который нужно упаковать. Что означает второй аргумент, число «32»? Есть ли в вашем компиляторе Ады нестандартный пакет прагм с двумя аргументами?
Еще одна придирка: ваше объявление функции «добавить» имеет тип параметра и идентификаторы параметров в неправильном порядке. У вас должно быть "a, b: Vec_256_Unsigned_32". Еще раз извините за комментарий, который на самом деле не отвечает на ваш вопрос...
Комментарий, который может быть вам полезен: в GNAT/GCC есть некоторая автовекторизация, как описано в docs.adacore.com/live/wave/gnat_ugn/html/gnat_ugn/gnat_ugn/…. Если это вам не поможет, могу только посоветовать написать дополнение на ассемблере, возможно, используя пакет System.Machine_Code, который должен быть предопределен в вашем GNAT.
re: ваше последнее редактирование: _mm256_add_epi32
является правильной встроенной функцией для целых чисел со знаком или без знака. x86 - это машина с дополнением до 2, поэтому нет отдельной инструкции для добавления со знаком или без знака; это та же бинарная операция. Внутренние имена по умолчанию равны epi
вместо epu
для инструкций, где одно и то же имя одинаково хорошо работает как со знаком, так и без знака.
Первое, что вам нужно сделать, это выучить Ада, так как 2/3 ваших объявлений Ады недействительны. Pragma Pack имеет только один аргумент (у GNAT нет версии, зависящей от реализации, с двумя), и в Аде 12 обычно следует использовать аспект, а не прагму. Выравнивание указано иначе. Ада не имеет «прототипов функций». Объявление функции для вашей операции сложения должно быть
function "+" (Left : in Vec_256_Unsigned_32; Right : in Vec_256_Unsigned_32) return Vec_256_Unsigned_32;
В Ada есть перегрузка операторов и пакеты для инкапсуляции и управления пространством имен, поэтому вам не нужны префиксы для всего, как в языках, в которых отсутствуют эти важные функции.
IIUC, определение C __m256i определяет массив long long
, который занимает 32 байта. Так как Interfaces.C не определяет эквивалент для long long
, эквивалент Ады зависит от размера long long
. Если это 64 бита, то это эквивалентно Interfaces.Integer_64, что составляет 8 байтов, поэтому эквивалент Ada будет
type M256i is array (1 .. 4) of Interfaces.Integer_64 with Convention => C;
(Все, что вы передаете подпрограмме C, должно быть определено в Interfaces.C или его дочерних элементах или объявлено с использованием соглашения C.)
Поскольку и M256i, и Vec_256_Unsigned_32 имеют размер 32 байта, вы можете конвертировать между ними, используя экземпляры Ada.Unchecked_Conversion.
Спасибо за ваш отзыв. Прошу прощения за то, что не был более внимателен. У меня просто вопрос вдогонку. Вместо использования pragma Pack следует использовать аспект/атрибут выравнивания. Например, должен ли я написать for Vec_256_Unsigned_32'Alignment use 32
, чтобы выровнять по границе 32 байта?
Определение GCC __m256i
как вектора long long
элементов является чисто деталью реализации. Операции над ним могут использовать любой размер элемента от байта до 64-бит или байтовый сдвиг в пределах 128-битных половинок. (_mm256_bslli_epi128
- felixcloutier.com/x86/pslldq). В GNU C вы можете писать такие вещи, как va += vb
, но это считается плохой практикой по сравнению с va = _mm256_add_epi64(va, vb)
, который также переносим на MSVC. _mm256_add_epi8(va, vb)
использует 8-битные элементы SIMD. Внутренний API Intel не определяет перегрузки операторов.
Но в любом случае, да, представление объекта — это 32-байтовый вектор. Однако он передается по значению в регистрах YMM, в отличие от массивов C. И встроенные функции не являются реальными функциями, они не существуют в библиотеке, где бы вы ни вызывали их, они встраиваются в одну инструкцию в «сайте вызова» в программе на C. Например, насыщающее добавление u8, которое хочет использовать OP, определено в avx2intrin.h GCC как extern __inline __m256i __attribute__ ((__gnu_inline__, __always_inline__, __artificial__))
с телом return (__m256i)__builtin_ia32_paddusb256 ((__v32qi)__A, (__v32qi)__B);
GCC определяет некоторые типы векторов для внутреннего использования, например __v32qi
, вектор из 32 четвертьцелочисленных элементов. Встроенная функция требует аргументы этого типа, поэтому она приводит к этому. Из другого языка, который не имеет собственных встроенных функций AVX2, вероятно, лучшее, что вы можете сделать, это передать указатели на функции C, которые вы пишете сами. (Что должно принимать указатель + длину и цикл в C, а не выполнять вызов функции для каждого вектора и заставлять несколько операндов сохранения/перезагрузки для каждого временного вектора!)
Я понял ответ на свой вопрос после проведения дополнительных исследований. Спасибо за ваш вклад. Я публикую это, так что, надеюсь, кто-то еще может извлечь из этого пользу.
Обновлено: я скорректировал свой ответ в соответствии с отзывами комментатора Питера Кордеса.
Например, если вы хотите определить тип данных 8 32-битных целых чисел со знаком, вы должны написать
type Vector_256_Integer_32 is array (0 .. 7) of Integer_32 with Convention => C, Alignment => 32;
Функция для сложения двух векторов вместе будет определена как
function "+" (Left, Right: Vector_256_Integer_32) return Vector_256_Integer_32;
pragma Import (Intrinsic, "+", "__builtin_ia32_paddd256");
Обратите внимание, что я использую встроенные функции GCC, а не встроенные функции из immintrin.h (потому что я не знаю, как импортировать встроенные функции из этого заголовочного файла).
В документации _mm256_add_epi32
указано, что используется инструкция vpaddd
. Похоже, что GCC __builtin_ia32_paddd256
переводится в эту инструкцию.
Ниже приведен пример программы на языке Ада и файла объявлений.
avx2.объявления
with Interfaces; use Interfaces;
package AVX2 is
--
-- Type Definitions
--
-- 256-bit Vector of 32-bit Signed Integers
type Vector_256_Integer_32 is array (0 .. 7) of Integer_32;
for Vector_256_Integer_32'Alignment use 32;
pragma Machine_Attribute (Vector_256_Integer_32, "vector_type");
pragma Machine_Attribute (Vector_256_Integer_32, "may_alias");
--
-- Function Definitions
--
-- Function: 256-bit Vector Addition of 32-bit Signed Integers
function Vector_256_Integer_32_Add
(Left, Right : Vector_256_Integer_32) return Vector_256_Integer_32 with
Convention => Intrinsic, Import => True,
External_Name => "__builtin_ia32_paddd256";
end AVX2;
main.adb
with AVX2; use AVX2;
with Interfaces; use Interfaces;
with Ada.Text_IO; use Ada.Text_IO;
procedure Main is
a, b, r : Vector_256_Integer_32;
begin
for i in Vector_256_Integer_32'Range loop
a (i) := 5 * (Integer_32 (i) + 5);
b (i) := 12 * (Integer_32 (i) + 12);
end loop;
r := Vector_256_Integer_32_Add(a, b);
for i in Vector_256_Integer_32'Range loop
Put_Line
("r(i) = a(i) + b(i) = " & a (i)'Image & " + " & b (i)'Image & " = " &
r (i)'Image);
end loop;
end Main;
Вот эквивалентная программа на C. Обратите внимание, что этот код был протестирован только в GCC и не обязательно является самым эффективным.
#include <stdio.h>
#include <immintrin.h>
#include <stdint.h>
int main()
{
__m256i ma;
__m256i mb;
__m256i mr;
int32_t a[8] __attribute__((aligned(32)));
int32_t b[8] __attribute__((aligned(32)));
int32_t r[8] __attribute__((aligned(32)));
for (int i = 0; i < 8; ++i) {
a[i] = 5 * (i + 5);
b[i] = 12 * (i + 12);
}
ma = _mm256_load_si256((void *const)a);
mb = _mm256_load_si256((void *const)b);
mr = _mm256_add_epi32(ma, mb);
_mm256_store_si256((void *)r, mr);
for (int i = 0; i < 8; ++i) {
printf("r[i] = a[i] + b[i] = %d + %d = %d\n", a[i], b[i], r[i]);
}
}
Обратите внимание, что вам нужно убедиться, что GNAT действительно может связываться со встроенными функциями в immintrin.h. - Возможно, вы упустили суть встроенных функций C. Это оболочки для встроенных компиляторов, а не библиотечные функции. Связывать не с чем. Посмотрите code-gen для функции C++: godbolt.org/z/48aYcPaez показывает __m256i f(__m256i a, __m256i b){ return _mm256_adds_epu8(a,b); }
компиляцию в vpaddusb ymm0, ymm0, ymm1
; ret
, так как аргументы передаются и возвращаются в векторных регистрах AVX, а встроенная расширяется до одной ассемблерной инструкции. Даже в отладочной сборке нет вызова.
Если вы хотите написать функции C для вызова из Ada, напишите функции, которые принимают указатели на весь массив, и зацикливайте внутри функции C. Написание реальных функций C, таких как struct wrap_m256i MM256_Adds_EPU8(const __m256i *, const __m256i *)
, было бы менее эффективным, а встроенные функции загрузки/сохранения были бы просто 32-байтными memcpy, так что это практически бессмысленно. Вы не можете передавать векторы по значению, если ваш компилятор Ады не знает, как использовать векторные типы SIMD изначально для аргументов функции/возвращаемых значений.
Вам не нужно использовать типы доступа. Определение function MM256_Load_SI256 (a : in out Vec_256_Unsigned_8) return M256u8 with ...;
или function MM256_Load_SI256 (a : aliased Vec_256_Unsigned_8) return M256u8 with ...;
заставит Аду передать указатель на параметр.
Примечания по стилю C: в современном C вы бы использовали alignas(32) int32_t a[8];
и #include <stdalign.h>
. (Или используйте C11 _Align()
без включения). Нет необходимости в непортативном __attribute__
, чтобы делать то, что теперь стандартизировано. И вам не нужно объявлять vars в верхней части функции, поэтому __m256i ma = _mm256_load_si256((const __m256i*)a);
, когда вы будете готовы использовать это, будет типичным (и многими считается лучшим стилем).
Re: эффективность: векторная часть в порядке. Нет недостатка в том, чтобы сообщить компилятору, что загрузки/хранения выровнены (используя load
вместо loadu
, как вы делаете), если вы можете дешево выровнять свои данные. Загрузка SIMD сразу после скалярного сохранения, скорее всего, приведет к остановке переадресации хранилища, что приведет к дополнительной задержке, но это всего лишь пример. И компилятор может превратить этот цикл инициализации в копию константы. (Желательно с vpmovzxbd
, чтобы упаковать целые числа в байты в .rodata, но GCC не настолько умен или не хочет обменивать дополнительную работу ALU на экономию места.)
_mm256_load_si256
— это встроенные функции загрузки, которые вам нужны._mm256_load_epi32
— странно избыточная версия, добавленная с AVX-512. Конечно, если ваш компилятор Ады не знает о них как о встроенных функциях или о чем-то, что вы можете определить в терминах встроенных функций, это не поможет. Весь смысл встроенных функций заключается в том, что они в основном компилируются в отдельные машинные инструкции (или в нагрузку, которая может быть операндом источника памяти для другой инструкции), а не в фактические вызовы функций. Я, к сожалению, ничего не знаю об Аде (языке), только его тезке.