4 minute read

Vectorization의 개념



벡터화(Vectorization)는 단일 명령어로 여러 데이터를 동시에 처리하는 기술입니다.
즉, 반복적인 계산을 수행하는 루프를 벡터 명령어 하나로 대체하여 스칼라 형식으로 하나씩 작업하는 것보다 처리 속도를 획기적으로 향상시킵니다.
주로 아래와 같이 성능을 향상 시키거나 코드를 단순화 할 때 사용됩니다.

  • 성능 향상: 현대 프로세서는 SIMD(Single Instruction, Multiple Data) 명령어 집합을 지원하여 벡터화된 코드를 매우 빠르게 실행합니다.
  • 코드 단순화: 복잡한 루프를 간단한 벡터 연산으로 표현하여 코드를 더욱 간결하게 만들 수 있습니다.

동작 방식


  1. 데이터를 벡터 레지스터에 로드
  2. 벡터 명령어를 사용하여 여러 데이터를 동시에 연산
  3. 결과를 메모리에 저장

Vectorization의 장단점


장점

  • 성능 향상: 특히 반복적인 연산에서 상당한 성능 향상을 얻을 수 있습니다.
  • CPU 효율 증가: 동일한 시간에 더 많은 연산을 처리할 수 있습니다.

단점

  • 구현의 복잡성: 벡터 연산을 사용하기 위해 코드를 직접 수정해야 하는 경우가 많습니다.
  • 데이터 정렬 문제: 데이터를 효율적으로 벡터화하기 위해서는 메모리 상에 데이터가 특정 방식으로 정렬되어 있어야 합니다.
  • 모든 코드에 적용 가능한 것은 아님: 조건 분기, 함수 호출 등이 많은 코드에서는 벡터화가 어려울 수 있습니다.

Auto Vectorization이란?


자동 벡터화(Auto Vectorization)는 컴파일러가 소스 코드를 분석하여 벡터화할 수 있는 부분을 자동으로 찾아 벡터 명령어로 변환하는 기술입니다.
컴파일러 옵션을 통해 자동 벡터화를 활성화하면 개발자가 직접 벡터화 코드를 작성하지 않아도 성능 향상을 얻을 수 있습니다

동작 방식


  1. 컴파일러가 소스 코드를 분석합니다.
  2. 반복문과 같은 벡터화 가능한 부분을 찾습니다.
  3. SIMD 명령어를 사용하여 벡터화된 코드를 생성합니다.

Auto Vectorization의 장단점


장점

  • 간편함: 프로그래머가 별도로 코드를 수정할 필요가 없습니다.
  • 이식성: 컴파일러가 알아서 처리하므로 코드의 이식성이 높습니다.

단점

  • 제한적: 모든 코드를 자동으로 벡터화할 수 있는 것은 아닙니다.
    코드의 구조, 데이터 의존성 등에 따라 자동 벡터화가 불가능할 수 있습니다.

  • 성능 예측의 어려움: 컴파일러가 어떻게 벡터화할지 정확히 예측하기 어려울 수 있습니다.


주의사항


Vectorization의 주의사항

  • 데이터 정렬 (Data Alignment): 벡터 연산은 메모리에 연속적으로 정렬된 데이터를 효율적으로 처리합니다. 데이터가 특정 바이트 단위(예: 16바이트, 32바이트)로 정렬되지 않은 경우, 추가적인 작업이 필요하거나 성능이 저하될 수 있습니다.
    • alignas 키워드를 사용하여 변수를 특정 바이트로 정렬할 수 있습니다.
    • 동적 할당 시에는 정렬된 메모리 할당 함수(예: aligned_alloc)를 사용해야 합니다.

  • 데이터 의존성 (Data Dependency): 반복문 내에서 이전 반복의 결과에 의존하는 경우 (예: a[i] = a[i-1] + b[i]) 벡터화가 어렵습니다. 이러한 의존성을 제거하거나 재구성해야 벡터화가 가능합니다.

  • 조건 분기 (Conditional Branching): 반복문 내에 조건 분기가 많은 경우 벡터화가 어려울 수 있습니다. 분기 예측 실패로 인한 성능 저하가 발생할 수 있기 때문입니다. 조건 분기를 최소화하거나, 마스크 연산 또는 선택 연산 등을 사용하여 벡터화 가능한 형태로 변경해야 합니다.

  • 함수 호출 (Function Call): 반복문 내에서 함수를 호출하는 경우 벡터화가 어려울 수 있습니다. 함수 호출 오버헤드가 발생하고, 컴파일러가 함수 내부를 분석하여 벡터화하기 어려울 수 있기 때문입니다. 가능하면 함수 호출을 인라인하거나, 벡터화된 함수를 사용해야 합니다.

  • 자료형 (Data Type): 벡터 연산은 특정 자료형(예: float, double, int)에 최적화되어 있습니다. 다른 자료형을 사용하는 경우 성능 향상이 미미하거나 오히려 성능이 저하될 수 있습니다.

  • SIMD 명령어 세트 (SIMD Instruction Set): 사용하는 CPU가 지원하는 SIMD 명령어 세트(예: SSE, AVX, AVX2, AVX-512)를 고려해야 합니다. 오래된 명령어 세트를 사용하면 최신 CPU의 성능을 제대로 활용하지 못할 수 있습니다. 컴파일러 옵션을 통해 특정 명령어 세트를 활성화할 수 있습니다.

  • 코드 이식성 (Code Portability): Intrinsic을 사용하는 경우 코드의 이식성이 떨어집니다. 다른 컴파일러나 다른 CPU 아키텍처에서 코드를 컴파일할 때 문제가 발생할 수 있습니다. 가능하면 컴파일러 지시어 또는 라이브러리를 사용하여 이식성을 높이는 것이 좋습니다.

  • 디버깅 (Debugging): 벡터화된 코드는 디버깅이 어려울 수 있습니다. 스칼라 코드와는 다른 방식으로 실행되기 때문에 디버거가 제대로 동작하지 않을 수 있습니다.

