Что я здесь не так делаю? Я получаю 4 нуля вместо:
2
4
6
8
Я также хотел бы изменить мою функцию .asm, чтобы она проходила через более длинные векторы, потому что здесь приведен пример. Я просто использовал вектор с четырьмя элементами, чтобы я мог суммировать этот вектор без цикла с SIMD 256-битными регистрами.
#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
Также я хотел бы получить некоторые другие разъяснения:
Что если я сделаю что-то вроде следующего, не помещая цикл в мою функцию сборки ?:
#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
Причина, по которой ваш код дает неправильный результат, заключается в том, что у вас есть синтаксис в вашей сборке задом наперед.
Вы используете синтаксис 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. выше
Процессоры 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 за такт. Таким образом, лучшее, что он может сделать, это четыре раза за такт.
Но позвольте мне рассказать вам маленький секрет, что Intel не хочет, чтобы люди много говорили. Большинство операций ограничено пропускной способностью памяти и не может извлечь большую пользу из распараллеливания. Это включает в себя операцию в вашем вопросе. Таким образом, хотя Intel продолжает выпускать новые технологии каждые несколько лет (например, AVX, FMA, AVX512, удваивая количество ядер), что удваивает производительность каждый раз, чтобы утверждать, что на практике достигается закон Мура, среднее преимущество является линейным, а не экспоненциальным и это было в течение нескольких лет.