Go 3분 소요

Go와 SIMD: CPU가 달라도 같은 코드로 최고 성능을 낼 수 있을까?

핵심 요약

  • SIMD는 하나의 명령으로 여러 데이터에 같은 연산을 적용하는 방식입니다.
  • 같은 소스 코드를 여러 CPU에서 쓰는 것과 각 CPU에서 최고 성능을 내는 것은 별개의 목표입니다.
  • 벡터의 폭뿐 아니라 메모리에 접근하는 방식과 데이터 배치도 성능을 좌우합니다.
  • 플랫폼 독립 SIMD의 효과는 CPU별로 측정하고 실제 서비스 성능을 비교해 확인해야 합니다.

서로 다른 CPU를 쓰는 서버에서 Go 서비스를 운영하면 성능을 최적화할 때 고려할 점도 늘어납니다. 여러 데이터를 한꺼번에 처리하는 SIMD를 쓰면 반복 계산을 줄일 수 있습니다. Go에서는 CPU별 코드를 일일이 작성하는 부담을 줄이면서 각 CPU의 성능도 충분히 끌어낼 수 있을까요?

SIMD는 계산을 한 묶음씩 처리합니다

숫자 배열의 모든 원소에 10을 더한다고 해 보겠습니다. 숫자를 하나씩 계산할 수도 있지만, 여러 숫자를 한 묶음으로 가져와 동시에 더할 수도 있습니다. SIMD는 이처럼 여러 데이터를 묶어서 계산하는 방식입니다.

SIMD는 ‘Single Instruction, Multiple Data’의 약자입니다. 하나의 명령을 여러 데이터에 적용한다는 뜻입니다. 여기서 숫자를 담는 묶음을 벡터라고 부릅니다.

예를 들어 128비트 벡터에는 32비트 정수 4개가 들어갑니다. 이 정수들의 덧셈을 지원하는 명령이라면 네 원소를 함께 계산할 수 있습니다.

그렇다고 프로그램 전체가 네 배 빨라지지는 않습니다. 메모리에서 데이터를 가져와 결과를 저장하는 데도 시간이 들고, 그 밖의 작업도 처리해야 합니다. SIMD는 여러 CPU 코어에 일을 나누는 병렬 처리와도 다릅니다. 한 코어 안에서 한꺼번에 처리하는 데이터의 묶음을 키우는 방식에 가깝습니다.

같은 코드를 쓰려면 CPU 차이를 누군가 맡아야 합니다

CPU마다 지원하는 SIMD 명령과 벡터의 폭이 다릅니다. 한 CPU에서 명령 하나로 끝나는 연산을 다른 CPU에서는 여러 명령으로 나눠 처리해야 할 수도 있습니다.

플랫폼 독립 SIMD는 애플리케이션 코드 아래에서 이런 차이를 처리해야 합니다. 개발자가 “이 데이터 묶음을 더하라”는 의도를 코드로 표현하면, 컴파일러나 라이브러리가 대상 CPU에 맞는 구현을 고르는 구조로 만들 수 있습니다.

여기서 이식성의 기준은 소스 코드입니다. 대상 CPU 아키텍처에 맞게 빌드하되, 개발자가 따로 관리해야 할 CPU별 코드는 줄이려는 것입니다.

문제는 공통으로 제공할 기능의 범위를 정하기가 쉽지 않다는 데 있습니다. 여러 CPU가 모두 쉽게 처리하는 연산만 제공하면 설계는 단순해집니다. 대신 특정 CPU의 강력한 기능을 쓸 기회는 줄어듭니다. 제공하는 기능을 늘리면 일부 CPU에서는 그 기능을 구현하는 비용이 커질 수 있습니다.

같은 연산을 코드로 표현할 수 있어도, 모든 CPU에서 같은 효율로 실행되리라는 보장은 없습니다.

벡터가 넓어져도 메모리가 발목을 잡을 수 있습니다

128비트 대신 256비트 벡터를 쓰면 같은 크기의 데이터를 더 많이 담을 수 있습니다. 하지만 계산량만으로 처리 속도가 정해지지는 않습니다.

숫자가 메모리에 연속으로 놓여 있으면 묶어서 읽기 좋습니다. 반대로 포인터를 따라 여기저기 흩어진 값을 가져와야 한다면 데이터를 모으는 데 드는 비용이 커질 수 있습니다.

이미 메모리에서 데이터를 읽는 속도가 한계에 이른 작업도 있습니다. 이런 작업에서는 계산 명령을 줄여도 전체 처리 시간이 기대만큼 줄지 않을 수 있습니다.

데이터의 크기도 살펴봐야 합니다. 짧은 배열에서는 SIMD 처리를 준비하는 데 드는 비용이 상대적으로 큽니다. 원소가 벡터에 딱 맞게 들어가지 않을 때 남은 원소를 어떻게 처리할지도 정해야 합니다.

따라서 “SIMD를 적용했다”고 설명할 때는 구체적인 조건도 밝혀야 합니다. 어떤 데이터를 얼마나 처리했고, 어느 작업에서 시간이 줄었는지 알아야 성능 숫자를 제대로 해석할 수 있습니다.

실험의 성패는 실제 작업에서 드러납니다

Go의 플랫폼 독립 SIMD 접근을 평가하려면 먼저 같은 작업을 하는 일반 구현과 비교해야 합니다. 이어서 CPU별로 최적화한 구현과 얼마나 차이가 나는지 살펴보면, 공통 코드를 유지하면서 성능을 얼마나 양보해야 하는지 알 수 있습니다.

측정할 때는 다음 항목을 확인하면 좋습니다.

  • 작은 입력과 큰 입력에서 각각 효과가 있는지 확인합니다.
  • CPU 모델과 지원 명령어에 따라 결과가 어떻게 달라지는지 비교합니다.
  • 특정 계산 함수의 속도와 서비스 전체의 응답 시간을 구분합니다.
  • SIMD 기능을 사용할 수 없는 환경에서도 올바른 결과가 나오는지 확인합니다.

특히 부분 성능과 전체 성능을 구분해야 합니다. 가령 전체 실행 시간의 20%를 차지하는 계산이 두 배 빨라져도, 나머지 조건이 같다면 전체 시간은 10% 줄어듭니다. 계산 함수가 크게 빨라졌다고 해서 서비스 전체의 성능도 같은 폭으로 좋아지는 것은 아닙니다.

Go에서 플랫폼 독립 SIMD를 쓰면 CPU별 최적화를 따로 유지하는 부담을 줄일 수 있을 것으로 기대합니다. 다만 ‘어디서나 최고 성능’을 목표로 한다면 CPU별로 검증해야 합니다. 특정 CPU에서 마지막 몇 퍼센트를 더 얻는 일과 여러 CPU에서 하나의 구현을 유지하는 일 중 무엇이 더 중요한지는 서비스를 운영하는 조건에 따라 판단해야 합니다.

Go SIMD 성능 최적화

댓글

    댓글을 불러오는 중...