Добавление двух векторов в сборку x86 64 с AVX2 плюс технические уточнения


Что я здесь делаю не так? Я получаю 4 нуля вместо:

2
4
6
8

Я также хотел бы изменить свой .функция asm для выполнения более длинных векторов, потому что для семплификации здесь я только что использовал вектор с четырьмя элементами, так что я могу суммировать этот вектор без цикла с 256-битными регистрами SIMD.

.cpp

#include <iostream>
#include <chrono>

extern "C" double *addVec(double *C, double *A, double *B, size_t &N);

int main()
{
    size_t N = 1 << 2;
    size_t reductions = N / 4;

    double *A = (double*)_aligned_malloc(N*sizeof(double), 32);
    double *B = (double*)_aligned_malloc(N*sizeof(double), 32);
    double *C = (double*)_aligned_malloc(N*sizeof(double), 32);

    for (size_t i = 0; i < N; i++)
    {
        A[i] = double(i + 1);
        B[i] = double(i + 1);
    }

    auto start = std::chrono::high_resolution_clock::now();

        double *out = addVec(C, A, B, reductions);

    auto finish = std::chrono::high_resolution_clock::now();

    for (size_t i = 0; i < N; i++)
    {
        std::cout << out[i] << std::endl;
    }

    std::cout << "nn";

    std::cout << std::chrono::duration_cast<std::chrono::nanoseconds>(finish - start).count() << " nsn";

    std::cin.get();

    _aligned_free(A);
    _aligned_free(B);
    _aligned_free(C);

    return 0;
}

.asm

.data
; C -> RCX
; A -> RDX
; B -> r8
; N -> r9
.code
    addVec proc
        ;xor rbx, rbx
        align 16
        ;aIn:
            vmovapd ymm0, ymmword ptr [rdx]
            ;vmovapd ymm1, ymmword ptr [rdx + rbx + 4]
            vmovapd ymm2, ymmword ptr [r8]
            ;vmovapd ymm3, ymmword ptr [r8 + rbx + 4]

            vaddpd ymm0, ymm2, ymm3

            vmovapd ymmword ptr [rcx], ymm3
        ;inc rbx
        ;cmp rbx, qword ptr [r9]
        ;jl aIn
        mov rax, rcx    ; return the address of the output vector
    ret
    addVec endp
end

Также я хотел бы получить некоторые другие разъяснения:

  1. существуют ли восемь 256-битных регистров (ymm0-ymm7) для каждое ядро моего процессора или их всего восемь?
  2. все остальные регистры, такие как rax, rbx и т. д... являются ли они суммарными или для каждого ядра?
  3. поскольку я могу обрабатывать 4 двойника за цикл только с сопроцессором SIMD и одним ядром, могу ли я выполнить другую команду за цикл с остальной частью моего процессора? Так, например, могу ли я добавить 5 двойников за цикл с одним ядром? (4 с SIMD + 1)
  4. Что, если я сделаю что-то вроде следующего, не помещая петлю в свою сборку функция?:

    #pragma openmp parallel for

    for (size_t i = 0; i < reductions; i++)

    addVec(C + i, A + i, B + i)

    Это будет форк coreNumber + hyperThreading потоков и каждый из них выполнять SIMD добавить на четыре двойных? Итак, в общей сложности 4 * coreNumber удваивается для каждого цикла? Я же не могу добавить сюда гиперпространство, верно?


Обновление могу ли я это сделать?:

.data
;// C -> RCX
;// A -> RDX
;// B -> r8
.code
    addVec proc
        ; One cycle 8 micro-op
            vmovapd ymm0, ymmword ptr [rdx]     ; 1 port
            vmovapd ymm1, ymmword ptr [rdx + 32]; 1 port
            vmovapd ymm2, ymmword ptr [r8]      ; 1 port
            vmovapd ymm3, ymmword ptr [r8 + 32] ; 1 port
            vfmadd231pd ymm0, ymm2, ymm4        ; 1 port
            vfmadd231pd ymm1, ymm3, ymm4        ; 1 port
            vmovapd ymmword ptr [rcx], ymm0     ; 1 port
            vmovapd ymmword ptr [rcx + 32], ymm1; 1 port

        ; Return the address of the output vector
        mov rax, rcx                            ; 1 port ?
    ret
    addVec endp
end

Или только потому, что я превысил бы шесть портов, которые вы мне сказали?

.data
;// C -> RCX
;// A -> RDX
;// B -> r8
.code
    addVec proc
        ;align 16
        ; One cycle 5 micro-op ?
        vmovapd ymm0, ymmword ptr [rdx]     ; 1 port
        vmovapd ymm1, ymmword ptr [r8]      ; 1 port
        vfmadd231pd ymm0, ymm1, ymm2        ; 1 port
        vmovapd ymmword ptr [rcx], ymm0     ; 1 port

        ; Return the address of the output vector
        mov rax, rcx                        ; 1 port ?
    ret
    addVec endp
end
1 5

1 ответ:

Причина, по которой ваш код получает неверный результат, заключается в том, что синтаксис в вашей сборке задом наперед.

