[모던 CMake] SIMD 프로젝트 구성과 빌드

이번 글에서는 CMake를 사용하여 SIMD(Single Instruction, Multiple Data) 기반의 응용 프로그램을 구성하고 빌드하는 방법을 더욱 자세히 알아보겠습니다. SIMD는 데이터 병렬 처리를 통해 성능을 향상시키는 기술로, 멀티미디어 처리, 신호 처리, 과학 계산 등 다양한 분야에서 활용됩니다. 이번 글에서는 다양한 운영체제, CPU 아키텍처, 컴파일러에 따른 컴파일러 플래그와 옵션 설정 방법을 자세히 살펴보고, 크로스 빌드 상황에서 필요한 설정과 라이브러리, 툴에 대해서도 알아보겠습니다. 또한, 조건부 빌드의 다양한 사례를 통해 실전에서의 활용 방법을 제시하겠습니다.

SIMD와 CMake의 통합

SIMD 명령어 집합은 CPU 아키텍처와 세대에 따라 다르며, 이를 활용하기 위해서는 컴파일러 플래그와 설정이 필요합니다. CMake를 사용하면 이러한 설정을 효율적으로 관리하고, 다양한 플랫폼과 아키텍처에 대응할 수 있습니다.

주요 SIMD 명령어 집합

  • x86 및 x86_64 아키텍처:
    • SSE 시리즈: SSE, SSE2, SSE3, SSSE3, SSE4.1, SSE4.2
    • AVX 시리즈: AVX, AVX2, AVX-512
  • ARM 아키텍처:
    • NEON
    • SVE (Scalable Vector Extensions)

컴파일러 플래그와 옵션 설정

운영체제, CPU 아키텍처, 사용하는 컴파일러에 따라 SIMD 명령어를 활성화하기 위한 플래그가 다릅니다. 아래에서는 GCC, Clang, MSVC에서 SIMD를 활성화하는 방법을 자세히 살펴보겠습니다.

GCC와 Clang에서의 SIMD 플래그

x86 및 x86_64 아키텍처

  • SSE2 활성화 (기본적으로 활성화되어 있을 수 있음):
  • target_compile_options(MySIMDApp PRIVATE -msse2)
  • SSE4.2 활성화:
  • target_compile_options(MySIMDApp PRIVATE -msse4.2)
  • AVX 활성화:
  • target_compile_options(MySIMDApp PRIVATE -mavx)
  • AVX2 활성화:
  • target_compile_options(MySIMDApp PRIVATE -mavx2)
  • AVX-512 활성화:
  • target_compile_options(MySIMDApp PRIVATE -mavx512f)

ARM 아키텍처

  • NEON 활성화:
  • target_compile_options(MySIMDApp PRIVATE -mfpu=neon)
  • SVE 활성화 (ARMv8-A 이상):
  • target_compile_options(MySIMDApp PRIVATE -march=armv8-a+sve)

MSVC에서의 SIMD 플래그

MSVC 컴파일러는 /arch 옵션을 사용하여 SIMD 명령어 집합을 지정합니다.

  • SSE2 활성화 (기본적으로 활성화됨):
  • target_compile_options(MySIMDApp PRIVATE /arch:SSE2)
  • AVX 활성화:
  • target_compile_options(MySIMDApp PRIVATE /arch:AVX)
  • AVX2 활성화:
  • target_compile_options(MySIMDApp PRIVATE /arch:AVX2)
  • AVX-512 활성화 (지원 여부 확인 필요):
    # MSVC에서 AVX-512 활성화 (Intel Compiler 사용 시)
    target_compile_options(MySIMDApp PRIVATE /QxCORE-AVX512)
    
  • MSVC는 /arch:AVX512 옵션을 제공하지 않습니다. AVX-512를 사용하려면 Intel Compiler를 사용하거나, MSVC에서 /Qx 옵션을 사용하여 특정 CPU를 타겟팅해야 합니다.

CMake에서 아키텍처별 설정 적용

