Добавление двух векторов в сборку 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
Также я хотел бы получить некоторые другие разъяснения:
- существуют ли восемь 256-битных регистров (ymm0-ymm7) для каждое ядро моего процессора или их всего восемь?
- все остальные регистры, такие как rax, rbx и т. д... являются ли они суммарными или для каждого ядра?
- поскольку я могу обрабатывать 4 двойника за цикл только с сопроцессором SIMD и одним ядром, могу ли я выполнить другую команду за цикл с остальной частью моего процессора? Так, например, могу ли я добавить 5 двойников за цикл с одним ядром? (4 с SIMD + 1)
-
Что, если я сделаю что-то вроде следующего, не помещая петлю в свою сборку функция?:
#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 ответ:
Причина, по которой ваш код получает неверный результат, заключается в том, что синтаксис в вашей сборке задом наперед.
Вы используете синтаксис 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. выше
Процессоры Core2 с 2006 года через Haswell все могут обрабатывать максимум четыре мкоп за сутки. Тем не менее, используя два метода, называемые микро-op fusion и macro-op fusion, можно достичь шести микроопераций за такт с Haswell.
Micro-op fusion может сплавлять, например, нагрузку и добавление в один так называемый сплавленный микрооп, но каждый микрооп все еще нуждается в своем собственном порту. Макро-ОП фьюжн может сплавить, например, скалярное сложение и прыжок в один микро-ОП, которому нужен только один порт. Макрооптическое слияние-это, по сути, два на одного.
Хасвелл имеет восемь портов. Вы можете получить шесть микроопераций за один такт, используя семь таких портов.Таким образом, фактически каждое ядро Haswell может обрабатывать шестнадцать двойников (четыре умножения и четыре сложения для каждого FMA), две 256-нагрузки, одно 256-битное хранилище и одно 64-битное сложение и ветвление за один такт. В этом вопросеполучение-пиковой-полосы пропускания-на-haswell-in-the-l1-cache-only-getting-62 , я получил (теоретически) пять микроопераций за один такт с использованием шести портов. Однако на практике на Хасвел это трудно достичь.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
Для вашей конкретной операции, которая считывает два массива и записывает один, он связан двумя считываниями за такт, поэтому он может выдавать только один FMA за такт. Поэтому лучшее, что он может сделать, - это четыре двойника за такт.
Если правильно распараллелить код и процессор будет иметь четыре физических ядра, то за один такт можно будет выполнить 64 операции с двойной плавающей запятой (2FMA*4cores). Это было бы теоретически лучшим для какая-то операция, но не для операции в вашем вопросе.
Но позвольте мне рассказать вам маленький секрет, о котором Intel не хочет, чтобы люди много говорили. большинство операций связаны с пропускной способностью памяти и не могут извлечь большой пользы из распараллеливания. Это включает в себя операцию в вашем вопросе. Поэтому, хотя Intel продолжает выпускать новые технологии каждые несколько лет (например, AVX, FMA, AVX512, удваивая количество ядер), которые удваивают производительность каждый раз, чтобы претендовать то, что закон Мура получается на практике, означает, что средняя выгода линейна, а не экспоненциальна, и так было уже несколько лет.