Как преобразовать этот ассемблерный код во внутренний код?

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

Вы можете напрямую скопировать ассемблерный код в блок __asm. Однако архитектура вашего проекта должна быть x86, поскольку x64 не поддерживается.

seccpur 22.07.2019 05:07

Привет, секпер. Спасибо за ваш ответ. Я уже скопировал код сборки в свой код. Моя проблема не в том, что я не работаю с ассемблерным кодом, а в том, что не могу правильно отладить этот встроенный ассемблерный код. Отладчик VS 2015 пропускает эти ассемблерные строки, поэтому указывает неправильную строку.

Seyon 22.07.2019 05:20
Стоит ли изучать 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 называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
2
557
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Я сделаю первые пару строк, чтобы вы начали, но на самом деле, если вы не можете прочитать сборку, вам нужно будет обратиться к руководству по процессору Intel, чтобы расшифровать его.

mov             esi, K8;
sub             esi, 8;
shl             esi, 2;
xor             edi, edi;
mov             edx, a;
mov             ebx, bb;
mov             esi, K8
  1. скопируйте содержимое K8 в esi
  2. вычесть 8 из значения в easi
  3. сдвиньте влево 2 бита esi и скопируйте результат в esi
  4. применить операцию xor к edi против edi (это будет 0, и причина ясна, если вы понимаете двоичный код и то, как работают регистры)
  5. скопировать содержимое a в edx
  6. скопировать содержимое bb в ebx
  7. скопировать содержимое K8 в esi

Отсюда вам нужно будет ознакомиться с бинарной и базовой архитектурой процессора и операндами языка ассемблера, которые имеют отношение к вашей проблеме, в зависимости от ваших знаний. Как только вы сможете прочитать каждую строку, вы сможете расшифровать блоки и, наконец, программу.

Спасибо за добрый ответ. Тогда как насчет «vxorps ymm3, ymm3, ymm3;»? Что я должен узнать больше об этом?

Seyon 22.07.2019 05:21

Несмотря на то, что я не сталкивался с инструкцией vxorps до того, как быстрый поиск в Google был всем, что мне нужно, чтобы найти определение. Я также новичок в SO, но я уверен, что идея состоит не в том, чтобы просто ответить вам на все, а в том, чтобы подтолкнуть вас в правильном направлении. Поправьте меня если я ошибаюсь.

Ace 22.07.2019 05:37

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 кажется немного избыточной.

Спасибо за добрый ответ. Не могли бы вы порекомендовать материалы для изучения внутренностей?

Seyon 22.07.2019 08:43

@Сейон Не совсем. Это тот случай, когда ресурсы быстро устаревают. Я просто использую руководство по внутренним функциям Intel: software.intel.com/sites/landingpage/IntrinsicsGuide/… и godbolt (с флагами -mavx2 -mfma -O2 и -ffast-maths).

robthebloke 22.07.2019 10:23

@robthebloke и будущие читатели: предпочитайте -march=haswell или -march=znver1-mavx2 -mfma. Особенно с gcc, установка -mtune= для процессора Intel с AVX2 + FMA исключает разделение loadu / storeu на несколько ассемблерных инструкций. Почему gcc не разрешает _mm256_loadu_pd как одиночный vmovupd? Если ваши данные действительно выравниваются во время выполнения, это, возможно, большой недостаток.

Peter Cordes 22.07.2019 18:43

Одним из преимуществ использования встроенных функций вместо asm является то, что компилятор может вывести трансляцию из цикла. (По крайней мере, если вы используете float *restrict pC или pA, чтобы компилятор знал, что сохранение через pC не повлияет на значения, считанные из pA.) И да, когда я писал свой ответ, я подумал, что что-то странное в этом матмул асм, но я не мог т положить мой палец на это. Избыточные нагрузки объясняют, почему при быстром беглом просмотре не было видно, идет ли цикл вниз по столбцу или по строке. Хорошо подмечено.

Peter Cordes 23.07.2019 05:49

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