avx2, simd 쪼렙임.
gcc를 그냥 빌드 하면 simd로 빌드된다. 따라서 128비트짜리 xmm 레지스터를 사용한다. gcc에 -march=haswell을 넣고 빌드하면 avx2로 빌드된다. 256비트짜리 ymm 레지스터를 사용할 수 있다.
... 하 글쓰다 날렸다 ... 소스 생략하고 요약하면 이렇다.
.
movq $a, -40(%rbp)
movq $b, -32(%rbp)
movq $c, -24(%rbp)
movq -40(%rbp), %rax
vmovdqa (%rax), %ymm1
movq -32(%rbp), %rax
vmovdqa (%rax), %ymm0
vpaddd %ymm0, %ymm1, %ymm0
movq -24(%rbp), %rax
vmovdqa %ymm0, (%rax)
addq $32, -40(%rbp)
addq $32, -32(%rbp)
addq $32, -24(%rbp)
movq -40(%rbp), %rax
vmovdqa (%rax), %ymm1
movq -32(%rbp), %rax
vmovdqa (%rax), %ymm0
vpaddd %ymm0, %ymm1, %ymm0
movq -24(%rbp), %rax
vmovdqa %ymm0, (%rax)
. . .
원본 c는 이렇다.
#include <stdio.h>
typedef int v4si __attribute__ ((vector_size (32)));
int a[] = {1,2,3,4,5,6,7,8, 2,3,4,4,5,6,7,8};
int b[] = {2,3,4,4,5,6,7,8, 2,3,4,4,5,6,7,8};
int c[16];
int main(int argc, char *argv[])
{
v4si *ap, *bp, *cp;
ap = a;
bp = b;
cp = c;
*cp = *ap + *bp;
ap++, bp++, cp++;
*cp = *ap + *bp;
printf("%d\n", c[0]);
}
...
얼라인을 맞추지 않고 그냥 날 계산을 때리면 메모리 복사를 한다고 낑낑댄다..
movq a(%rip), %rax
movq %rax, -304(%rbp)
movq a+8(%rip), %rax
movq %rax, -296(%rbp)
movq a+16(%rip), %rax
movq %rax, -288(%rbp)
movq a+24(%rip), %rax
movq %rax, -280(%rbp)
movq a+32(%rip), %rax
movq %rax, -272(%rbp)
movq a+40(%rip), %rax
movq %rax, -264(%rbp)
movq a+48(%rip), %rax
movq %rax, -256(%rbp)
movq a+56(%rip), %rax
movq %rax, -248(%rbp)
movq b(%rip), %rax
movq %rax, -240(%rbp)
movq b+8(%rip), %rax
movq %rax, -232(%rbp)
movq b+16(%rip), %rax
movq %rax, -224(%rbp)
movq b+24(%rip), %rax
movq %rax, -216(%rbp)
movq b+32(%rip), %rax
movq %rax, -208(%rbp)
movq b+40(%rip), %rax
movq %rax, -200(%rbp)
movq b+48(%rip), %rax
movq %rax, -192(%rbp)
movq b+56(%rip), %rax
movq %rax, -184(%rbp)
vmovdqa -304(%rbp), %ymm1
vmovdqa -240(%rbp), %ymm0
vpaddd %ymm0, %ymm1, %ymm1
vmovdqa -272(%rbp), %ymm2
vmovdqa -208(%rbp), %ymm0
vpaddd %ymm0, %ymm2, %ymm0
vmovdqa %ymm1, -464(%rbp)
vmovdqa %ymm0, -432(%rbp)
movq -464(%rbp), %rax
movq %rax, -176(%rbp)
movq -456(%rbp), %rax
movq %rax, -168(%rbp)
movq -448(%rbp), %rax
movq %rax, -160(%rbp)
movq -440(%rbp), %rax
movq %rax, -152(%rbp)
movq -432(%rbp), %rax
movq %rax, -144(%rbp)
movq -424(%rbp), %rax
movq %rax, -136(%rbp)
movq -416(%rbp), %rax
movq %rax, -128(%rbp)
movq -408(%rbp), %rax
movq %rax, -120(%rbp)
movq -176(%rbp), %rax
movq %rax, -368(%rbp)
movq -168(%rbp), %rax
movq %rax, -360(%rbp)
movq -160(%rbp), %rax
movq %rax, -352(%rbp)
movq -152(%rbp), %rax
movq %rax, -344(%rbp)
movq -144(%rbp), %rax
movq %rax, -336(%rbp)
movq -136(%rbp), %rax
movq %rax, -328(%rbp)
movq -128(%rbp), %rax
movq %rax, -320(%rbp)
movq -120(%rbp), %rax
movq %rax, -312(%rbp)
...
#include <stdio.h>
typedef int v4si __attribute__ ((vector_size (64)));
v4si a = {1,2,3,4,5,6,7,8, 2,3,4,4,5,6,7,8};
v4si b = {2,3,4,4,5,6,7,8, 2,3,4,4,5,6,7,8};
int main(int argc, char *argv[])
{
printf("hello world\n");
v4si c = a + b;
printf("%d\n", c[0]);
}
O2를 붙여보니 내용은 좀 달라지지만 맥락은 같다. 결과적으로 보면, 그냥 C 프로그래밍 할 때와 마찬가지다. 포인터로 다루면 메모리 복사를 회피할 수 있다. 끗.
+추가 : 아이러니컬 하게도 캐시라인은 보통 64바이트다. 64바이트 이하의 전역 데이터를 다룰때 자칫하면 거짓공유에 빠질 수 있다. 전역 데이터의 멀티 코어 공유가 있다면 64바이트 패딩을 치고 avx2를 쓰는 쪽이 아무것도 하지 않은 것에 비해 수십 배는 빠를 것 같다.
+추가 : 마찬가지로 avx2를 이용하여 memcpy를 구현하면 64바이트 단위로 복사를 구현할 수 있다. 물론 현재의 memcpy는 최적화가 다방면으로 잘 되어 있다고 들었다. 일반적인 용도로 memcpy를 재구현할 필요는 없으나, 대규모 벡터 연산이 필요한 경우 avx2를 응용한 포인터 연산을 고려해 볼 수 있다.