[C++] AVX

alirz-pixel·2022년 10월 27일
0

C, C++

목록 보기
2/6

AVX

AVX는 인텔이 2008년에 발표한 고성능 ISA로 기존의 SSE(Streaming SIMD Extensions)에 포함된 많은 operation들을 지원함과 동시에 더 빠른 속도로 더 큰 덩어리(chunk)의 데이터를 처리할 수 있는 혁신적인 기술이다.

추가적으로 AVX는 SIMD 레지스터 폭이 128비트에서 256비트로 증가되었고, 2 피연산자 구조에서 3 피연산자 구조로 변경되었다고 한다.

using in C language

AVX 명령은 mov, add와 같이 어셈블리 명령어에 해당하지만, C/C++에서 사용할 때는 emmintrin.h와 같은 라이브러리를 통해 사용할 수 있다.

Vector Programming

AVX는 작은 사이즈의 연산들을 각각 처리하는 것이 아니라 한번에 커다란 연산들의 Chunk를 처리함으로써 어플리케이션의 속도를 비약적으로 향상시킨다. 이러한 커다란 크기의 데이터 덩어리를 vector라고 하며 최대 256 bit의 데이터까지 담아낼 수 있다.

크기 8의 float 배열 코드

#include <iostream>
#include <intrin.h>
#include <chrono>
#define MAX 100000

void multiply_and_add(const float* a, const float* b, const float* c, float* d) {
	for (int i = 0; i < 8; i++) {
		d[i] = a[i] * b[i];
		d[i] = d[i] + c[i];
	}
}

int main() {
	float a[8] = { 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0 };
	float b[8] = { 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0 };
	float c[8] = { 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0 };
	float d[8];

	std::chrono::nanoseconds sum = std::chrono::nanoseconds::zero();
	for (int i = 0; i < MAX; i++) {
		std::chrono::system_clock::time_point start = std::chrono::system_clock::now();
		multiply_and_add(a, b, c, d);
		std::chrono::system_clock::time_point end = std::chrono::system_clock::now();

		std::chrono::nanoseconds nano = end - start;
		sum += nano;
	}
	std::cout << "Elapsed time: " << sum.count() / MAX << "\n";

	return 0;
}

// Elapsed time : 70

위의 연산을 수행하는 코드가 있다고 했을 때, AVX2 명령어 함수를 사용하게 되면 더 빠르게 처리가 가능하다.

256 bit (8 * 32) 를 저장하는 _mm256 코드

#include <iostream>
#include <intrin.h>
#include <chrono>
#define MAX 100000

__m256 avx_multiply_and_add(__m256 a, __m256 b, __m256 c) {
	return _mm256_fmadd_ps(a, b, c);
}



int main() {
	// _mm256_set_ps 함수는 초기화 값을 역순으로 저장하기 때문에 _mm256_setr_ps 사용
	__m256 a = _mm256_setr_ps( 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0 );
	__m256 b = _mm256_setr_ps( 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0 );
	__m256 c = _mm256_setr_ps( 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0 );
	__m256 d = _mm256_setzero_ps();

	std::chrono::nanoseconds sum = std::chrono::nanoseconds::zero();
	for (int i = 0; i < MAX; i++) {
		std::chrono::system_clock::time_point start = std::chrono::system_clock::now();
		d = avx_multiply_and_add(a, b, c);
		std::chrono::system_clock::time_point end = std::chrono::system_clock::now();

		std::chrono::nanoseconds nano = end - start;
		sum += nano;
	}
	std::cout << "Elapsed time: " << sum.count() / MAX << "\n";

	return 0;
}
// Elapsed time : 60

AVX Programming

1. Datatype

자료형비트 수설명
__m1281284 floats
__m128d1282 doubles
__m128i128integers (bit 체제에 따라 다름)
__m2562568 floats
__m256d2564 doubles
__m256i256integers (bit 체제에 따라 다름)

위의 표와 같이 각각의 타입명은 __m + <비트수>로 표시된다.
__m256i, __m128i 처럼 뒤에 i가 붙었다고 해서 int형만 지원할 것 같지만 char, short, unsigned long long과 같은 여러 정수형 자료형을 포함할 수 있다.

2. Function Naming Convention

__mm<bit_width>_<name>_<data_type>

  1. <bit_width>는 return value에 해당하는 vector의 사이즈를 나타낸다.
  2. <name>은 operator의 이름을 나타낸다.
  3. <datatype>은 함수의 주요 인자의 데이터 타입을 나타낸다.