CMake의 조건문과 생성기 표현식을 사용하여 운영체제, 아키텍처, 컴파일러에 따라 컴파일러 플래그를 적용할 수 있습니다.

타겟 프로퍼티와 조건문 사용

add_executable(MySIMDApp main.cpp)

# 컴파일러 식별
if(MSVC)
    # MSVC의 경우
    target_compile_options(MySIMDApp PRIVATE /arch:AVX2)
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
    # GCC의 경우
    target_compile_options(MySIMDApp PRIVATE -mavx2)
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
    # Clang의 경우
    target_compile_options(MySIMDApp PRIVATE -mavx2)
else()
    message(WARNING "지원되지 않는 컴파일러입니다.")
endif()

아키텍처 감지 및 조건부 설정

if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64" OR CMAKE_SYSTEM_PROCESSOR MATCHES "AMD64")
    message(STATUS "타겟 아키텍처: x86_64")
    # x86_64용 플래그 설정
    target_compile_definitions(MySIMDApp PRIVATE ARCH_X86_64)
elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64" OR CMAKE_SYSTEM_PROCESSOR MATCHES "ARM64")
    message(STATUS "타겟 아키텍처: ARM64")
    # ARM64용 플래그 설정
    target_compile_definitions(MySIMDApp PRIVATE ARCH_ARM64)
else()
    message(WARNING "알 수 없는 아키텍처: ${CMAKE_SYSTEM_PROCESSOR}")
endif()

생성기 표현식을 사용한 컴파일러 플래그 설정

target_compile_options(MySIMDApp PRIVATE
    $<$<AND:$<CXX_COMPILER_ID:GNU>,$<PLATFORM_ID:Linux>>:-mavx2>
    $<$<AND:$<CXX_COMPILER_ID:Clang>,$<PLATFORM_ID:Darwin>>:-mavx2>
    $<$<CXX_COMPILER_ID:MSVC>:/arch:AVX2>
)

조건부 빌드의 다양한 사례

SIMD 코드를 작성할 때, 지원되는 명령어 집합에 따라 다른 코드를 빌드하거나 실행해야 하는 경우가 많습니다. 이를 위해 컴파일타임과 런타임에 지원 여부를 확인하고, 코드의 일부를 조건부로 포함하거나 제외할 수 있습니다.

컴파일타임 조건부 빌드

컴파일러가 정의하는 매크로를 활용하여 특정 SIMD 명령어 집합이 지원되는지 확인하고, 이에 따라 코드를 분기할 수 있습니다.

예제: AVX2 지원 여부에 따른 코드 분기

#if defined(__AVX2__)
    #define SIMD_LEVEL_AVX2
#elif defined(__AVX__)
    #define SIMD_LEVEL_AVX
#elif defined(__SSE4_2__)
    #define SIMD_LEVEL_SSE4_2
#elif defined(__SSE2__)
    #define SIMD_LEVEL_SSE2
#else
    #define SIMD_LEVEL_NONE
#endif

// ...

void compute() {
#if defined(SIMD_LEVEL_AVX2)
    // AVX2 코드를 사용
    compute_with_avx2();
#elif defined(SIMD_LEVEL_AVX)
    // AVX 코드를 사용
    compute_with_avx();
#elif defined(SIMD_LEVEL_SSE4_2)
    // SSE4.2 코드를 사용
    compute_with_sse4_2();
#elif defined(SIMD_LEVEL_SSE2)
    // SSE2 코드를 사용
    compute_with_sse2();
#else
    // SIMD를 사용하지 않는 코드
    compute_without_simd();
#endif
}

런타임 조건부 빌드

런타임에 CPU의 지원 여부를 확인하고, 적절한 함수를 호출하는 방법입니다. 이를 위해 CPUID 명령어나 라이브러리를 사용할 수 있습니다.

예제: x86 아키텍처에서의 SIMD 지원 여부 확인

#include <immintrin.h>
#include <cpuid.h>

