AVX는 인텔이 2008년에 발표한 고성능 ISA로 기존의 SSE(Streaming SIMD Extensions)에 포함된 많은 operation들을 지원함과 동시에 더 빠른 속도로 더 큰 덩어리(chunk)의 데이터를 처리할 수 있는 혁신적인 기술이다.
추가적으로 AVX는 SIMD 레지스터 폭이 128비트에서 256비트로 증가되었고, 2 피연산자 구조에서 3 피연산자 구조로 변경되었다고 한다.
AVX 명령은 mov, add
와 같이 어셈블리 명령어에 해당하지만, C/C++에서 사용할 때는 emmintrin.h
와 같은 라이브러리를 통해 사용할 수 있다.
AVX는 작은 사이즈의 연산들을 각각 처리하는 것이 아니라 한번에 커다란 연산들의 Chunk를 처리함으로써 어플리케이션의 속도를 비약적으로 향상시킨다. 이러한 커다란 크기의 데이터 덩어리를 vector라고 하며 최대 256 bit의 데이터까지 담아낼 수 있다.
#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 명령어 함수를 사용하게 되면 더 빠르게 처리가 가능하다.
#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
자료형 | 비트 수 | 설명 |
---|---|---|
__m128 | 128 | 4 floats |
__m128d | 128 | 2 doubles |
__m128i | 128 | integers (bit 체제에 따라 다름) |
__m256 | 256 | 8 floats |
__m256d | 256 | 4 doubles |
__m256i | 256 | integers (bit 체제에 따라 다름) |
위의 표와 같이 각각의 타입명은 __m
+ <비트수>로 표시된다.
__m256i
, __m128i
처럼 뒤에 i
가 붙었다고 해서 int
형만 지원할 것 같지만 char
, short
, unsigned long long
과 같은 여러 정수형 자료형을 포함할 수 있다.
__mm<bit_width>_<name>_<data_type>
<bit_width>
는 return value에 해당하는 vector의 사이즈를 나타낸다.<name>
은 operator의 이름을 나타낸다.<datatype>
은 함수의 주요 인자의 데이터 타입을 나타낸다.
아래는 <datatype>
의 종류이다.
floats
를 포함하는 벡터doubles
를 포함하는 벡터signed integer
를 포함하는 벡터unsigned integer
를 포함하는 벡터타입이 명시되지 않은 128, 256 비트
의 벡터리턴 벡터의 타입과 인풋 벡터타입이 다를 경우
함수명 | 설명 |
---|---|
__mm256_setzero_ps / pd | 0으로 채워진 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 / m128i | 256 bit 크기의 벡터를 2개의 128 bit 크기의 벡터로 초기화 |
__mm256_setr_ps / pd | 8개의 floats / 4개의 doubles를 가지고 벡터를 역순으로 초기화 |
__mm256_setr_epi8 / epi16 / epi32 / epi64 | 주어진 정수값들로 벡터를 역순으로 초기화 |
__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] << " ";
}
함수명 | 설명 |
---|---|
_mm256_load_ps / pd | 인자로 전달된 메모리 주소로부터 fp 벡터를 로드 |
_mm256_load_si256 | 인자로 전달된 메모리 주소로부터 int 벡터를 로드 |
_mm256_loadu_ps / pd | 인자로 전달된 "아직 할당되지 않은" 메모리 주소로부터 fp 벡터를 로드 |
_mm256_loadu_si256 | 인자로 전달된 "아직 할당되지 않은" 메모리 주소로부터 int 벡터를 로드 |
_mm_maskload_ps / pd | 128 / 256 bit의 일부를 로드 |
_mm256_maskload_ps / pd | mask에 따라서 fp 벡터를 로드 |
[AVX2 함수] _mm_maskload_epi32 / 64 | 128 / 256 bit의 일부를 로드 |
[AVX2 함수] _mm256_maskload_epi32 / 64 | mask에 따라서 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_*
를 사용하면된다.
#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이면 로드되지 않는다.