Auto Vectorization의 주의사항

  • 컴파일러 의존성 (Compiler Dependency): 자동 벡터화는 컴파일러의 기능에 의존합니다. 컴파일러의 버전이나 설정에 따라 벡터화 결과가 달라질 수 있습니다.

  • 예측 불가능성 (Unpredictability): 컴파일러가 어떤 부분을 벡터화할지 정확히 예측하기 어려울 수 있습니다. 컴파일러가 벡터화하지 못하는 경우도 발생할 수 있습니다.

  • 최적화 수준 (Optimization Level): 컴파일러의 최적화 수준을 높여야 자동 벡터화가 효과적으로 수행됩니다. 일반적으로 -O2 또는 그 이상의 최적화 레벨을 사용하는 것이 좋습니다.

  • -ffast-math 옵션: 일부 컴파일러에서는 -ffast-math 옵션을 사용하여 부동 소수점 연산의 정확도를 다소 포기하는 대신 벡터화를 더 적극적으로 수행하도록 할 수 있습니다.하지만 이 옵션을 사용하면 결과의 정확도가 달라질 수 있으므로 주의해야 합니다.

  • 루프 구조 (Loop Structure): 자동 벡터화는 특정 형태의 루프에만 적용 가능합니다.
    • 간단한 카운터 변수를 사용하는 루프가 벡터화에 유리합니다.
    • 루프의 시작과 끝을 명확하게 알 수 있어야 합니다.
    • 루프 내에 복잡한 조건 분기나 함수 호출이 없어야 합니다.

  • 데이터 의존성 (Data Dependency): 자동 벡터화 역시 데이터 의존성에 영향을 받습니다. 컴파일러가 데이터 의존성을 분석하여 벡터화 가능 여부를 판단합니다.

  • 컴파일러 보고서 (Compiler Report): 컴파일러는 자동 벡터화 결과를 보고하는 기능을 제공합니다. 이러한 보고서를 활용하여 어떤 부분이 벡터화되었고 어떤 부분이 벡터화되지 않았는지 확인할 수 있습니다. (예: MSVC의 /Qvec-report, GCC의 -fopt-info-vec)



코드


#include <iostream>
#include <immintrin.h> // AVX, AVX2 명령어 사용을 위한 헤더

void add_arrays(float* a, float* b, float* c, int n) {
    for (int i = 0; i < n; i += 8) { // 8개의 float 데이터를 한번에 처리 (AVX2)
        __m256 va = _mm256_load_ps(a + i);
        __m256 vb = _mm256_load_ps(b + i);
        __m256 vc = _mm256_add_ps(va, vb);
        _mm256_store_ps(c + i, vc);
    }
}

int main() {
    float a[8] = {1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f};
    float b[8] = {9.0f, 10.0f, 11.0f, 12.0f, 13.0f, 14.0f, 15.0f, 16.0f};
    float c[8];
    add_arrays(a, b, c, 8);
    for (int i = 0; i < 8; ++i) {
        std::cout << c[i] << " ";
    }
    std::cout << std::endl;
    return 0;
}



Top