avx2, simd와 align

2017/02/09

avx2, simd 쪼렙임.

gcc를 그냥 빌드 하면 simd로 빌드된다. 따라서 128비트짜리 xmm 레지스터를 사용한다. gcc에 -march=haswell을 넣고 빌드하면 avx2로 빌드된다. 256비트짜리 ymm 레지스터를 사용할 수 있다.

... 하 글쓰다 날렸다 ... 소스 생략하고 요약하면 이렇다.

  1. simd로 빌드하여 xmm 레지스터를 이용하면 128비트 초과시 3쿠션을 태운다.
  2. avx2로 빌드하여 ymm 레지스터를 이용하면 한 큐에 퉁친다.
  3. 명령어 갯수만 줄어드는 것이 아니라 메모리 적재까지 줄어든다.
  4. ymm레지스터를 사용할 때에도 256비트를 초과하면 메모리 적재를 한 큐에 하지 못한다. (gcc 5.4.0 우분투 빌드, 최적화 없을 때 확인해 봄.)
  5. 따라서 align에 맞춰서 포인터 연산으로 전환하면 다음과 같이 최적화 된다.

.

	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를 응용한 포인터 연산을 고려해 볼 수 있다.