아래는 <datatype>의 종류이다.

  • ps: floats를 포함하는 벡터
  • pd: doubles를 포함하는 벡터
  • epi8/epi16/epi32/epi64: 8, 16, 32, 64 비트의 signed integer를 포함하는 벡터
  • epu8/epu16/epu32/epu64: 8, 16, 32, 64 비트의 unsigned integer를 포함하는 벡터
  • si128/si256: 타입이 명시되지 않은 128, 256 비트의 벡터
  • m128/m128i/m128d/m256/m256i/m256d: 리턴 벡터의 타입과 인풋 벡터타입이 다를 경우

Initalization Intrinsics

1. 스칼라 값으로 초기화 하기

함수명설명
__mm256_setzero_ps / pd0으로 채워진 floating point 벡터를 반환
__mm256_setzero_si256각 바이트가 0으로 초기화된 int 벡터를 반환
__mm256_set1_ps / pd벡터를 floating pint값으로 채움
__mm256_set1_epi8 / epi16 / epi32 / epi64벡터를 int 값으로 채움
__mm256_set_ps / pd벡터를 8개의 floats 또는 4개의 doubles로 채움
__mm256_set_epi8 / epi16 / epi32 / epi64벡터를 주어진 int 값으로 초기화
__mm256_set_m158 / m128d / m128i256 bit 크기의 벡터를 2개의 128 bit 크기의 벡터로 초기화
__mm256_setr_ps / pd8개의 floats / 4개의 doubles를 가지고 벡터를 역순으로 초기화
__mm256_setr_epi8 / epi16 / epi32 / epi64주어진 정수값들로 벡터를 역순으로 초기화

example

__m256 a = _mm256_setr_ps( 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0 );
float* fp = (float*)&a;
for (int i = 0; i < 8; i++) {
	std::cout << fp[i] << " ";
}

2. 메모리에서 로드한 데이터로 벡터 초기화하기

함수명설명
_mm256_load_ps / pd인자로 전달된 메모리 주소로부터 fp 벡터를 로드
_mm256_load_si256인자로 전달된 메모리 주소로부터 int 벡터를 로드
_mm256_loadu_ps / pd인자로 전달된 "아직 할당되지 않은" 메모리 주소로부터 fp 벡터를 로드
_mm256_loadu_si256인자로 전달된 "아직 할당되지 않은" 메모리 주소로부터 int 벡터를 로드
_mm_maskload_ps / pd128 / 256 bit의 일부를 로드
_mm256_maskload_ps / pdmask에 따라서 fp 벡터를 로드
[AVX2 함수] _mm_maskload_epi32 / 64128 / 256 bit의 일부를 로드
[AVX2 함수] _mm256_maskload_epi32 / 64mask에 따라서 fp 벡터를 로드

*주의) : _mm256_load_ 로 시작하는 intrinsic 함수들은 반드시 32 byte 단위로 aligne된 데이터만을 인자로 받을 수 있다.

메모리 할당 후, 벡터 초기화하기

#include <iostream>
#include <cstdlib>
#include <immintrin.h>

int main()
{
	int cnt = 8;
	
	// 8개의 float 데이터를 메모리에 할당
	float* aligned_floats = (float*)_aligned_malloc(sizeof(float) * 8 * cnt, 32);
	if (aligned_floats == nullptr) {
		return -1;
	}

	// initialize data
	for (int i = 0; i < cnt; i++) {
		aligned_floats[i] = (float)i;
	}

	__m256 f_vec = _mm256_load_ps(aligned_floats);
	float* f = (float*)&f_vec;
	for (int i = 0; i < cnt; i++) {
		std::cout << f[i] << " ";
	}
	_aligned_free(aligned_floats);
	// result : 0 1 2 3 4 5 6 7 

	return 0;
} 

alignment가 맞춰지지 않은 상태에서 _mm256_load_*를 사용하면 segmentation fault가 발생하게 된다. 이 경우엔 _mm256_loadu_*를 사용하면된다.

maskload

#include <iostream>
#include <immintrin.h>

int main()
{
	// 8개의 float 데이터를 메모리에 할당
	int i_arr[8] = { 10, 20, 30, 40, 50, 60, 70, 80 };

	__m256i mask = _mm256_setr_epi32(-20, -42, -31, -67, 24, 63, -12, 64);
	__m256i result = _mm256_maskload_epi32(i_arr, mask);

	int* res = (int*)&result;
	for (int i = 0; i < 8; i++) {
		std::cout << res[i] << " ";
	}

	return 0;
}

// result: 10 20 30 40 0 0 70 0

함수 이름에 mask가 들어간 함수는 alignment가 맞지 않은 데이터를 마스킹하여 호환 가능하도록 만들어주는 역할을 한다. 그래서 _maskload_가 들어간 함수는 인자로 (1) 로드할 메모리 주소 (2) mask 비트 패턴을 받는데, mask 비트 패턴의 MSB가 1이면 해당 부분의 메모리는 로드가 되고, 0이면 로드되지 않는다.

0개의 댓글