HyperAI초신경
Back to Headlines

SIMD 함수의 복잡한 현실과 성능 문제

2일 전

SIMD 함수의 복잡한 현실 우리는 이 블로그에서 SIMD(Single Instruction Multiple Data, 단일 명령 다중 데이터)와 벡터화에 대해 많이 논의해 왔습니다. 이번 포스트에서는 SIMD 함수가 무엇인지, 언제 유용한지, 그리고 어떻게 효과적으로 선언하고 사용할 수 있는지를 알아봅니다. SIMD 함수란 하나의 함수 호출로 여러 개의 데이터를 처리하는 함수입니다. 예를 들어 일반적인 사인 함수는 다음과 같습니다: cpp double sin(double angle); 이 함수는 하나의 double 값을 받아서 하나의 double 값을 반환합니다. 벡터 버전의 사인 함수는 한 번의 함수 호출로 네 개의 값을 처리합니다: cpp double[4] sin(double angle[4]); 혹은 AVX SIMD 타입을 사용한다면 다음과 같이 표현될 수 있습니다: cpp __m256d sin(__m256d angle); 벡터 함수의 기본 아이디어는 여러 데이터 요소를 한 번의 함수 호출로 처리하여 성능을 향상시키는 것입니다. 이는 수동 벡터화 함수나 컴파일러가 자동으로 생성하는 벡터화 함수를 통해 이루어집니다. 벡터 함수의 필요성 벡터 함수라는 용어는 두 가지 관련 개념을 지칭할 수 있습니다: 1. 수동으로 벡터화된 함수 2. 컴파일러가 자동으로 루프를 벡터화하기 위해 선택할 수 있는 함수 이 포스트에서는 두 번째 유형의 벡터 함수, 즉 컴파일러가 자동으로 루프를 벡터화하기 위해 선택할 수 있는 함수에 대해 이야기하겠습니다. 예를 들어, 다음과 같은 루프가 있을 때: cpp for (size_t i = 0; i < n; i++) { res[i] = sin(in[i]); } 컴파일러는 스칼라 버전의 sin 함수를 호출하거나 벡터 버전의 sin 함수를 호출할 수 있습니다. 대부분의 경우 성능 향상을 위해 벡터 버전을 선택하지만, 일부 루프 반복이 벡터 코드로 처리할 수 없는 경우에는 스칼라 버전을 호출할 수도 있습니다. 벡터 함수의 선언과 정의 벡터 함수를 선언하고 정의하는 방법은 여러 가지가 있습니다. OpenMP 프AGMA와 컴파일러 확장을 사용할 수 있습니다. ```cpp pragma omp declare simd double sin(double v); // GCC용 attribute((simd)) double sin(double v); ``` 내부적으로 대부분의 컴파일러는 OpenMP 프AGMA와 자체 구현을 비슷하게 처리합니다. OpenMP 프AGMA를 사용할 때는 컴파일러 플래그 -fopenmp 또는 -fopenmp-simd를 활성화해야 합니다. 특히 라이브러리를 배포할 때 이러한 플래그를 보장할 수 없을 경우, 다른 컴파일러에 대한 확장을 사용하는 것이 좋습니다. GNU C 라이브러리에서는 다음과 같이 처리합니다: ```cpp if defined x86_64 && defined FAST_MATH if defined _OPENMP && _OPENMP >= 201307 / OpenMP case. / define __DECL_SIMD_x86_64 _Pragma("omp declare simd notinbranch") elif __GNUC_PREREQ(6,0) / W/o OpenMP use GCC 6. attribute((simd)). */ define DECL_SIMD_x86_64 __attribute((simd("notinbranch"))) endif ``` 벡터 함수의 매개변수 유형 다음은 이미지의 특정 열을 합계하는 함수입니다: cpp double sum_column(double const * const img_ptr, size_t column, size_t width, size_t height) { double s = 0.0; for (size_t j = 0; j < height; j++) { s += img_ptr[j * width + column]; } return s; } 이 함수는 평면 이미지 img_ptr의 너비와 높이, 그리고 합계를 구할 열 번호 column을 매개변수로 받습니다. 벡터 버전의 루프를 작성하려면 여러 가지 경우를 고려해야 합니다. 호출자가 어떤 형태의 벡터 함수를 필요로 하는지에 따라 다음과 같이 선언할 수 있습니다: ```cpp // 4개의 연속된 열을 SIMD로 계산, 예: 열 0, 1, 2, 3 pragma omp declare simd uniform(img_ptr, width, height) linear(column) double sum_column(double const * const img_ptr, size_t column, size_t width, size_t height); // 4개의 임의의 열을 SIMD로 계산, 예: 열 2, 11, 13, 4 pragma omp declare simd uniform(img_ptr, width, height) double sum_column(double const * const img_ptr, size_t column, size_t width, size_t height); // 모든 매개변수가 일정한 경우 pragma omp declare simd uniform(column, width, height) double sum_column(double const * const img_ptr, size_t column, size_t width, size_t height); ``` 매개변수를 정확히 지정하여 벡터 함수의 성능을 최적화하는 것이 중요합니다. 가장 유연한 벡터 함수는 성능이 가장 느린 경우가 많기 때문입니다. inbranch와 notinbranch 속성 inbranch와 notinbranch 속성을 사용하면, 함수가 브랜치 내에서 호출되는지 여부를 지정할 수 있습니다. 예를 들어, 다음과 같은 루프가 있을 때: cpp for (size_t i = 0; i < WIDTH; i++) { if (sum_columns0[i] == 0.0) { sum_columns0[i] = sum_column(img_ptr, i, WIDTH, HEIGHT); } } 이 루프는 sum_column 함수를 무조건적으로 호출하지 않고, 조건에 따라 호출합니다. 따라서 벡터 함수는 일부 열에 대해서만 계산해야 합니다. 이를 위해 inbranch와 notinbranch 속성을 사용합니다. ```cpp pragma omp declare simd notinbranch double sin(double v); ``` 벡터 함수의 오버라이딩 컴파일러가 벡터 함수를 효율적으로 생성하지 않으면, 사용자 정의 벡터화 구현을 제공할 수 있습니다. 예를 들어, 다음 벡터 함수를 선언한 경우: ```cpp pragma omp declare simd attribute((const, nothrow)) double square(double x); ``` 컴파일러는 벡터 함수의 존재를 인식하지만, 구체적인 구현을 찾을 수 없습니다. 이 문제를 해결하려면 다른 파일에서 벡터 함수를 정의해야 합니다. ```cpp double square(double x) { return x * x; } extern "C" __m256d _ZGVdN4v__Z6squared(__m256d x) { return _mm256_mul_pd(x, x); } extern "C" __m256d _ZGVdM4v__Z6squared(__m256d x, __m256d mask) { __m256d r = _mm256_mul_pd(x, x); return _mm256_blendv_pd(r, x, mask); } ``` 컴파일러의 벡터 이름 맹글링 벡터 함수를 정의할 때, 컴파일러는 여러 버전의 함수를 생성합니다. 예를 들어, Godbolt에서 벡터 함수의 이름 맹글링을 확인할 수 있습니다. X64와 ARM에서 함수 이름이 다르다는 점에 주의해야 합니다. ```cpp pragma omp declare simd simdlen(8) notinbranch attribute((const, nothrow)) double square(double x); ``` AVX를 사용할 때, 256비트 레지스터에 4개의 double 값을 저장할 수 있습니다. 하지만 8개의 double 값을 한 번의 벡터화 반복에서 처리하도록 요청한 경우, 원래 값 x는 두 개의 벡터 레지스터에 저장됩니다. 해당 정의는 다음과 같습니다: ```cpp struct __m256dx2 { __m256d v0; __m256d v1; }; __m256dx2 square2(__m256d x0, __m256d x1) { __m256dx2 res; res.v0 = _mm256_mul_pd(x0, x0); res.v1 = _mm256_mul_pd(x1, x1); return res; } ``` 함수 인라인 벡터 함수의 선언과 오버라이딩을 동일한 컴파일 단위에서 수행할 수 없습니다. 이는 GCC에서 특히 중요합니다. 함수 인라인은 모든 최적화의 어머니라고 할 수 있으며, 사용자 정의 벡터 함수 구현을 인라인하기 위해서는 링크 시간 최적화(LTO)를 사용해야 합니다. -flto 플래그로 컴파일하고 링크해야 하지만, 이 플래그는 컴파일과 링크 시간을 크게 증가시킬 수 있습니다. 컴파일러의 특징 GCC의 벡터 함수 기능을 실험하면서, 이 기능이 아직 완벽하지 않다는 것을 알게 되었습니다. 예를 들어, 컴파일러가 생성한 벡터 함수는 종종 스칼라 버전을 여러 번 반복하는 것과 동일합니다. 이는 우리가 원하는 결과가 아닙니다. 결론 처음에는 성능에 관심이 있는 프로그래머들에게 유용한 기능으로 보였지만, 다양한 컴파일러와 환경에서 효율적으로 작동시키는 것이 어려울 수 있다는 점이 불행합니다. 그러나 벡터 함수의 기능은 고성능 프로그래밍에서 큰 가치를 가질 수 있으므로, 이 기능을 효과적으로 활용하기 위한 지속적인 노력이 필요합니다. 업계 전문가의 평가 SIMD 함수는 이론적으로는 매력적인 성능 향상을 제공하지만, 실제 사용 시 여러 제한 사항과 주의 사항이 있어 그 효과성이 감소할 수 있습니다. 특히, 컴파일러 지원이 제한적이며, 벡터 함수의 유연성과 성능 사이의 균형을 맞추는 것이 어려운 점이 있습니다. 그러나 이 기능의 잠재적 가치는 고성능 컴퓨팅 분야에서 여전히 중요하다는 점을 기억해야 합니다. 회사 프로필 이 글을 작성한 회사는 성능 최적화, 벡터화, 고성능 컴퓨팅 분야에서 전문적인 서비스를 제공합니다. 프로젝트의 성능 문제에 대해 논의하거나, 벡터화 트레이닝을 받고 싶다면 문의하시거나 LinkedIn, Twitter, Mastodon을 통해 새로운 콘텐츠가 제공되면 바로 알림을 받으실 수 있습니다.

Related Links