bool supportsAVX2() {
    unsigned int eax, ebx, ecx, edx;
    __cpuid_count(7, 0, eax, ebx, ecx, edx);
    return (ebx & (1 << 5)) != 0; // AVX2 비트 체크
}

void compute() {
    if (supportsAVX2()) {
        // AVX2 함수를 호출
        compute_with_avx2();
    } else if (supportsAVX()) {
        // AVX 함수를 호출
        compute_with_avx();
    } else {
        // 일반 함수를 호출
        compute_without_simd();
    }
}

함수 포인터를 활용한 동적 디스패치

함수 포인터를 사용하여 런타임에 적절한 함수를 선택하는 방법입니다.

void (*compute_func)();

void init_compute_func() {
    if (supportsAVX2()) {
        compute_func = compute_with_avx2;
    } else if (supportsAVX()) {
        compute_func = compute_with_avx;
    } else {
        compute_func = compute_without_simd;
    }
}

int main() {
    init_compute_func();
    compute_func();
    return 0;
}

크로스 컴파일 설정

크로스 컴파일을 위해서는 타겟 플랫폼에 맞는 툴체인 파일을 작성하고, 필요한 라이브러리와 툴을 설치해야 합니다.

툴체인 파일 작성

예제: x86_64에서 ARMv7 리눅스용 크로스 컴파일 툴체인 파일

# toolchain-armv7.cmake

set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR arm)

set(TOOLCHAIN_PREFIX arm-linux-gnueabihf)

set(CMAKE_C_COMPILER ${TOOLCHAIN_PREFIX}-gcc)
set(CMAKE_CXX_COMPILER ${TOOLCHAIN_PREFIX}-g++)

set(CMAKE_SYSROOT /path/to/armv7/sysroot)

set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})

set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)

빌드 시 툴체인 파일 지정

cmake -B build -S . -DCMAKE_TOOLCHAIN_FILE=toolchain-armv7.cmake
cmake --build build

크로스 컴파일 시 SIMD 플래그 설정

if(CMAKE_CROSSCOMPILING)
    if(CMAKE_SYSTEM_PROCESSOR MATCHES "arm")
        message(STATUS "크로스 컴파일 대상: ARM")
        target_compile_options(MySIMDApp PRIVATE -mfpu=neon)
    endif()
endif()

다양한 상황에 따른 조건부 빌드 예제

예제 1: 플랫폼별로 다른 라이브러리 링크

SIMD 관련 라이브러리가 플랫폼별로 다를 수 있습니다. 예를 들어, macOS에서는 Accelerate.framework를 사용하고, Linux에서는 libmvec을 사용할 수 있습니다.

if(APPLE)
    target_link_libraries(MySIMDApp PRIVATE "-framework Accelerate")
elseif(UNIX)
    target_link_libraries(MySIMDApp PRIVATE mvec)
endif()

예제 2: 컴파일러 버전에 따른 플래그 설정

컴파일러 버전에 따라 지원되는 SIMD 명령어 집합이 다를 수 있습니다.

if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
    if(CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 4.9)
        target_compile_options(MySIMDApp PRIVATE -mavx2)
    else()
        message(WARNING "GCC 버전이 낮아 AVX2를 사용할 수 없습니다.")
    endif()
endif()

예제 3: 사용자 정의 옵션을 통한 SIMD 활성화

사용자에게 SIMD 활성화를 선택할 수 있도록 CMake 옵션을 제공합니다.

option(ENABLE_AVX2 "Enable AVX2 instructions" OFF)

if(ENABLE_AVX2)
    if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
        target_compile_options(MySIMDApp PRIVATE /arch:AVX2)
    elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
        target_compile_options(MySIMDApp PRIVATE -mavx2)
    endif()
    target_compile_definitions(MySIMDApp PRIVATE USE_AVX2)
endif()

코드에서 USE_AVX2 매크로를 사용하여 AVX2 관련 코드를 조건부로 포함할 수 있습니다.

