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

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

2
4
6
8

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

.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 << "\n\n";

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

std::cin.get();

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

return 0;
}

.как м

.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 double для каждого цикла? Я не могу добавить гиперпоточность здесь, верно?


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

.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

5

Решение

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

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

vaddpd ymm0, ymm2, ymm3

в

 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-память полоса пропускания 50-падение-в-эффективности, использующая-адрес, которые-отличаются-по-4096-. Он использует встроенные функции и разворачивается восемь раз. Dissasemble код с gcc -S или же objdump -d, Другой источник, который делает почти то же самое и написан на ассемблере, находится на получение пиковой пропускной способности на Haswell-в-l1-кэш только пробивной-62. В файле triad_fma_asm.asm изменить линию pi: dd 3.14159 в pi: dd 1.0, В обоих этих примерах используется одиночная с плавающей запятой, поэтому если вы хотите удвоить, вам придется внести необходимые изменения.

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

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

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

  1. см. 1. выше

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

Micro-Op Fusion может перегореть, например, нагрузка и дополнение в одну так называемую микрооперацию, но каждая микрооперация все еще нуждается в собственном порте. Macro-op Fusion может объединить, например, скалярное дополнение и переход в одну микрооперацию, для которой нужен только один порт. Macro-op fusion — это, по сути, два к одному.

У Haswell есть восемь портов. Вы можете получить шесть микроопераций за один такт, используя семь таких портов.

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-в-l1-кэш только пробивной-62, Я получил (теоретически) пять микроопераций за один такт, используя шесть портов. Однако на практике на Haswell этого трудно достичь.

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

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

Но позвольте мне рассказать вам маленький секрет, что Intel не хочет, чтобы люди много говорили. Большинство операций ограничено пропускной способностью памяти и не может извлечь большую пользу из распараллеливания. Это включает в себя операцию в вашем вопросе. Таким образом, хотя Intel продолжает выпускать новые технологии каждые несколько лет (например, AVX, FMA, AVX512, удваивая количество ядер), что удваивает производительность каждый раз, чтобы утверждать, что на практике достигается закон Мура, среднее преимущество является линейным, а не экспоненциальным и это было в течение нескольких лет.

8

Другие решения


По вопросам рекламы [email protected]