Ниже это похоже на встроенные функции, однако я не знаком с внутренними функциями. Пожалуйста, помогите мне преобразовать реальный код. В частности, testFunc() для меня более неоднозначен. Я предполагаю, что это также для скалярного произведения двух векторов с плавающей запятой, но метки Lrep и Lexit меня смущают. Пожалуйста, разберитесь для меня ясно. А встроенные функции доступны для мобильного процессора?
void testFunc(int M, int N, int K, float* A, float* B, float* C)
{
float *a;
float *b = new float[K*N];
float *pointb = B;
float *bb;
float *answer = C;
float c[8];
for (int j = 0, k; j < K; j++) {
bb = b + j;
for (k = N / 8; k > 0; k--) {
*bb = *pointb++; bb += K;
*bb = *pointb++; bb += K;
*bb = *pointb++; bb += K;
*bb = *pointb++; bb += K;
*bb = *pointb++; bb += K;
*bb = *pointb++; bb += K;
*bb = *pointb++; bb += K;
*bb = *pointb++; bb += K;
}
for (k = N / 8 * 8; k < N; k++) {
*bb = *pointb++; bb += K;
}
}
int K8 = K / 8 * 8;
for (int i = 0; i < M; i++) for (int k = 0; k < N; k++) {
a = A + i * K;
bb = b + k * K;
__asm {
mov esi, K8;
sub esi, 8;
shl esi, 2;
xor edi, edi;
mov edx, a;
mov ebx, bb;
vxorps ymm3, ymm3, ymm3;
Lrep:
cmp edi, esi;
jg Lexit;
vmovups ymm0, ymmword ptr[edx + edi];
vfmadd231ps ymm3, ymm0, ymmword ptr[ebx + edi];
add edi, 32;
jmp Lrep;
Lexit:
vmovups ymmword ptr[c], ymm3;
}
for (int j = K8; j < K; ) {
*c += *(a + j) * *(bb + j); j++;
}
*answer = (c[0] + c[1] + c[2] + c[3] + c[4] + c[5] + c[6] + c[7]);
answer++;
}
}
а также
pA = A;
for (k = 0; k < K; k++) {
pC = C;
for (i = 0; i < M; i++) {
pA = A + i * K + k;
pB = B + k * N;
for (j = N / 32; j > 0; j--) {
_asm {
mov eax, pC;
mov ebx, pA;
mov ecx, pB;
vmovups ymm0, ymmword ptr[eax];
vmovss xmm1, dword ptr[ebx];
vbroadcastss ymm4, xmm1;
vmovups ymm2, ymmword ptr[ecx];
vfmadd231ps ymm0, ymm4, ymm2;
vmovups ymmword ptr[eax], ymm0;
}
pC += 8; pB += 8;
_asm {
mov eax, pC;
mov ebx, pA;
mov ecx, pB;
vmovups ymm0, ymmword ptr[eax];
vmovss xmm1, dword ptr[ebx];
vbroadcastss ymm4, xmm1;
vmovups ymm2, ymmword ptr[ecx];
vfmadd231ps ymm0, ymm4, ymm2;
vmovups ymmword ptr[eax], ymm0;
}
pC += 8; pB += 8;
_asm {
mov eax, pC;
mov ebx, pA;
mov ecx, pB;
vmovups ymm0, ymmword ptr[eax];
vmovss xmm1, dword ptr[ebx];
vbroadcastss ymm4, xmm1;
vmovups ymm2, ymmword ptr[ecx];
vfmadd231ps ymm0, ymm4, ymm2;
vmovups ymmword ptr[eax], ymm0;
}
pC += 8; pB += 8;
_asm {
mov eax, pC;
mov ebx, pA;
mov ecx, pB;
vmovups ymm0, ymmword ptr[eax];
vmovss xmm1, dword ptr[ebx];
vbroadcastss ymm4, xmm1;
vmovups ymm2, ymmword ptr[ecx];
vfmadd231ps ymm0, ymm4, ymm2;
vmovups ymmword ptr[eax], ymm0;
}
pC += 8; pB += 8;
}
for (j = N / 32 * 32; j < N; j++) {
*pC += *pA * *pB;
pC += 1; pB += 1;
}
}
}
Привет, секпер. Спасибо за ваш ответ. Я уже скопировал код сборки в свой код. Моя проблема не в том, что я не работаю с ассемблерным кодом, а в том, что не могу правильно отладить этот встроенный ассемблерный код. Отладчик VS 2015 пропускает эти ассемблерные строки, поэтому указывает неправильную строку.
Я сделаю первые пару строк, чтобы вы начали, но на самом деле, если вы не можете прочитать сборку, вам нужно будет обратиться к руководству по процессору Intel, чтобы расшифровать его.
mov esi, K8;
sub esi, 8;
shl esi, 2;
xor edi, edi;
mov edx, a;
mov ebx, bb;
mov esi, K8
Отсюда вам нужно будет ознакомиться с бинарной и базовой архитектурой процессора и операндами языка ассемблера, которые имеют отношение к вашей проблеме, в зависимости от ваших знаний. Как только вы сможете прочитать каждую строку, вы сможете расшифровать блоки и, наконец, программу.
Спасибо за добрый ответ. Тогда как насчет «vxorps ymm3, ymm3, ymm3;»? Что я должен узнать больше об этом?
Несмотря на то, что я не сталкивался с инструкцией vxorps до того, как быстрый поиск в Google был всем, что мне нужно, чтобы найти определение. Я также новичок в SO, но я уверен, что идея состоит не в том, чтобы просто ответить вам на все, а в том, чтобы подтолкнуть вас в правильном направлении. Поправьте меня если я ошибаюсь.
2 векторные загрузки (из одной и той же позиции в 2 массивах), подающие FMA в векторный аккумулятор, пахнут для меня скалярным произведением.
Я не проверял справочное руководство asm, чтобы увидеть, что операнд назначения был суммой, а не 1 множимых, но это имеет смысл.
Тройной вложенный цикл выглядит как матричное умножение. Он передает 1 вход, выполняя векторную загрузку из другого для подачи FMA, поэтому, вероятно, он генерирует SIMD-вектор результатов для выходной строки.
Использование встроенного синтаксиса ассемблера MSVC для этого довольно плохо; он может принимать входные данные только через операнды памяти, поэтому он принудительно перезагружает + сохраняет между каждым блоком asm. Если вы собираетесь развернуться, используйте один большой ассемблерный оператор и используйте смещения в режимах адресации.
ИДК, почему цикл dot-produce написан неэффективно (как с условной, так и с безусловной ветвью внутри цикла), а не развернут с несколькими аккумуляторами. В значительной степени побеждает цель ручного кодирования на ассемблере. См. Почему mulss занимает всего 3 такта на Haswell, в отличие от таблиц инструкций Agner?, чтобы узнать, как использовать несколько аккумуляторов, чтобы скрыть задержку FMA. Или позвольте clang сделать это за вас при развертывании + векторизации чистого цикла C.
Я также не знаю, почему он не суммирует результат по горизонтали, а просто сохраняет его в памяти с помощью vmovups [c], ymm3
. Кажется бессмысленным. Я предполагаю, что вызывающая сторона должна перезагрузиться из памяти и суммировать, или вы можете объявить функцию как возвращающую вектор __m256
и игнорировать хранилище.
В любом случае, вы, очевидно, можете написать точечный продукт в скалярном коде C, возможно, используя fma(a[i], b[i], sum)
из math.h, чтобы воспроизвести поведение asm, не округляя временный результат.
Или скопируйте ручную векторизацию с помощью встроенных функций, таких как sum = _mm256_fmadd_ps(_mm256_loadu_ps(a[i]), _mm256_loadu_ps(b[i]), sum);
или что-то в этом роде. (См. Руководство по внутренним функциям Intel).
Во встроенных функциях этот код повторяется 4 раза.
{
// vmovups ymm0, ymmword ptr[eax];
__m256 tempC = _mm256_loadu_ps((float*)pC);
// vmovss xmm1, dword ptr[ebx];
// vbroadcastss ymm4, xmm1;
__m256 tempA = _mm256_set1_ps(*pA);
// vmovups ymm2, ymmword ptr[ecx];
__m256 tempB = _mm256_loadu_ps((float*)pB);
// vfmadd231ps ymm0, ymm4, ymm2;
__m256 result = _mm256_fmadd_ps(tempA, tempB, tempC);
// vmovups ymmword ptr[eax], ymm0;
_mm256_storeu_ps(pC, result);
}
pC += 8; pB += 8;
Однако постоянная передача одного и того же значения из pA кажется немного избыточной.
Спасибо за добрый ответ. Не могли бы вы порекомендовать материалы для изучения внутренностей?
@Сейон Не совсем. Это тот случай, когда ресурсы быстро устаревают. Я просто использую руководство по внутренним функциям Intel: software.intel.com/sites/landingpage/IntrinsicsGuide/… и godbolt (с флагами -mavx2 -mfma -O2 и -ffast-maths).
@robthebloke и будущие читатели: предпочитайте -march=haswell
или -march=znver1
-mavx2 -mfma
. Особенно с gcc, установка -mtune=
для процессора Intel с AVX2 + FMA исключает разделение loadu
/ storeu
на несколько ассемблерных инструкций. Почему gcc не разрешает _mm256_loadu_pd как одиночный vmovupd? Если ваши данные действительно выравниваются во время выполнения, это, возможно, большой недостаток.
Одним из преимуществ использования встроенных функций вместо asm является то, что компилятор может вывести трансляцию из цикла. (По крайней мере, если вы используете float *restrict pC
или pA
, чтобы компилятор знал, что сохранение через pC
не повлияет на значения, считанные из pA
.) И да, когда я писал свой ответ, я подумал, что что-то странное в этом матмул асм, но я не мог т положить мой палец на это. Избыточные нагрузки объясняют, почему при быстром беглом просмотре не было видно, идет ли цикл вниз по столбцу или по строке. Хорошо подмечено.
Вы можете напрямую скопировать ассемблерный код в блок
__asm
. Однако архитектура вашего проекта должна быть x86, поскольку x64 не поддерживается.