Вы используете синтаксис Intel, в котором пункт назначения должен стоять перед источником. Так и в вашем оригинале .код asm вы должны изменить

vaddpd ymm0, ymm2, ymm3

To

 vaddpd ymm3, ymm2, ymm0
Один из способов увидеть это-использовать внутренние компоненты, а затем посмотреть на разборку.
extern "C" double *addVec(double * __restrict C, double * __restrict A, double * __restrict B, size_t &N) {
    __m256d x = _mm256_load_pd((const double*)A);
    __m256d y = _mm256_load_pd((const double*)B);
    __m256d z = _mm256_add_pd(x,y);
    _mm256_store_pd((double*)C, z);
    return C;
}

Лицемерно из GCC на Linux используя g++ -S -O3 -mavx -masm=intel -mabi=ms foo.cpp дает:

vmovapd ymm0, YMMWORD PTR [rdx]
mov     rax, rcx
vaddpd  ymm0, ymm0, YMMWORD PTR [r8]
vmovapd YMMWORD PTR [rcx], ymm0
vzeroupper
ret

Инструкция vaddpd ymm0, ymm0, YMMWORD PTR [rdx] взрывает нагрузку и дополнение в одном плавленого микро-ФП. Когда я использую эту функцию в ваш код, он получает 2,4,6,8.

Можно найти исходный код, который суммирует два массива x и y и записывает в массив z вl1-memory-bandwidth-50-drop-in-efficiency-using-addresses-which-differ-by-4096 . Это использует внутренние компоненты и развертывается восемь раз. Соберите код с помощью gcc -S или objdump -d. Другой источник, который делает почти то же самое и написан в сборке, находится на получение-пик-пропускной способности-на-haswell-in-the-l1-cache-only-getting-62 . В файле triad_fma_asm.asm измените строку pi: dd 3.14159 на pi: dd 1.0. В обоих этих примерах используется одна плавающая точка, поэтому, если вы хотите удвоить, вам придется внести необходимые изменения.

Ответы на другие ваши вопросы:

    Каждое ядро вашего процессора-это физически различное устройство со своим набором регистров. Каждое ядро имеет 16 регистров общего назначения (например, rax, rbx, r8, r9,...) и несколько регистры специального назначения (например, RFLAGS). В 32-битном режиме каждое ядро имеет восемь 256-битных регистров, а в 64-битном режиме-шестнадцать 256-битных регистров. Когда AVX-512 будет доступен, будет тридцать два 512-битных регистра (но только восемь в 32-битном режиме).
Обратите внимание, что каждое ядро имеет гораздо больше регистров, чем логические, которые вы можете запрограммировать непосредственно.
  1. См. 1. выше

  2. Процессоры Core2 с 2006 года через Haswell все могут обрабатывать максимум четыре мкоп за сутки. Тем не менее, используя два метода, называемые микро-op fusion и macro-op fusion, можно достичь шести микроопераций за такт с Haswell.

Micro-op fusion может сплавлять, например, нагрузку и добавление в один так называемый сплавленный микрооп, но каждый микрооп все еще нуждается в своем собственном порту. Макро-ОП фьюжн может сплавить, например, скалярное сложение и прыжок в один микро-ОП, которому нужен только один порт. Макрооптическое слияние-это, по сути, два на одного.

Хасвелл имеет восемь портов. Вы можете получить шесть микроопераций за один такт, используя семь таких портов.
256-load + 256-FMA    //one fused µop using two ports
256-load + 256-FMA    //one fused µop using two ports
256-store             //one µop using two ports
64-bit add + jump     //one µop using one port
Таким образом, фактически каждое ядро Haswell может обрабатывать шестнадцать двойников (четыре умножения и четыре сложения для каждого FMA), две 256-нагрузки, одно 256-битное хранилище и одно 64-битное сложение и ветвление за один такт. В этом вопросеполучение-пиковой-полосы пропускания-на-haswell-in-the-l1-cache-only-getting-62 , я получил (теоретически) пять микроопераций за один такт с использованием шести портов. Однако на практике на Хасвел это трудно достичь.

Для вашей конкретной операции, которая считывает два массива и записывает один, он связан двумя считываниями за такт, поэтому он может выдавать только один FMA за такт. Поэтому лучшее, что он может сделать, - это четыре двойника за такт.

    Если правильно распараллелить код и процессор будет иметь четыре физических ядра, то за один такт можно будет выполнить 64 операции с двойной плавающей запятой (2FMA*4cores). Это было бы теоретически лучшим для какая-то операция, но не для операции в вашем вопросе.
Но позвольте мне рассказать вам маленький секрет, о котором Intel не хочет, чтобы люди много говорили. большинство операций связаны с пропускной способностью памяти и не могут извлечь большой пользы из распараллеливания. Это включает в себя операцию в вашем вопросе. Поэтому, хотя Intel продолжает выпускать новые технологии каждые несколько лет (например, AVX, FMA, AVX512, удваивая количество ядер), которые удваивают производительность каждый раз, чтобы претендовать то, что закон Мура получается на практике, означает, что средняя выгода линейна, а не экспоненциальна, и так было уже несколько лет.