란 일반 연산과 SIMD의 연산 성능 실험 SIMD ? 란 일반 연산과 SIMD의 연산 성능 실험 SIMD ? 🔎 SIMD란? 는 Single Instruction, Multiple Data의 약자로 하나의 명령어로 여러 개의 값은 동 시에 계산하는 방식이다. 병렬 컴퓨팅의 한 종류 한 번에 여러 연산을 해야하는 벡터 연산, 행렬 연산에 유용하다. SIMD 🔨 SSE: SIMD 확장 라 불리는 명령어 셋은 SIMD를 확장한 것으로 한 번 SSE (Streaming SIMD Extensions) . 에 여러 데이터를 처리할 수 있다 128비트 너비의 레지스터를 사용할 수 있으며, 하나의 명령으로 128 비트만큼의 연산을 수 행할 수 있다. - 벡터 연산을 지원한다. 4개의 32 비트 정수/float 2개의 64 비트 정수/float 이후 SSE2, SSE3, SSE4등 발전 되어 256비트/512비트의 레지스터도 추가되었다. 이후 예시에서 SSE를 사용하기위해 헤더를 include하였다. <immintrin.h> SISD 연산과 SIMD 연산 성능 차이 와 SISD의 어셈블리 Instruction SIMD 다음 블로그를 참고하였다: SIMD에 대한 집중탐구! 란 일반 연산과 SIMD의 연산 성능 실험 SIMD ? 1 직접 덧셈의 어셈블리 코드를 비교해보았다. 🙋 어셈블리 코드 확인하는 방법: 다음 링크를 참고하면 된다: 비주얼 스튜디오 어셈블리코드 확인하기 (DisAssembly) 전체 코드 파일SISD 덧셈 함수 void add(int* a, int* b, int* c) { c[0] c[1] c[2] c[3] = = = = a[0] a[1] a[2] a[3] + + + + b[0]; b[1]; b[2]; b[3]; } SIMD 덧셈 함수 - void addSIMD(int* a, int* b, int* c) { __m128i mA = _mm_load_si128((__m128i*)a); __m128i mB = _mm_load_si128((__m128i*)b); __m128i mC = _mm_add_epi32(mA, mB); 란 일반 연산과 SIMD의 연산 성능 실험 SIMD ? 2 _mm_store_si128((__m128i*)c, mC); } 의 Insturction들 SISD <br><br> SIMD 의 Insturction들 파일 <br><br> 같은 결과 값을 내는 연산에서 SIMD연산이 SISD보다 적은 Instruction을 쓰는 걸 볼 수 있 다. 성능 비교 - 시간 덧셈과 같은 단순한 연산에선 성능 차이가 크게 없지만, 행렬 곱셈 등 복잡한 연산에선 2~4 배의 차이를 보인다. 연산이 복잡할수록, 연산 횟수가 많을 수록 성능에 큰 차이를 보인다. 덧셈 개의 elements를 가진 int 배열을 1000000번 연산했을 때의 차이이다. 전체 코드 파일 // ...(생략) 위의 add 함수들과 동일 4 int main() { 생략 // ... ( ) int a[] = { 1, 2, 3, 4 }; int b[] = { 5, 6, 7, 8 }; int c[4]; auto start = chrono::high_resolution_clock::now(); for (int i = 0; i < 1000000; i++) { add(a, b, c); } auto end = chrono::high_resolution_clock::now(); auto addTime = chrono::duration_cast<chrono::nanosecond s>(end - start).count(); 란 일반 연산과 SIMD의 연산 성능 실험 SIMD ? 3 cout << "add: " << addTime << "ns" << endl; start = chrono::high_resolution_clock::now(); for (int i = 0; i < 1000000; i++) { addSIMD(a, b, c); } end = chrono::high_resolution_clock::now(); auto addSIMDTime = chrono::duration_cast<chrono::nanose conds>(end - start).count(); cout << "addSIMD: " << addSIMDTime << "ns" << endl; 생략) 결과 비교 // ... ( } 큰 차이를 보이진 않지만, 성능이 향상됨을 볼 수 있다. 행렬 곱셈 이번엔 좀 더 복잡한 연산을 해보자. 1000 * 1000 크기의 행렬 곱셈을 수행해보았다. 전체 코드 파일 // ... const int N = 1000; 일반 행렬 곱셈 // void matrixMulti(float* A, float* B, float* C) { for (int i = 0; i < N; i++) { 란 일반 연산과 SIMD의 연산 성능 실험 SIMD ? 4 for (int j = 0; j < N; j++) { C[i * N + j] = 0; for (int k = 0; k < N; k++) { C[i * N + j] += A[i * N + k] * B[k * N + j]; } } } } 비트 레지스터를 사용하여 행렬 곱셈 // 128 SIMD void matrixMultiplySIMD128(float* A, float* B, float* C) { for (int i = 0; i < N; ++i) { for (int j = 0; j < N; ++j) { __m128 sum = _mm_setzero_ps(); for (int k = 0; k < N; k += 4) { __m128 a = _mm_loadu_ps(&A[i * N + k]); __m128 b = _mm_loadu_ps(&B[k * N + j]); sum = _mm_add_ps(sum, _mm_mul_ps(a, b)); } float result[4]; _mm_storeu_ps(result, sum); C[i * N + j] = result[0] + result[1] + result [2] + result[3]; } } } 비트 레지스터를 사용하여 행렬 곱셈 // 256 SIMD void matrixMultiplySIMD256(float* A, float* B, float* C) { for (int i = 0; i < N; ++i) 란 일반 연산과 SIMD의 연산 성능 실험 SIMD ? 5 { for (int j = 0; j < N; ++j) { __m256 sum for (int k { __m256 __m256 = _mm256_setzero_ps(); = 0; k < N; k += 8) a = _mm256_loadu_ps(&A[i * N + k]); b = _mm256_loadu_ps(&B[k * N + j]); sum = _mm256_add_ps(sum, _mm256_mul_ps(a, b)); } float result[8]; _mm256_storeu_ps(result, sum); C[i * N + j] = result[0] + result[1] + result [2] + result[3] + result[4] + result[5] + result[6] + resul t[7]; } } } int main() { // ...( // 생략) 행렬 초기화 일반 행렬 곱셈 auto start = chrono::high_resolution_clock::now(); matrixMulti(A, B, C); auto end = chrono::high_resolution_clock::now(); auto regularTime = chrono::duration_cast<chrono::millis econds>(end - start).count(); cout << "Regular Matrix Multiply: " << regularTime << "ms" << endl; 행렬 곱셈 // SIMD 128 start = chrono::high_resolution_clock::now(); matrixMultiplySIMD128(A, B, C); end = chrono::high_resolution_clock::now(); auto simdTime = chrono::duration_cast<chrono::milliseco 란 일반 연산과 SIMD의 연산 성능 실험 SIMD ? 6 nds>(end - start).count(); cout << "SIMD Matrix Multiply: " << simdTime << "ms" << endl; 행렬 곱셈 // SIMD 256 start = chrono::high_resolution_clock::now(); matrixMultiplySIMD256(A, B, C); end = chrono::high_resolution_clock::now(); auto simdTime256 = chrono::duration_cast<chrono::millis econds>(end - start).count(); cout << "SIMD Matrix Multiply 256: " << simdTime256 << "ms" << endl; 생략) 결과 출력 및 비교 // ... ( } 행렬 곱셈에서는 성능 차이가 크게 나는 것을 볼 수 있고, 레지스터의 크기가 커질 수록, 성능이 빨라짐을 볼 수 있다. 위의 예시처럼 1000 * 1000와 같은 큰 행렬을 계산하기 위해선 지스터를 사용하는 것이 좋겠지만, 란 일반 연산과 SIMD의 연산 성능 실험 SIMD ? __m256 , __m512 등더큰레 7 만일 벡터 연산과 같이 2~4 요소를 연산한다면 에서 더 좋을 것이다. __m128 를 사용하는 것이 기능/메모리 차원 기타 출처: 을 이용한 3D 게임 프로그래밍 입문, 프랭크 D. 루나 SIMD - 위키백과, 우리 모두의 백과사전 (wikipedia.org) 스트리밍 SIMD 확장 - 위키백과, 우리 모두의 백과사전 (wikipedia.org) DirectX 11 란 일반 연산과 SIMD의 연산 성능 실험 SIMD ? 8