SIMD 연산은 C언어에서 += 연산과 유사한 동작을 한다


예)


int a,b;


a += b;


와 유사한 연산을 한다





B : byte

W :word            

D : double word  = 4 byte

Q : quad word    = 8 byte


S는 signed US 는 unsiged 를 나타냄으로


PADDUSW 의 경우는?


위 규칙대로 해석한다면 Unsigned Word 타입으로 unsiged short : 16 bit 이며 오버플로우시 + 부호를 유지한다로

볼 수 있다








배열에 값 담아 놓고 c++ 로 더할때와 simd 를 사용해서 더할때와의 속도차 비교

[소스코드 날림 주의.. ㅋ]


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
#include <iostream>
#include <fstream>
#include <windows.h>
#include <string.h>
#include <chrono>
 
 
 
int totalSum(int sizeint data[])
{
    int sum=0;
    for(int i=0;i< size++i)
    {
        sum += data[i];
    }
    
    return sum;
}
 
 
int main()
{
 
 
    const int unit = 16;
    const int size = 40002;
    int _package=4, totalByte =0;
 
    int data[size= { 0, };
    int result[4= {0,};
 
    int quotientByte = 0;
    int quotient = (size * _package) / unit;                    // 16 byte로 떨어지는 개수, 소수점 버림
    int remainder = (size * _package) % unit;                // 16 byte 이외의 엽ㄴ
    int total = 0;
 
 
    std::fill(data, &data[size], 2);
 
 
    std::chrono::system_clock::time_point start1 = std::chrono::system_clock::now();
    std::cout<<"C++ total : "<<totalSum(size,data)<<std::endl;        //80004
    std::chrono::duration<double> sec1 = std::chrono::system_clock::now() - start1;
 
    std::cout << "simd 걸린 시간(초) : " << sec1.count() << " seconds" << std::endl;
 
 
 
 
    
    std::chrono::system_clock::time_point start = std::chrono::system_clock::now();
 
    __asm {
 
        pushad
        
        mov eax, 16
        mov ecx, quotient
        mul ecx
        mov quotientByte, eax
 
        lea esi, data
        
        pxor xmm1, xmm1
        mov ecx, 0
 
        loopAdd:
        movdqu xmm0, [esi + ecx]            //16 byte 를 xmm0 에 복사
        paddd xmm1, xmm0
 
        add ecx, unit
        cmp ecx, quotientByte
        jl    loopAdd                                //0이 아니면
 
        movdqu result, xmm1
 
        popad
        emms
    }
 
 
    //일단 지금까지의 합 구하기 (이 구문은 나중에 simd 로 한번에 처리할 수 있음)
    
    for (int i=0;i<_package;++i)
    {
        total += result[i];
    }
 
    //나머지 여분 값 더하기
    for(int i= quotientByte/_package; i< (quotientByte +remainder)/ _package; ++i)
    {
        total += data[i];
    }
 
    std::chrono::duration<double> sec = std::chrono::system_clock::now() - start;
 
    std::cout << "simd 걸린 시간(초) : " << sec.count() << " seconds" << std::endl;
 
 
    std::cout << "SIMD total : " << total << std::endl;        //80004
 
    system("pause");
 
 
    return 0;
}
 
 
 
 
 

cs






simd 연산으로 더한 결과가 훨씬 빠름을 알 수 있다

최종 total 값이 자료형의 범위를 넘지 않는다면 더 작은 바이트(4바이트가 아닌 2바이트)에서 연산 할 경우 더 높은 성능향상을

기대할 수 있다





반응형


SIMD 의 종류와 MMX

 

여기서는 Intel x86 아키텍쳐의 SIMD 만 설명하겠습니다.

먼저 Pentium 시리즈와 함께 처음 나온 MMX 가 있습니다.

이 집합은 FPU(x87) 에서 사용되는 레지스터를 공유해서 사용합니다.

즉 따로 MMX 용 레지스터가 존재하지는 않습니다.

 

그리고 MMX 후에 나온 SSE 가 있습니다.

아마 모두 한 번쯤은 들어보셨을 정도로 대표적인 SIMD 입니다.

(이름 자체도 Streaming SIMD Extensions 의 약자입니다.)

xmm 이라는 128비트 레지스터 8개가 추가되어 SIMD 전용 레지스터로 사용됩니다.

http://cafe.naver.com/cafec/243004



EMMS 를 사용하는 이유는 


The EMMS Instruction: Why You Need It

Using EMMS is like emptying a container to accommodate new content. The EMMS instruction clears the MMX™ registers and sets the value of the floating-point tag word to empty.

You should clear the MMX™ registers before issuing a floating-point instruction because floating-point convention specifies that the floating-point stack be cleared after use. Insert the EMMS instruction at the end of all MMX™ code segments to avoid a floating-point overflow exception.

Why You Need EMMS to Reset After an MMX™ Instruction





CAUTION

Failure to empty the multimedia state after using an MMX™ technology instruction and before using a floating-point instruction can result in unexpected execution or poor performance.

https://software.intel.com/en-us/node/524274




반응형





movdqu

mov : asm 에서의 mov 와 유사한 의미
dq  : double quad (word) 로 quad word는 word 의 4배 인 8 byte 와 
      앞의 double 이 붙어 2*8 = 총 16 바이트인데 word 가 기본 pack 사이즈를 말함으로 2바이트씩 나눈 배열을 말함
u   : unalign 으로 메모리에 있는 값들이 정렬되지 않은 값을 레지스터로 옮길때 붙이는 첨자
      (정렬되지 않은것은 캐쉬미스가 발생하여 오래 걸릴수 있음, 하단 그림 참조)



movdqa

a : align  : 메모리가 정렬된 상태로 정렬돈 메모리를 레지스터로 복사 시킬때에는 16개의 바이트의 메모리가 모두
             붙어 있기 때문에 캐쉬에서 레지스터로 옮길때 부하없이 로드할 수 있다 (하단 그림 참조)
a를 제외한 나머지는 movdqu 에서 movdq 들과 동일한 의미









SIMD 로 배열 copy 작성

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
int main(int argc, char* argv[])
 
{
 
    short A[8= {1,2,3,4,5,6,7,8};
 
    short B[8= {0};        
 
    __asm
 
    {
 
        pushad
 
        movdqu  xmm0, A    //한 사이클에 모두 복사된다    
 
        movdqu  B, xmm0    //한 사이클에 모두 복사된다    
 
        popad
 
        emms
 
    }
 
    printf("B : %d,%d,%d,%d,%d,%d,%d,%d\n",B[7],B[6],B[5],B[4],  B[3],B[2],B[1],B[0]);
 
    return 0;
 
}

cs



배열에서 xmm0 레지스터로 copy 되는 내역을 볼 수 있는데 순서는 반대로 copy 된다
xmm0 에서 배열로 copy 될때도 반대로
그리하여 다시 배열로 올때는 일반적으로 예상할 수 있는 A 상태 그대로의 순서대로 오는 것을 알 수 있다


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
-        xmm0    {m128_f32= {1.837e-40#DEN, 3.673e-40#DEN, 5.510e-40#DEN, 7.347e-40#DEN}  m128_f64= {5.562748306789e-309#DEN, ...} ...}    __m128
+        m128_f32    {1.837e-40#DEN, 3.673e-40#DEN, 5.510e-40#DEN, 7.347e-40#DEN}    float[4]
+        m128_f64    {5.562748306789e-309#DEN, 1.112551783418e-308#DEN}    double[2]
+        m128_i8    "\x1"    char[16]
-        m128_i16    {12345678}    short[8]
        [0]    1    short
        [1]    2    short
        [2]    3    short
        [3]    4    short
        [4]    5    short
        [5]    6    short
        [6]    7    short
        [7]    8    short
+        m128_i32    {131073262147393221524295}    int[4]
+        m128_i64    {11259127918755852251829878849541}    __int64[2]
+        m128_u8    "\x1"    unsigned char[16]
+        m128_u16    {12345678}    unsigned short[8]
+        m128_u32    {131073262147393221524295}    unsigned int[4]
+        m128_u64    {11259127918755852251829878849541}    unsigned __int64[2]
        eax    39226256    unsigned int

cs



XMM0 = 0008000700060005-0004000300020001 

XMM1 = 0000000000000000-0000000000000000 

XMM2 = 0000000000000000-0000000000000000 

XMM3 = 0000000000000000-0000000000000000 

XMM4 = 0000000000000000-0000000000000000 

XMM5 = 0000000000000000-0000000000000000 

XMM6 = 0000000000000000-0000000000000000 

XMM7 = 0000000000000000-0000000000000000 





결과

B : 8,7,6,5,4,3,2,1





[movdqa]
정렬된 데이이터의 데이터 복사

__declspec(align(16)) 과 movdqa를 사용한 예이며 결과는 movdqu 와 동일하다


단 주의해야하는 점은 movdqa 의 경우 정렬된 데이터를 취급함으로 __declspec(align(16)) 를 배열에서 빼고
실행할 경우 크래쉬가 발생한다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int main(int argc, char* argv[])
{
 
    __declspec(align(16)) short A[8= {1,2,3,4,5,6,7,8};
    __declspec(align(16)) short B[8= {0};
    __asm
    {
        pushad
        movdqa  xmm0, A        
        movdqa  B, xmm0        
        popad
        emms
    }
    printf("B : %d,%d,%d,%d,%d,%d,%d,%d\n",B[7],B[6],B[5],B[4],  B[3],B[2],B[1],B[0]);
    return 0;
}
 

cs



반응형

simd 는 범용레지스터가 아닌 전용 레지스를 사용한다


32bit : xmm0 ~ xmm7

64bit : xmm0 ~ xmm15



와 같은 레지스터들을 갖고 simd 명령어는 다음 처럼 사용 한다


simd 명령어  p + operation + type






예를 들어


paddsw 라고 한다면


p는 packed 를 의미하는 첨자이고 

add 는 일반 어셈블리의 add 와 유사한 내용이며 

sw 가 의미하는 것은 각각 s : signed,   w : word  라는 접미사의 내용이다


를 의미 하는 것으로 


2바이트로 팩된 데이터들을 한 사이클에 8개의 add 연산을 하게하는 명령어를 말한다



paddusw 는  unsigned word 를 뜻한다







반응형



위 그림에서 Register Bank 에 대해 좀더 자세히 알아본다면


레지스터 뱅크는 레지스터들이 모여있는곳을 말한다



좀더 자세히 보면




범용 레지스터(General Register) 와 XMM 전용 레지스터(XMM0~XMM7) 들이 있다

* 32bit 컴퓨터는 32bit 레지스터를 사용 64bit 컴퓨터는 레지스터 또한 64 bit 이다


범용레지스터 네이밍 (Extention) E(A~D)X,  ESI(ESourceIndex), EDI(EDestinationIndex),  ESP, EBP 이 두게는 스택 포인터 베이스 포인터의 네이밍 

(의미를 알면 외우기가 좀더 편함)


64bit 로 가면 앞에 접두어가 E 였던것이 R 로 바뀌게 된다

R(A~D)X 의 형태임이고 64 bit 에서는 32bit와는 다르게 8개의 레지스터가 위 그림처럼 더 추가 된다(R8~R15)


그리고 RAX 안에는 EAX 가 포함된 형태가 된다 위 그림에서 하얀색 부분을 EAX 라 보면 됨


XMM 레지스터의 경우에는 64bit 로 가면서 8 개가 위 그림처럼 더 추가된다










반응형



C에서 선언한 변수들은 스택, 동적으로 할당된것은 heap 에 있게 되고 그것들이 메모리영역에 올라와 있게 되고


만약 

int a=3,b=7, c;

c = a + b;


의 연산을 한다고 할 경우


먼저 데이터들 a, b 를 캐쉬 그리고 캐쉬에서 General Register 들로 옮겨야 한데 이때 옮기는 역할을 하는 것이

LSU 가 하게 된다 (올리기도 내리기도 한다)


이때 올라간 데이터가 정수라면 ALU 를 통해서 만약 올라간 데이터가 float 이면 FPU 를 통해서 연산되고


현재는 정수임으로 ALU 에서 연산된다고 할 경우 + 에 해당하는 명령어의 경우는 이러한 연산 명령어들이 모여 있는 곳인



명령어 세트에서 가져와 L1 Instruction cache(L1 명령어 캐쉬)  로 로드 된다음 이 명령어를 ALU 로 이동시킨다음 두 변수와 연산한 


결과를 다시 레지스터에 저장한다음 이 값을 다시 LSU 를 통해서 메모리영역 변수 C 에 최정 저장하게 되는 과정을 거친다










반응형



인텔® 스트리밍 SIMD 확장 기술


마지막 검토일: 25-Jul-2017
문서 ID: 000005779

인텔® 스트리밍 SIMD 확장은 무엇입니까?

  • single instruction multiple data(SIMD)
  • 인텔® 스트리밍 SIMD 확장(인텔® SSE)

스트리밍 SIMD 확장(sse)

SSE는 single instruction multiple data을 가능하게 하는 프로세서 기술. 이전 프로세서만 프로세스 당 명령을 단일 데이터 요소. SSE 명령을 여러 데이터 요소를 처리하는 수 있습니다. 3d 그래픽, 빠른 처리와 같은 많은 응용 프로그램에서 사용됩니다.

SSE는 MMX™ 기술 교체하도록 설계되었습니다. 이 확장된 세대의 인텔® 프로세서를 포함 SSE2, SSE3/SSE3S, 및 SSE4. 각 반복가 새로운 명령과 향상된 성능 제공.

Streaming SIMD Extensions 2(SSE2)

SSE2 는 다양한 응용 프로그램에서 우수한 성능을 제공하는 144개의 명령의 추가로 MMX™ 기술 및 SSE 기술을 확장합니다. SIMD MMX™ 기술 도입된 정수 명령은 64비트에서 128비트로 확장되었으며 SIMD 정수 유형 연산의 유효 실행 속도를 두 배로 늘려줍니다.

두 번 정밀도 부동 소수점 SIMD 명령 SIMD 형식에서 2개의 부동 소수점 연산의 동시 실행 허용 배정밀 연산에 대한 지원 컨텐츠 작성, 재무, 엔지니어링 및 과학 애플리케이션을 가속화하는 데 도움이 됩니다.

원래 SSE 명령어는 또한 다중 데이터 유형(예를 들어, 이중 및 사중 단어)에 대한 산술 연산을 지원함으로써 보다 유연하고 다양한 계산을 지원하는 향상된. SSE2 지침은 소프트웨어 개발자는 최대 허용 알고리즘을 구현하고 성능 향상을 제공할 수 있는 유연성이 MPEG제공됨 - 2와 같은 소프트웨어를 실행하는 경우, MP3, 3d 그래픽 등.

Streaming SIMD Extensions 3(SSE3)

90nm 프로세스 기반 인텔® 펜티엄® 4 프로세서의 출시는 스트리밍 SIMD 확장 3(SSE3)에는 13 SIMD 보다 더 많은 명령을 선보입니다 SSE2. 새로운 13개 명령은 주로 스레드 동기화를 비롯해 미디어 및 게임과 같은 특정 응용 프로그램 영역을 향상시키도록 설계되었습니다.

Streaming SIMD Extensions 4(SSE4)

SSE4 54 명령으로 구성됩니다. 일부 SSE4인텔 문서.1라고 하는 47개의 명령어로 구성된 Penryn에서 사용할 수 있습니다. SSE4.2, 두 번째 부분 집합의 7로 구성된 나머지 지침을 먼저nehalem 기반의 인텔® 코어™ i7 프로세서에서 사용할 수 있습니다. 크레딧을 인텔 개발자들의 피드백 명령어 세트 개발에 있어.



https://www.intel.co.kr/content/www/kr/ko/support/articles/000005779/processors.html

반응형



아래 그림은 c를 통해 SIMD 연산을 하는 구조를 보여준다






  1. 먼저 32 bit float array[4] 개를 만들어서 128 bit 로 Packing 한다
  2. 이후 패킹된 데이터를 xmm 레지스터에 넣어야 하는데 __m128 a   와 같이 만들어 a 에 넣으면 되는데
    이떄 array 에 있는 값이 a 에 4사이클에 걸쳐서 복사되는 것이 아닌 1 사이클로  array 내용이 a 에 복사되게끔 한다(simd)
  3. 그 이후 sqrt 하나의 명령어를(_mm_sqrt_ps) 실행 하여 1사이클로 4개의 수에 sqrt 를 적용한다
  4. 결과를 result 변수에 저장한다







성능적으로는 C,C++ 보다는 빠른 성능을 보인다


어셈블리어로 SIMD 를 사용하면 성능은 빠르다는 것은 당연한 것인데 

Vector Class 로 가면 갈 수록 약간 느린 경향을 보이긴 하지만 느려지지 않게 잘 파악하면

(SIMD + 어셈)과 (Inteinsic 함수 ) 를 사용하는 것 못지 않게 거의 유사한 성능으로 프로그래밍 할 수 있다

잘만 지키면 3가지 모두 유사한 성능을 보임


하지만 Package 에 Pack 이 몇개 들어가느냐에 따라 성능의 차이는 존재 할 수 있다

어쩌면 당연한 결과.. 


현재 시대에서는 i7 이 많이 보급되어지는 추세라 위 표 보다는 성능이 더 높을 것이라 예상한다(표는 2005~6년 보급형 컴퓨터의 기준이다)







반응형





128 bit xmm 레지스터에 데이터를 담을때에는 항상 같은 Data type  으로 Pack 해야한다



이게 무슨말이냐면 128 bit 인 xmm 레지스터에 일정한 단위로 데이터를 쌓아야(세팅해야) 한다는 뜻이다


이때 이 쌓는단위를 Pack 이라 하며 모두 쌓았을때 Package 표에 나와 있는 것처럼 128 bit(16 byte) 의 크기어야 하며


한 xmm 레지스터안의 Pack 들의 사이즈는 모두 동일해야 하며 그중 하나라도 다를 경우 오작동한다!





반응형

SISD - Single Instruction Single Data



SIMD - Single Instruction Multiple Data



SIMD 는 하나의 명령어로 여러개의 데이터를 처리하는 것을 말한다


SISD는 하나의 명령어로 하나의 데이터 처리








예를 들어 c언어에서 두개의 변수를 더하는 명령어 + 는 두개의 변수를 더하는 즉 하나의 명령어로 두 변수를 더해 하나의 변수에 결과를 저장한다

만약 8개의 덧셈을 한다고 한다면 cpu 8 클럭이 지난 후에 8개의 덧셈 연산이 완료 되지만



SIMD 는 1 사이클에 8개의 변수와 상응하는 값을 덧셈연산 할 수 있다


PADDW  는 SIMD 에서 덧셈 연산을 말하며 

MOVDQU XMM0, A 에서의 A 는 short 형 크기 8개가 나열된 배열이라고 보면 된다

이 경우  두개의 변수를 더하게 되면 한사이클로 위 그림처럼 8 개의 덧셈을 처리할 수 있다



이런 연산이 가능한것은 SIMD 를 사용하게 되면 CPU 에서 사용하는 연산을 위해 사용되는 범용 레지스터외에 XMM 이라는 

특수한 레지스터를 사용하기 때문에 이러한 처리속도의 이득을 얻을 수 있다





  • SIMD 는 xmm0~xmm7 번의 레지스터를 사용하며 각 레지스터의 크기는 16byte 이다 (128 bit)
  • 언어는 어셈블리로 SIMD 명령어를 사용하거나 C함수에서 Intrinsic 함수를 사용하거나 C++  에서 Vector Class 를 사용한다





SIMD 는 Intel 과 AMD 에서 사용가능





스트리밍 SIMD 확장

스트리밍 SIMD 확장(Streaming SIMD Extensions, SSE)은 x86 아키텍처에 대한 SIMD(단일 명령 다중 데이터) 명령어 집합 확장이며, 인텔이 1999년에 펜티엄 III 시리즈 프로세서에 도입하였다. 이 기능은 1998년 등장한 AMD사의 3D나우! 기술에 대응한다. SSE는 70가지의 새로운 명령어와 추가적인 레지스터로 구성되며, 명령어의 대부분은 부동 소수점에 대한 연산이다.

SSE는 펜티엄 III가 코드명 Katmai로 알려져 있을 시기에는 KNI(Katmai New Instructions)로 불렸다. 이후 이 이름은 ISSE(Internet Streaming SIMD Extensions)로 정해졌었으며, 이후 SSE로 변경되었다. AMD는 애슬론 XP와 듀론 프로세서를 기점으로 SSE 명령 지원을 추가했다.

레지스터

SSE는 x86 아키텍처에서 XMM0~XMM7의 8개의 128비트 레지스터를 추가한다. 또한 x86-64에서는 XMM8~XMM15의 8개의 레지스터가 추가되었다(단, 이 레지스터는 64비트 모드에서만 사용가능하다). 추가적으로, 32비트 레지스터 MXCSR는 SSE 명령어의 상태 및 제어에 사용된다.

XMM registers.png

SSE는 XMM 레지스터의 자료구조로 4개의 32비트 단정도 부동 소수점을 사용했다. 즉, 하나의 레지스터에 4개의 값이 들어가는 형태였다. SSE에서는 정수 계산을 지원하지 않는다. 이것은 MMX 명령어를 사용하는 방식으로 극복이 가능했다. 또한, SSE2부터는 SSE를 확장하여 다음의 자료구조를 지원한다.

  • 2개의 64비트 배정도 부동 소수점
  • 2개의 64비트 정수
  • 4개의 32비트 정수
  • 8개의 16비트 정수
  • 16개의 8비트 정수

SSE를 처음 지원한 펜티엄 III는 SSE와 FPU를 동시에 사용할 수 없도록 만들어졌다. 이러한 구조는 명령어 파이프라인 효율성을 떨어뜨렸다.

XMM 레지스터는 태스크 스위치 시에 값을 보존해야 하는 대상이기 때문에, 운영 체제가 이 레지스터를 사용하도록 명시적으로 활성화하기 전까지는 사용이 불가능하다. 다시 말하면, 운영 체제가 SSE 레지스터를 보존하는 명령어인 FXSAVE와 FXSTOR를 사용할 수 있어야 한다는 의미이다. 이러한 지원은 주요 IA-32 운영 체제에서 빠르게 추가되었다.

SSE 명령어

  • 부동 소수점 명령어
    • 메모리 대 레지스터 / 레지스터 대 메모리 / 레지스터 대 레지스터 데이터 이동
      • Scalar – MOVSS
      • Packed – MOVAPS, MOVUPS, MOVLPS, MOVHPS, MOVLHPS, MOVHLPS
    • 산술
      • Scalar – ADDSS, SUBSS, MULSS, DIVSS, RCPSS, SQRTSS, MAXSS, MINSS, RSQRTSS
      • Packed – ADDPS, SUBPS, MULPS, DIVPS, RCPPS, SQRTPS, MAXPS, MINPS, RSQRTPS
    • 비교
      • Scalar – CMPSS, COMISS, UCOMISS
      • Packed – CMPPS
    • 데이터 셔플 / 언패킹
      • Packed – SHUFPS, UNPCKHPS, UNPCKLPS
    • 자료형 변환
      • Scalar – CVTSI2SS, CVTSS2SI, CVTTSS2SI
      • Packed – CVTPI2PS, CVTPS2PI, CVTTPS2PI
    • 비트 논리 명령어
      • Packed – ANDPS, ORPS, XORPS, ANDNPS
  • 정수 명령어
    • 산술
      • PMULHUW, PSADBW, PAVGB, PAVGW, PMAXUB, PMINUB, PMAXSW, PMINSW
    • 데이터 이동
      • PEXTRW, PINSRW
    • 기타
      • PMOVMSKB, PSHUFW
  • 다른 명령어
    • MXCSR 관리
      • LDMXCSR, STMXCSR
    • 캐시 및 메모리 관리
      • MOVNTQ, MOVNTPS, MASKMOVQ, PREFETCH0, PREFETCH1, PREFETCH2, PREFETCHNTA, SFENCE

다음 예는 SSE의 장점을 보여준다. 컴퓨터 그래픽등에서 아주 자주 사용하는 명령어인 벡터 더하기를 보자. 두개의 단밀도를 더하기 위해서는 x87를 사용하는 4개의 구성요소 벡터가 4개의 부동소수점 더하기 명령어가 필요하다.

 vec_res.x = v1.x + v2.x;
 vec_res.y = v1.y + v2.y;
 vec_res.z = v1.z + v2.z;
 vec_res.w = v1.w + v2.w;

이것은 오브젝트코드에서 4개의 x87 FADD명령어에 해당한다. 반면에 다음 수도 코드(pseudo-code)에서는 하나의 128비트 ‘packed-add’ 명령어가 4개의 스칼라 더하기 명령어를 대체한다.

 movaps xmm0,address-of-v1          ;xmm0=v1.w | v1.z | v1.y | v1.x
 addps xmm0,address-of-v2           ;xmm0=v1.w+v2.w | v1.z+v2.z | v1.y+v2.y | v1.x+v2.x
 movaps address-of-vec_res,xmm0

뒤에 나온 버전

같이 보기


https://ko.wikipedia.org/wiki/%EC%8A%A4%ED%8A%B8%EB%A6%AC%EB%B0%8D_SIMD_%ED%99%95%EC%9E%A5#SSE_%EB%AA%85%EB%A0%B9%EC%96%B4

반응형

'운영체제 & 병렬처리 > TBB & SIMD' 카테고리의 다른 글

SIMD 연산구조와 성능  (0) 2018.02.09
Pack 과 Package  (0) 2018.02.09
왜 스레딩 빌딩 블록인가  (0) 2012.11.02
TBB 환경설정 threading building blocks  (0) 2012.11.02
[TBB] 기본 알고리즘  (0) 2012.11.02

http://studycan.tistory.com/96

[TBB] 왜 스레딩 빌딩 블록인가?

인텔 스레딩 빌딩 블록(Intel Threading Building Blocks)은 C++로 병렬 프로그램을 구현할 때 사용할 수 있는 일종의 라이브러리이며, 풍부하고 완벽한 방법론을 제공함으로써 스레드 처리에 대한 전문 지식 없이도 멀티코어 프로세서 시스템의 성능을 향상시킬 수 있도록 도와준다. 스레딩 빌딩 블록은 단순히 기존의 스레드를 대체하는 것이 아니라, 플랫폼의 세부 사항과 스레드 처리 메커니즘을 추상화한 ‘태스크’ 개념을 기반으로 하는 높은 수준의 병렬처리 기술이라고 할 수 있으며 성능(performance)과 조정성(scalability)를 목표로 한다.

이 글에서는 인텔 스레딩 빌딩 블록에 대해 소개하고 C++에서 사용되고 있는 기존의 유사 기술들과 비교하여 더 나은 점을 무엇인지 알아본다. 스레딩 빌딩 블록은 C++의 템플릿과 제네릭 프로그래밍 개념에 기반을 두고 있지만 이 글을 읽기 위해 이 개념들과 스레드 처리에 대한 사전 경험은 전혀 필요하지 않다.

설치 관련
http://studycan.tistory.com/94

개요

위 그림은 2009년 12월 19일 기준으로 다나와 사이트에서 판매되고 있는 CPU 중 1위 부터 10위까지 판매 순위이이다. 판매량을 보면 알다시피 듀얼코어나 쿼드코어 등의 멀티코어 프로세서가 대중화되고 있지만, 기존 스레딩 패키지(threading packages)를 사용하여 간단한 병렬 반복문을 작성하는 것 조차 아직 지루한 작업이다. 프로세서 코어의 개수가 증가함에 따라 프로그램의 성능도 효율적으로 조정되도록 작성하는 것은 이 작업보다 훨씬 어렵다. 여기서, 프로세서 코어의 개수가 증가할 때 프로그램의 성능도 그에 적응하여 향상된다는 의미를 가진 용어를 ‘조정성(Scalability)’이라고 정의해보자.

스레딩 빌딩 블록은 표준 C++ 코드로 조정성 있는 병렬 프로그램을 작성해주는 라이브러리이다. C++버전이나 컴파일러가 필요하지 않으며, 프로세서 종류나 운영체제와 관계없이 사실상 모든 C++ 컴파일러에 사용할 수 있다는 것이 큰 장점이다.

스레딩 빌딩 블록은 공통적 병렬 반복 패턴(parallel iteration patterns)을 위한 템플릿을 제공하는데, 이 템플릿들을 사용하면 프로그래머는 동기화(synchronization), 부하 분산(load balancing), 캐시 최적화(cache optimization)와 같은 전문 기술을 알지 못하더라도 여러 개의 프로세서 코어로부터 단 하나의 프로세서 코어에서도 속도 증가 효과를 거둘 수 있다. 스레딩 빌딩 블록 라이브러리를 사용하기 위해 스레드(thread)가 아닌 ‘태스크(task)’를 규정하고, 이 라이브러리를 하여금 이 태스크들을 스레드에 효율적인 방식으로 대응시키게 한다. 이렇게 하면 더욱 편리한 병렬처리를 구현할 수 있고, 스레드를 직접 사용하는 것보다 더 나은 결과를 얻을 수 있다.

어떤 이점이 있는가?

오늘 날의 컴퓨팅 환경에서, 프로그래머의 목표는 ‘조정성’이다. 즉 듀얼코어 프록세서에서 두 개의 코어가 모두 사용되고, 쿼드코어 프로세서에서는 네 개의 코어가 모두 사용되는 것이다. 그 이상의 코어를 가진 프로세서에서도 마찬가지다.

다양한 병렬 프로그래밍을 위한 방법들 중에서 TBB 는 다음과 같은 이유들로 기존 스레딩 패키지와 차별된다.

스레드 대신 ‘태스크’를 규정한다.
대부분의 스레딩 패키지들은 스레드를 일일히 생성, 연결 및 관리 등을 하는 하드웨어에 가까운 낮은 수준의 무거운 구조이기 때문에 비효율적이고, 스레드를 직접 사용하여 프로그래밍 할 때에는 일일이 스레드와 태스크를 효율적으로 대응시켜야 하는 불편함이 있는데 TBB 라이브러리는 프로세서의 리소스를 효율적으로 사용하는 방식으로 태스크를 스레드에 자동 스케줄링해 준다. 이 런타임은 우리가 규정할 많은 태스크들의 부하를 분산시키는데 매우 효과적이다. 이외에도 원초적 스레드를 직접 사용하는 방법으로 어셈블리 언어를 사용하는 것인데 이는 그 비용이 엄청나기 때문에 좋은 방안이 아니다.

성능 향상을 위한 스레드 처리 기술을 지향한다.
다른 스레딩 패키지들은 궁극적인 해결책이 아닌, 기반 기술만 제공하는 낮은 수준의 도구가 되는 경향이 있다면 TBB는 계산의 비중이 높은 작업을 병렬로 처리하고 더 높은 수준을 가지면서 더 단순한 해결책을 제공하는데 목표를 두고 처리

다른 스레딩 패키지들과도 잘 호환된다.
OpenMP등과 같은 다른 스레딩 패키지들과도 호환성을 가진다. 내부적으로 OpenMP를 사용하는 IPP나 MKL등 라이브러리를 자유롭게 연결할 수 있다.

조정성이 있는 데이터 병렬 프로그래밍을 강력히 지향한다.
일반적으로 기능 블록의 개수가 고정되어 있는 등의 문제로 하나의 프로그램을 여러 개의 기능 블록을 나누고 각 블록을 나누고 각 블록에 개별 스레드를 할당하는 방식으로는 높은 조정성을 기대하기 힘들다. TBB는 데이터 병렬 프로그래밍을 강력히 지향하는데 이 방식은 데이터 집합을 더 작은 조각으로 나누기 때문에 프로세서를 추가함에 따라 프로그램 성능이 향상된다. 전역 태스크 큐(global task queue)와 같은 고전적 병목 현상들도 막아주는데, 전역 태스크 큐에서 각 프로세서는 새 태스크를 얻기 위해 기다리면서 잠금 상태가 되어야 한다.

제네릭 프로그래밍을 기반으로 한다.
TBB는 제네릭 프로그래밍 방식을 사용하는데 제네릭의 본질은 최소의 제약을 가진 가장 일반적인 알고리즘을 작성하는 데 있다.

원초적 스레드 처리 방식과 MPI의 비교

POSIX 스레드(pthreads)나 윈도우 스레드 같은 원초적 스레드 인터페이스를 사용하는 프로그래밍은 공유 메모리를 사용한 병렬 처리를 구현할 때 많은 프로그래머들이 사용하는 방식 중 하나였다. Boost Threads와 같이 이식성을 높여주는 래퍼(wrapper)들도 있는데, 이 것들은 이식성이 매우 좋은 원초적 스레드 인터페이스이다. 수천 개의 프로세서를 탑재한 슈퍼컴퓨터에서는 일반적으로 공유 메로리를 사용하는 방식이 없기 때문에, 그 유명한 MPI(Message Passing Interface) 표준을 통해 메시지 전달 기법을 사용하여 병렬처리를 수행한다.

원초적 스레드와 MPI는 가장 낮은 수준에서 병렬처리를 구현하다. 따라서 병럴 처리에 대한 어셈블리 언어라고도 말할 수 있다. 이런 방식들은 최대의 유연성을 주지만, 프로그래머의 노력, 디버깅 시간 및 유지보수 면에서 많은 비용이 든다는 단점이 있다.

원초적 스레드를 사용할 경우 알게 될 사실은 정확하고 효율적인 구현을 위한 기본 작업과 데이터 공유 처리가 어렵고 지루하다는 것이다. 이 때 작성되는 코드는 운영체제의 특정 처리 기능에 종속적이다. 그리고 너무 낮은 수준 접근이기 때문에 직관적이지 못하며, 결과적으로 그 코드를 조정 가능한 성능을 갖도록 설계하기 힘들다.

논리적 스레드 대신 태스크를 사용할 경우, 얻을 수 있는 또 다른 이점은 태스크가 훨씬 더 가볍다는 것이다. 리눅스 시스템에서, 태스크를 시작하고 종료시키는 시간은 스레드를 시작하고 종료하는 것보다 18배 정도 빠르다. 윈도우 시스템에서는 100배가 넘는다.

스레드와 MPI를 사용하면, 결국 태스크들을 프로세서 코어들에 명시적으로 대응시켜 주어야 한다. 태스크를 사용한 병렬 처리 구현하기 위해 스레딩 빌딩 블록을 사용한다면 스레드를 사용할 때 보다 더 정교하게 동시성(concurrency)을 구현할 수 있으며, 그로 인해 조정성(scalability)이 향상 된다.

OpenMP와의 비교

TBB와 함께, 또 하나의 유망한 스레드 추상화 기술은 바로 OpenMP이다. 오늘날 가장 성공적인 병렬 확장 기술인 OpenMP는 포트란이나 C프로그램에서 사용할 수 있으며 프라그마(pragma), 루틴, 환경 변수들로 구성된 언어 확장팩이다. OpenMP는 사용자 병렬 프로그램을 구현하게 해주며, 컴파일러로 하여금 프로그래머의 의도를 반영한 프로그램을 생성하게 해준다. 이 기술은 포트란과 C언어의 한계를 극복한 중대한 발전이고, 컴파일러가 코드에서 병렬처리할 부분을 자동으로 찾아내지 못하게 막아준다.

C프로그램에 대해 고려해본다면 OpenMP는 ‘C로 작성된 포트란 스타일 코드에 탁월하다’라고 알려져 왔다. OpenMP는 루프 구조와 C코드에 초점을 맞추고 있기 때문에 이것은 틀린 말이 아니다. C++라고 해서 OpenMP가 특별한 것을 제공하는 것은 아니다. OpenMP의 구조는 백터 처리용 슈퍼 컴퓨터를 위해 개발된 루프 중첩 구조와 동일하다. 당시 이 방식의 슈퍼컴퓨터에는 초기 세대 병렬 프로세서들로 탑재되어 있었으며, 그 안에서 실행되는 프로그램은 상당한 양의 포트란 코드로 작성되었고, 겹겹이 중첩된 루프들 안에서 엄청난 양의 계산 작업을 수행되었다. 그리고 그러한 중첩 루프들을 병렬 코드로 변환 하는 것은 결과 면에서 매우 가치 있는 작업이 될 수 있었다.

OpenMP는 세 가지 스케줄링 방식(static, guided 및 dynamic) 중 하나를 선택해야 한다. 그러나 TBB를 사용하면 프로그래머는 이런 스케줄링 정책에 대해 전혀 걱정할 필요 없다. 자동으로 ‘분할 정복(dvivide-and-conquer)’이라는 한 가지 스케줄링 방식만을 사용함으로써 이러한 문제를 해결했다. 또한 워크 스틸링(work stealing, 로딩된 프로세서에서 유휴 상태에 있는 프로세서로 태스크를 이동시키는 테크닉) 기능도 구현되어 있는데, 이 기능은 dynamic 및 guided 스케줄링과 비교되지만 중앙 처리자 없이도 수행된다는 점이 다르다. 때때로 다른 프로세서나 또는 동시적으로 연관된 코드에 방해 받지 않는 시스템에서는 static 스케줄링이 더 빠르기도 하다. 하지만 중첩된 병렬 처리 환경에서는 분할 정복 방식이 잘 맞는다.

TBB에서 제네릭 프로그래밍을 지원한다는 것은 병렬처리의 대상이 내장된 기본 형식들에 국한되지 않는다는 것을 의미한다. OpenMP는 기본 형식에 대해서만 리덕션(reduction)을 허용하는 반면, TBB의 parallel_reduce는 모든 형식에서 사용할 수 있다.

TBB는 OpenMP의 약점을 극복하고자 C++를 지원하도록 설계되었고, C++ 프로그램에 대해 가장 간단한 해결책을 제공해준다. TBB는 정적 루프 중첩 구조에 국한되지 않고, 그 이상으로 태스크 기반의 병렬처리를 위한 재귀 모델과 제네릭 알고리즘을 구현한다.

재귀적 분할, 태스크 스틸링 및 알고리즘

TBB의 병렬처리 모델을 직관적으로 만들기 위해 여러 가지 개념들이 사용되었다. 대부분의 개념들은 적절한 수준의 병렬 태스크가 될 때까지 문제를 필요한 만큼 재귀적으로 분할하는 방식을 사용한다. 걸격 이 방식은 작업을 더욱 명백하게 정적(static)으로 분할하는 것보다 훨씬 낫다고 밝혀지는데, 전역 태스크 큐와 태스크 스틸링을 사용할 때에도 완벽하게 들어 맞는다. 이것은 태스크 큐와 같은 전역 리소스의 사용을 막아주는 중대한 설계 고려 사항인데, 전역 리소스는 조정성을 제한 할 수 있다.

여러분은 병렬처리에 적용할 알고리즘 구조(for 루프, while 루프, 파이프라인, 분할 정복 등)의 선택을 고민하는 과정에서, 결국 그 것들을 조합해야 한다는 것을 알게 될 것이다. 예를 들어 여러분의 프로그램에서 파이프라인들의 병렬 집합과 이 파이프라인들을 제어 하는 parallel_for 루프를 조합하여 사용해야 한다는 것을 알게 되었다면, 이것을 구현하는 것도 쉽다는 것을 알게 될 것이다. 뿐만 아니라, 재귀와 태스크 스틸링을 기초 설계에 포함시킴으로써 응용 프로그램을 더욱 효율적이고 조정성 있게 만들 수 있다.

반응형

http://kindtis.tistory.com/345

왠지 병렬처리를 안하면 안될거 같아!! 지금이라도 써보고 EVE에 적용시키자라는 생각으로 Threading Building Blocks ( 이하 TBB )를 설치해봤습니다. OpenMP도 봐봐야 할텐데 여기저기서 TBB를 많이 쓰시는 것 같아 TBB부터 돌려봤습니다.


일단 인텔 TBB 사이트에서 최신 안정화 버전을 다운로드 : http://threadingbuildingblocks.org/file.php?fid=77
다운 받은 라이브러리를 적당한 곳에 압축해제 해줍니다. 그리고 VS에서 환경 설정을 해줍니다.

Include 디렉터리


라이브러리 디렉터리
( 사용 중인 VS 버전에 맞는 디렉터리로 설정 )


종속성 파일 설정
( 디버그 모드에서는 tbb_debug.lib )


여기까지 하면 기본적인 TBB 사용을 위한 환경 설정이 됩니다. 참고로 TBB를 사용한 어플리케이션을 실행하게되면 tbb.dll 파일을 필요로 합니다. dll 파일들은 TBB 폴더안의 BIN 폴더에 각 환경별로 있으니 가져다 쓰시면 됩니다.


- Code Generation : 당연히 아래 옵션 킨다.
"Multi-threaded Debug DLL (/MDd)"

- Preprocessor : Debug 모드 일경우 추가 선언. Release 모드 일경우 제거.
"TBB_USE_DEBUG"

반응형

'운영체제 & 병렬처리 > TBB & SIMD' 카테고리의 다른 글

SIMD 연산구조와 성능  (0) 2018.02.09
Pack 과 Package  (0) 2018.02.09
SIMD - Single Instruction Multiple Data? 와 PADDW  (0) 2018.02.09
왜 스레딩 빌딩 블록인가  (0) 2012.11.02
[TBB] 기본 알고리즘  (0) 2012.11.02

http://studycan.tistory.com/100

[TBB] 기본 알고리즘

라이브러리 초기화 및 종료

인텔 스레딩 빌딩 블록의 컴포넌트들은 tbb 네임스페이스에 정의 된다. 이 글에서는 명시적으로 사용하기로 한다.

라이브러리에 있는 알고리즘 템플릿 또는 태스크 스케줄러를 사용하는 모든 스레드는 반드시tbb::task_scheduler_init 객체를 초기화해야 한다. 하나의 스레드에는 동시에 초기화해야 할, 하나 이상의 이 객체들이 있을 수 있다. 태스크 스케줄러는 모든 tast_scheduler_init 객체가 종료될 때 끝난다. 기본적으로, tast_scheduler_init의 생성자는 초기화를 수행하고 소멸자는 종료를 수행한다.

라이브러리 초기화
----------------------------------------------------------------------------------------------------------------------------------------------------------
#include "tbb/task_scheduler_init.h"
using namespace tbb;

int main(){
task_scheduler_init init;
. . .

return 0;
}

자동 시작/종료는 구현되지 않았는데, OpenMP를 구현해 본 인텔의 경험에 의하면, 우리는 그것을 배후에서 수행하는 것이 일부 운영체제에서는 상당히 많은 문제를 야기한다는 것을 알고 있다. 특히, 스레드가 종료되는 때를 항상 아는 것은 매우 어려운 문제이기 때문이다.

초기화를 한 번 이상 호출해도 프로그램 오류를 일으키지 않겠지만, 약간 낭비스러운 일이 될 것이며, 인텔 스레드 체커와 같은 디버깅 또는 분석 도구를 사용할 때 추가 경고들을 상당히 많이 발생시킬 수 있다.

task_scheduler_init의 생성자는 호출 스레드를 포함하여 원하는 스레드의 개수를 지정하는 선택적인 매개변수를 취하며, 이러한 선택적인 매개변수는 다음 중 하나가 될 수 있다.

  • 값 task_scheduler_init::automatic는 매개변수가 지정되지 않을 때의 기본 값이다. 이 값은 매서드 task_scheduler_init:: initialize를 위해 존재한다.
  • 값 task_scheduler_init::deferred는 매서드 task_scheduler_init::initialize(n)가 호출될 때까지 초기화가 연기되게 한다. 값 n은 생성자의 선택적인 매개변수에 대한 어떠한 합법적인 값도 될 수 있다.
  • 사용할 스레드의 개수를 지정하는 양의 정수 값

목적에 따른 생성자 인수 지정 방법

제품 개발 : task_scheduler_init::initialize(n) ‘인수를 지정
제품 출시 : task_scheduler_init::automatic ‘인수 지정 안함

큰 규모의 소프트웨어 프로젝트에서 다양한 컴포넌트들이 다른 컴포넌트에 최적이 될 스레드의 개수를 알 방법이 없기 때문이다. 하드웨어 스레드는 공유된 전역 리소스이며, 사용할 스레드의 개수를 결정하는 것은 태스크 스케줄러에게 맡기는 것이 최선이다.

스레드 개수보다 더 많은 태스크를 생성하도록 프로그램을 설계하고, 태스크 스케줄러가 태스크들을 스레드에 대응시키게 한다.

스케줄러 조기 종료
---------------------------------------------------------------------------------------------------------------------------------------------------------
#include "tbb/task_scheduler_init.h"
using namespace tbb;

int main(int argc, char* argv[]){
int nthread = strtol(argv[0], 0, 0);
task_scheduler_init init ( task_scheduler_init::deferred );

if( nthread >= 1 )
init.intialize(nthread);
. . .
if( nthread >= 1 )
init.terminate();

return 0;
}

task_scheduler_init의 소멸자는 task_scheduler_init이 초기화 되었는지 확인하고, 만일 그렇다면 종료를 수행하기 때문에 terminate()의 호출을 생략할 수 있다.

태스크 스케줄러는 초기화와 종료에 사용하기에는 다소 비용이 높기 때문에, 스레드가 생성될 떄메인 루틴에 task_scheduler_init을 집어 넣고, 병렬 알고리즘 템플릿을 사용할 때마다 스케줄러를 생성하지 않을 것을 권장한다.

반응형

+ Recent posts