프로그램의 성능에 미치는 벡터화의 영향을 조사하고 있습니다. 이와 관련하여 다음 코드를 작성했습니다.루프를 벡터화하면 성능이 향상되지 않는 이유
#include <stdio.h>
#include <sys/time.h>
#include <stdlib.h>
#define LEN 10000000
int main(){
struct timeval stTime, endTime;
double* a = (double*)malloc(LEN*sizeof(*a));
double* b = (double*)malloc(LEN*sizeof(*b));
double* c = (double*)malloc(LEN*sizeof(*c));
int k;
for(k = 0; k < LEN; k++){
a[k] = rand();
b[k] = rand();
}
gettimeofday(&stTime, NULL);
for(k = 0; k < LEN; k++)
c[k] = a[k] * b[k];
gettimeofday(&endTime, NULL);
FILE* fh = fopen("dump", "w");
for(k = 0; k < LEN; k++)
fprintf(fh, "c[%d] = %f\t", k, c[k]);
fclose(fh);
double timeE = (double)(endTime.tv_usec + endTime.tv_sec*1000000 - stTime.tv_usec - stTime.tv_sec*1000000);
printf("Time elapsed: %f\n", timeE);
return 0;
}
이 코드에서는 두 벡터를 초기화하고 곱하기 만합니다. 결과는 벡터 c
에 저장됩니다. 두 번째 명령이 성공적를 벡터화 이후 성능 향상을 기대할
1) icc -O2 TestSMID.c -o TestSMID -no-vec -no-simd
2) icc -O2 TestSMID.c -o TestSMID -vec-report2
:
for(k = 0; k < LEN; k++)
c[k] = a[k] * b[k];
내가 두 명령을 사용하여 다음 코드를 컴파일 : 내가 주로 관심은 루프 다음 벡터화의 효과 고리. 그러나 필자의 연구에 따르면 루프가 벡터화되면 성능이 향상되지 않습니다.
주제에 익숙하지 않아서 뭔가를 놓친 것 같습니다. 따라서, 제 코드에 문제가 있다면 알려주십시오.
미리 도움을 주셔서 감사합니다.
추 신 : 저는 Mac OSX를 사용하고 있습니다. 따라서 할당 된 모든 메모리가 16 바이트 정렬이므로 데이터를 정렬 할 필요가 없습니다.
편집 : 여러분의 의견과 답변에 모두 감사드립니다. @Mysticial이 제안한 대답에 대해 생각해 보았습니다. 여기에 언급해야 할 몇 가지 추가 사항이 있습니다. 먼저 @Vinska가 언급했듯이 c[k]=a[k]*b[k]
은 한 사이클 만 거치지 않습니다. 루프 인덱스 증분과 k
이 LEN
보다 작도록 비교하는 작업 외에도 작업을 수행하기 위해 수행해야 할 다른 작업이 있습니다. 컴파일러에 의해 생성 된 어셈블리 코드를 살펴보면, 간단한 곱셈은 훨씬 더 많은 사이클을 필요로한다는 것을 알 수 있습니다.
L_B1.9: # Preds L_B1.8
movq %r13, %rax #25.5
andq $15, %rax #25.5
testl %eax, %eax #25.5
je L_B1.12 # Prob 50% #25.5
# LOE rbx r12 r13 r14 r15 eax
L_B1.10: # Preds L_B1.9
testb $7, %al #25.5
jne L_B1.32 # Prob 10% #25.5
# LOE rbx r12 r13 r14 r15
L_B1.11: # Preds L_B1.10
movsd (%r14), %xmm0 #26.16
movl $1, %eax #25.5
mulsd (%r15), %xmm0 #26.23
movsd %xmm0, (%r13) #26.9
# LOE rbx r12 r13 r14 r15 eax
L_B1.12: # Preds L_B1.11 L_B1.9
movl %eax, %edx #25.5
movl %eax, %eax #26.23
negl %edx #25.5
andl $1, %edx #25.5
negl %edx #25.5
addl $10000000, %edx #25.5
lea (%r15,%rax,8), %rcx #26.23
testq $15, %rcx #25.5
je L_B1.16 # Prob 60% #25.5
# LOE rdx rbx r12 r13 r14 r15 eax
L_B1.13: # Preds L_B1.12
movl %eax, %eax #25.5
# LOE rax rdx rbx r12 r13 r14 r15
L_B1.14: # Preds L_B1.14 L_B1.13
movups (%r15,%rax,8), %xmm0 #26.23
movsd (%r14,%rax,8), %xmm1 #26.16
movhpd 8(%r14,%rax,8), %xmm1 #26.16
mulpd %xmm0, %xmm1 #26.23
movntpd %xmm1, (%r13,%rax,8) #26.9
addq $2, %rax #25.5
cmpq %rdx, %rax #25.5
jb L_B1.14 # Prob 99% #25.5
jmp L_B1.20 # Prob 100% #25.5
# LOE rax rdx rbx r12 r13 r14 r15
L_B1.16: # Preds L_B1.12
movl %eax, %eax #25.5
# LOE rax rdx rbx r12 r13 r14 r15
L_B1.17: # Preds L_B1.17 L_B1.16
movsd (%r14,%rax,8), %xmm0 #26.16
movhpd 8(%r14,%rax,8), %xmm0 #26.16
mulpd (%r15,%rax,8), %xmm0 #26.23
movntpd %xmm0, (%r13,%rax,8) #26.9
addq $2, %rax #25.5
cmpq %rdx, %rax #25.5
jb L_B1.17 # Prob 99% #25.5
# LOE rax rdx rbx r12 r13 r14 r15
L_B1.18: # Preds L_B1.17
mfence #25.5
# LOE rdx rbx r12 r13 r14 r15
L_B1.19: # Preds L_B1.18
mfence #25.5
# LOE rdx rbx r12 r13 r14 r15
L_B1.20: # Preds L_B1.14 L_B1.19 L_B1.32
cmpq $10000000, %rdx #25.5
jae L_B1.24 # Prob 0% #25.5
# LOE rdx rbx r12 r13 r14 r15
L_B1.22: # Preds L_B1.20 L_B1.22
movsd (%r14,%rdx,8), %xmm0 #26.16
mulsd (%r15,%rdx,8), %xmm0 #26.23
movsd %xmm0, (%r13,%rdx,8) #26.9
incq %rdx #25.5
cmpq $10000000, %rdx #25.5
jb L_B1.22 # Prob 99% #25.5
# LOE rdx rbx r12 r13 r14 r15
L_B1.24: # Preds L_B1.22 L_B1.20
그리고 비 vectoized 버전은 다음과 같습니다 : 같은 벡터화 버전 보이는이 옆에
L_B1.9: # Preds L_B1.8
xorl %eax, %eax #25.5
# LOE rbx r12 r13 r14 r15 eax
L_B1.10: # Preds L_B1.10 L_B1.9
lea (%rax,%rax), %edx #26.9
incl %eax #25.5
cmpl $5000000, %eax #25.5
movsd (%r15,%rdx,8), %xmm0 #26.16
movsd 8(%r15,%rdx,8), %xmm1 #26.16
mulsd (%r13,%rdx,8), %xmm0 #26.23
mulsd 8(%r13,%rdx,8), %xmm1 #26.23
movsd %xmm0, (%rbx,%rdx,8) #26.9
movsd %xmm1, 8(%rbx,%rdx,8) #26.9
jb L_B1.10 # Prob 99% #25.5
# LOE rbx r12 r13 r14 r15 eax
, 프로세서는 24 바이트를로드하지 않습니다. 메모리에 액세스 할 때마다 전체 행 (64 바이트)이로드됩니다. 더 중요한 것은 a
, b
및 c
에 필요한 메모리가 연속적이므로, 프리 페처는 확실히 많은 도움을 주며 다음 블록을 미리로드합니다. 그런데, 나는 @Mysticial에 의해 계산 된 메모리 대역폭이 너무 비관적이라고 생각합니다.
또한 SIMD를 사용하여 매우 간단한 추가 프로그램의 성능을 향상시키는 방법은 Intel Vectorization Guide에 나와 있습니다. 따라서 우리는이 간단한 루프에 대한 성능 향상을 얻을 수 있어야합니다.
Edit2 : 의견을 보내 주셔서 다시 한 번 감사드립니다. 또한, @ 미스테리 샘플 코드에 감사드립니다, 나는 마침내 성능 향상에 SIMD의 효과를 보았다. Mysticial이 지적한 바와 같이이 문제는 메모리 대역폭이었다. a
, L1 캐시에 맞는 b
및 c
의 작은 크기를 선택하면 SIMD가 성능을 크게 향상시키는 데 도움이 될 수 있음을 알 수 있습니다.
icc -O2 -o TestSMIDNoVec -no-vec TestSMID2.c: 17.34 sec
icc -O2 -o TestSMIDVecNoUnroll -vec-report2 TestSMID2.c: 9.33 sec
그리고 루프를 줄이기 것은 더욱 성능을 향상 : 또한
icc -O2 -o TestSMIDVecUnroll -vec-report2 TestSMID2.c -unroll=8: 8.6sec
, 나는 그것이 반복을 완료하는 나의 프로세서에 대해 하나의 사이클을 소요 언급해야 여기에 내가 가진 결과는 -O2
으로 컴파일 할 때.
PS :이 원래의 대답은 다시 2013 년 2017 하드웨어로 유효 내 컴퓨터가 맥북 프로 코어 I5의 @의 2.5GHz의에게있다 (듀얼 코어)
방금 내 프로세서가주기 당 1 반복을 수행 할 수 있음을 입증하고 가능한 방법에 대한 설명을 업데이트했습니다. – Mysticial
나는이 일을 정말로 싫어하지만, 빌드 명령은 실행 파일의 두 버전을 같은 파일에 저장합니다. 두 버전의 이름이 다른 경우 훨씬 더 명확했을 것입니다. – wallyk
"정렬 할 필요가 없습니다"라고 말하면서 생성 된 asm 코드는 모든 정렬 가능성을 검사합니다. 정렬되지 않은 srces를위한 루프와 메모리 피연산자가있는'mulpd '를 사용하는 루프가 있습니다. 그러나 정렬 된 버전조차도 128b를로드하기 위해 이상한'movsd' +'movhpd' 시퀀스를 사용합니다. 제 생각에 그것은'c'와'a'는 정렬되어 있고,'b'는 정렬되지 않았습니다 (스칼라 소개 뒤에). 나는 오래된 아키텍처에서 2 insn 시퀀스가 때때로 'movupd'보다 빠르다는 것을 기억한다. 루프의 유일하게 정렬 된 버전은 하나의 소스에 대해서는'movupd'를 사용하고, 다른 하나는/boggle을위한 2 insn 메소드를 사용합니다. –