#ifdef USE_AVX2
// AVX2 코드를 포함
#endif

예제 4: 크로스 플랫폼 SIMD 추상화 라이브러리 사용

SIMD 추상화 라이브러리를 사용하여 플랫폼 간 코드 호환성을 유지합니다.

xsimd 사용 예제

include(FetchContent)
FetchContent_Declare(
  xsimd
  GIT_REPOSITORY https://github.com/xtensor-stack/xsimd.git
  GIT_TAG 8.1.0
)
FetchContent_MakeAvailable(xsimd)

target_link_libraries(MySIMDApp PRIVATE xsimd)

코드에서 xsimd를 사용하여 SIMD 연산을 수행합니다.

#include <xsimd/xsimd.hpp>

void compute_with_xsimd() {
    using namespace xsimd;
    std::vector<float> a = {/* 데이터 */};
    std::vector<float> b = {/* 데이터 */};
    std::vector<float> c(a.size());

    size_t simd_size = batch<float>::size;
    size_t i = 0;

    for (; i + simd_size <= a.size(); i += simd_size) {
        batch<float> va = load_unaligned(&a[i]);
        batch<float> vb = load_unaligned(&b[i]);
        batch<float> vc = va + vb;
        vc.store_unaligned(&c[i]);
    }

    // 남은 요소 처리
    for (; i < a.size(); ++i) {
        c[i] = a[i] + b[i];
    }
}

크로스 빌드에 필요한 라이브러리와 툴

크로스 컴파일러 설치

  • ARM용 GCC: gcc-arm-linux-gnueabihf 또는 gcc-aarch64-linux-gnu 패키지를 설치합니다.
    • Ubuntu 예시:
      sudo apt-get install gcc-arm-linux-gnueabihf
      
  • 크로스 컴파일용 Clang: 타겟 아키텍처를 지정하여 사용할 수 있습니다.

sysroot 설정

  • 타겟 시스템의 라이브러리와 헤더 파일이 포함된 sysroot를 설정합니다.
  • sysroot는 타겟 시스템에서 추출하거나, 제공되는 이미지에서 가져올 수 있습니다.
  • CMake 툴체인 파일에서 CMAKE_SYSROOT를 설정합니다.

CMake 툴체인 파일 설정

  • CMAKE_FIND_ROOT_PATH를 설정하여 find_package() 등이 올바르게 작동하도록 합니다.
set(CMAKE_FIND_ROOT_PATH /path/to/sysroot)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)

실전에서의 활용 팁

  • 컴파일러 에러 확인: SIMD 명령어 집합이 지원되지 않는 경우 컴파일 에러가 발생할 수 있습니다. 컴파일러 에러 메시지를 확인하고 플래그 설정을 조정합니다.
  • 성능 측정: SIMD 최적화의 효과를 측정하기 위해 벤치마킹을 수행합니다. 다양한 아키텍처에서 성능을 비교합니다.
  • 테스트 커버리지 유지: SIMD 코드와 일반 코드에 대한 테스트를 모두 작성하여 코드의 정확성을 검증합니다.
  • 문서화: 프로젝트의 CMake 설정과 SIMD 관련 코드에 대한 주석과 문서를 작성하여 다른 개발자들이 이해하기 쉽게 합니다.

참고 자료

이번 글에서는 SIMD 프로젝트를 위한 CMake 설정에 대해 더욱 자세히 알아보았습니다. 운영체제, CPU 아키텍처, 컴파일러에 따른 컴파일러 플래그 설정 방법을 구체적으로 살펴보고, 조건부 빌드의 다양한 사례를 통해 실전에서의 활용 방법을 제시하였습니다. 크로스 컴파일 상황에서의 설정과 필요한 라이브러리, 툴에 대해서도 다루었습니다. 이를 통해 다양한 플랫폼에서 SIMD를 활용한 고성능 응용 프로그램을 효율적으로 개발할 수 있을 것입니다.

반응형