Как мне определить тип данных __m256i в Аде?

Я пытаюсь написать библиотеку для 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

Моя идея реализации этой функции состоит в том, чтобы сделать это в три шага.

  1. Загрузите a и b с помощью _mm256_load_si256 в локальную переменную.
  2. Выполните операцию сложения, используя _mm256_add_epi32.
  3. Преобразуйте результат обратно в тип 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__ задокументирован здесь.

_mm256_load_si256 — это встроенные функции загрузки, которые вам нужны. _mm256_load_epi32 — странно избыточная версия, добавленная с AVX-512. Конечно, если ваш компилятор Ады не знает о них как о встроенных функциях или о чем-то, что вы можете определить в терминах встроенных функций, это не поможет. Весь смысл встроенных функций заключается в том, что они в основном компилируются в отдельные машинные инструкции (или в нагрузку, которая может быть операндом источника памяти для другой инструкции), а не в фактические вызовы функций. Я, к сожалению, ничего не знаю об Аде (языке), только его тезке.
Peter Cordes 16.12.2022 08:32

Это второстепенный вопрос, который вам не поможет: ваш «пакет прагмы» имеет два аргумента. Ada RM говорит, что у него должен быть только один аргумент — тип, который нужно упаковать. Что означает второй аргумент, число «32»? Есть ли в вашем компиляторе Ады нестандартный пакет прагм с двумя аргументами?

Niklas Holsti 16.12.2022 10:44

Еще одна придирка: ваше объявление функции «добавить» имеет тип параметра и идентификаторы параметров в неправильном порядке. У вас должно быть "a, b: Vec_256_Unsigned_32". Еще раз извините за комментарий, который на самом деле не отвечает на ваш вопрос...

Niklas Holsti 16.12.2022 10:46

Комментарий, который может быть вам полезен: в GNAT/GCC есть некоторая автовекторизация, как описано в docs.adacore.com/live/wave/gnat_ugn/html/gnat_ugn/gnat_ugn/…‌​. Если это вам не поможет, могу только посоветовать написать дополнение на ассемблере, возможно, используя пакет System.Machine_Code, который должен быть предопределен в вашем GNAT.

Niklas Holsti 16.12.2022 11:03

re: ваше последнее редактирование: _mm256_add_epi32 является правильной встроенной функцией для целых чисел со знаком или без знака. x86 - это машина с дополнением до 2, поэтому нет отдельной инструкции для добавления со знаком или без знака; это та же бинарная операция. Внутренние имена по умолчанию равны epi вместо epu для инструкций, где одно и то же имя одинаково хорошо работает как со знаком, так и без знака.

Peter Cordes 18.12.2022 05:28
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
4
5
141
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Первое, что вам нужно сделать, это выучить Ада, так как 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 байта?

Alex F. 16.12.2022 13:22

Определение 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 не определяет перегрузки операторов.

Peter Cordes 16.12.2022 18:23

Но в любом случае, да, представление объекта — это 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);

Peter Cordes 16.12.2022 18:27

GCC определяет некоторые типы векторов для внутреннего использования, например __v32qi, вектор из 32 четвертьцелочисленных элементов. Встроенная функция требует аргументы этого типа, поэтому она приводит к этому. Из другого языка, который не имеет собственных встроенных функций AVX2, вероятно, лучшее, что вы можете сделать, это передать указатели на функции C, которые вы пишете сами. (Что должно принимать указатель + длину и цикл в C, а не выполнять вызов функции для каждого вектора и заставлять несколько операндов сохранения/перезагрузки для каждого временного вектора!)

Peter Cordes 16.12.2022 18:30
Ответ принят как подходящий

Я понял ответ на свой вопрос после проведения дополнительных исследований. Спасибо за ваш вклад. Я публикую это, так что, надеюсь, кто-то еще может извлечь из этого пользу.

Обновлено: я скорректировал свой ответ в соответствии с отзывами комментатора Питера Кордеса.

Например, если вы хотите определить тип данных 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, а встроенная расширяется до одной ассемблерной инструкции. Даже в отладочной сборке нет вызова.

Peter Cordes 16.12.2022 18:11

Если вы хотите написать функции C для вызова из Ada, напишите функции, которые принимают указатели на весь массив, и зацикливайте внутри функции C. Написание реальных функций C, таких как struct wrap_m256i MM256_Adds_EPU8(const __m256i *, const __m256i *), было бы менее эффективным, а встроенные функции загрузки/сохранения были бы просто 32-байтными memcpy, так что это практически бессмысленно. Вы не можете передавать векторы по значению, если ваш компилятор Ады не знает, как использовать векторные типы SIMD изначально для аргументов функции/возвращаемых значений.

Peter Cordes 16.12.2022 18:16

Вам не нужно использовать типы доступа. Определение 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 ...; заставит Аду передать указатель на параметр.

Jeffrey R. Carter 17.12.2022 11:27

Примечания по стилю C: в современном C вы бы использовали alignas(32) int32_t a[8]; и #include <stdalign.h>. (Или используйте C11 _Align() без включения). Нет необходимости в непортативном __attribute__, чтобы делать то, что теперь стандартизировано. И вам не нужно объявлять vars в верхней части функции, поэтому __m256i ma = _mm256_load_si256((const __m256i*)a);, когда вы будете готовы использовать это, будет типичным (и многими считается лучшим стилем).

Peter Cordes 18.12.2022 03:54

Re: эффективность: векторная часть в порядке. Нет недостатка в том, чтобы сообщить компилятору, что загрузки/хранения выровнены (используя load вместо loadu, как вы делаете), если вы можете дешево выровнять свои данные. Загрузка SIMD сразу после скалярного сохранения, скорее всего, приведет к остановке переадресации хранилища, что приведет к дополнительной задержке, но это всего лишь пример. И компилятор может превратить этот цикл инициализации в копию константы. (Желательно с vpmovzxbd, чтобы упаковать целые числа в байты в .rodata, но GCC не настолько умен или не хочет обменивать дополнительную работу ALU на экономию места.)

Peter Cordes 18.12.2022 03:57

Другие вопросы по теме