Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Ndc12 2

8,789 views

Published on

Ndc12 2

  1. 1. 멀티쓰레드 프로그래밍이 왜이리 힘드나요?(컴파일러와 하드웨어에서 Lock-free 알고리즘 까지) 정내훈 한국산업기술대학교 게임공학과
  2. 2. 2-2 발표자 소개• KAIST 전산과 박사 – 전공 : 멀티프로세서 CPU용 메모리 일관성 유지 HW• NCSoft 근무 – Alterlife , Project M 프로그램 팀장• 현 : 한국산업기술대학교 게임공학과 부교수 – 학부 강의 : 게임서버프로그래밍 – 대학원 강의 : 멀티코어프로그래밍
  3. 3. 2-3 목차• 도입• 현실• Welcome to the Hell.• 새로운 희망• 우리의 나아갈 길• 실제성능
  4. 4. 2-4 도입• 멀티쓰레드 프로그래밍 이란? – 멀티코어 혹은 멀티프로세서 컴퓨터의 성능을 이끌어 내기 위한 프로그래밍 기법 – 흑마술의 일종 • 잘못 사용하면 패가 망신
  5. 5. 2-5 도입• 흑마술 멀티쓰레드 프로그래밍의 위험성 – “자꾸 죽는데 이유를 모르겠어요” • 자매품 : “이상한 값이 나오는데 이유를 모르겠어요” – “더 느려져요”
  6. 6. 2-6 내용• 도입• 현실• Welcome to the Hell.• 새로운 희망• 우리의 나아갈 길
  7. 7. 2-7 현실• “멀티쓰레드 안 해도 되지 않나요?” – NO! – “MultiThread 프로그래밍을 하지 않는 이상 프로그램의 성능은 전혀 나아지지 않을 것임” – by Intel, AMD • “공짜 점심은 끝났어요~~”
  8. 8. 2-8 현실피할 곳도 숨을 곳도 없습니다.
  9. 9. 2-9 현실• 멀티쓰레드 프로그래밍을 하지 않으면? – (멀티코어) CPU가 놀아요. – 경쟁회사 제품보다 느려요. • FPS • 동접
  10. 10. 2-10 현실• 멀티 코어 CPU가 왜 나왔는가? – 예전에는 만들기 힘들어서? No – 다른 방법들의 약발이 다 떨어져서! • 클럭 속도, 캐시, 슈퍼스칼라, Out-of-order, 동적 분기 예측… – 늦게 나온 이유 • 프로그래머에게 욕을 먹을 것이 뻔하기 때문.
  11. 11. 2-11 현실• 컴퓨터 공학을 전공했지만 학부에서 가르치지 않았다.• 큰맘 먹고 스터디를 시작했지만 한 달도 못 가서 흐지부지 되었다. (원인은 다음 페이지)• 그냥 멀티쓰레드 안 쓰기로 했다.
  12. 12. 2-12 현실• 좋은 교재
  13. 13. 2-13 현실• 왜 멀티쓰레드 프로그래밍이 어려운가? – 다른 쓰레드의 영향을 고려해서 프로그램 해야 하기 때문에 – 에러 재현과 디버깅이 힘들어서 – Visual Studio가 사기를 치고 있기 때문• 왜 멀티쓰레드 프로그래밍이 진짜로 어려운가? – CPU가 사기를 치고 있기 때문
  14. 14. 2-14 내용• 도입• 현실• Welcome to the Hell.• 새로운 희망• 우리의 나아갈 길
  15. 15. 2-15 고생길• Visual Studio의 사기 DWORD WINAPI ThreadFunc1(LPVOID lpVoid) { data = 1; flag = true; } DWORD WINAPI ThreadFunc2(LPVOID lpVoid) { while(!flag); my_data = data; }
  16. 16. 2-16 고생길• Visual Studio의 사기 DWORD WINAPI ThreadFunc2(LPVOID lpVOid) {DWORD WINAPI ThreadFunc2(LPVOID lpVoid) 00951020 mov al,byte ptr [flag (953374h)]{ while(!flag); while (!flag); my_data = data; 00951025 test al,al 00951027 je ThreadFunc2+5 (951025h)} int my_data = data; printf("Data is %Xn", my_data); 00951029 mov eax,dword ptr [data (953370h)] 0095102E push eax 0095102F push offset string "Data is %Xn" (952104h) 00951034 call dword ptr [__imp__printf싱글 쓰레드 프로그램이면? (9520ACh)] 0095103A add esp,8 VS는 무죄! return 0; 0095103D xor eax,eax } 0095103F ret 4
  17. 17. 2-17 고생길• Visual Studio의 사기를 피하는 방법 – volatile을 사용하면 된다. • 반드시 메모리를 읽고 쓴다. • 변수를 레지스터에 할당하지 않는다. • 읽고 쓰는 순서를 지킨다. – 참 쉽죠? – “어셈블리를 모르면 Visual Studio의 사기를 알 수 없다” 흠좀무…
  18. 18. 2-18 고생길• 정말 쉬운가??? struct Qnode { volatile int data; volatile Qnode* next; }; 무엇이 문제일까?? DWORD WINAPI ThreadFunc1(LPVOID lpVoid) { while ( qnode->next == NULL ) { } my_data = qnode->next->data; }
  19. 19. 고생길• volatile의 사용법 – volatile int * a; • *a = 1; // 순서를 지킴 • a = b; // 순서를 지키지 않는다. – int * volatile a; • *a = 1; // 순서를 지키지 않음, • a = b; // 이것은 순서를 지킴
  20. 20. 고생길• Volatile 위치 오류의 예 volatile Qnode* next; Qnode * volatile next; void UnLock() { Qnode *qnode; qnode = myNode; if (qnode->next == NULL) { LONG long_qnode = reinterpret_cast<LONG>(qnode); volatile LONG *long_tail = reinterpret_cast<volatile LONG*>(&tail); if ( CAS(long_tail, NULL, long_qnode) ) return; while ( qnode->next == NULL ) { } } qnode->next->locked = false; qnode->next = NULL; 011F1089 mov eax,dword ptr [esi+4] } 011F108C lea esp,[esp] 011F1090 cmp eax,ebx 011F1092 je ThreadFunc+90h (11F1090h)01191090 mov eax,dword ptr [esi+4]01191093 test eax,eax01191095 je ThreadFunc+90h (1191090h)
  21. 21. 2-21 고생길• Thread 2개로 합계 1억을 만드는 프로그램#include <windows.h>#include <stdio.h>volatile int sum = 0;DWORD WINAPI ThreadFunc(LPVOID lpVoid){ for (int i=1;i<=25000000;i++) sum += 2; return 0;} int main() { DWORD addr; HANDLE hThread2 = CreateThread(NULL, 0, ThreadFunc, NULL, 0, &addr); HANDLE hThread3 = CreateThread(NULL, 0, ThreadFunc, NULL, 0, &addr); WaitForSingleObject(hThread2, INFINITE); WaitForSingleObject(hThread3, INFINITE); CloseHandle(hThread2); CloseHandle(hThread3); printf(“Result is %dn", sum); getchar(); return 0; }
  22. 22. 2-22 고생길• 다중 쓰레드 - 결과
  23. 23. 2-23 고생길• 결과는?• 엉뚱한 답
  24. 24. 2-24 고생길• 왜 틀린 결과가 나왔을까? ─ “sum+=2”가 문제이다.
  25. 25. 2-25 고생길• 왜 틀린 결과가 나왔을까? – DATA RACE (복수의 쓰레드에서 같은 공유 메모리에 동시에 WRITE하려는 행위.) 때문이다. 쓰레드 1 쓰레드 2 MOV EAX, SUM sum = 200 ADD EAX, 2 MOV EAX, SUM sum = 200 MOV SUM, EAX ADD EAX, 2 sum = 202 MOV SUM, EAX sum = 202
  26. 26. 2-26 고생길• 해결 방법은? – Data Race가 있으면 멀티 쓰레드 프로그램이 의도하지 않은 결과를 낼 수 있다. – Data Race를 없애면 된다.• 어떻게 – Lock과 Unlock을 사용한다.
  27. 27. 2-27 고생길• 결과
  28. 28. 2-28 고생길• 결과가 옳게 나왔다. 만족하는가? 실행시간 결과 1 Thread 280577 100000000 2 Thread 146823 50876664 4 Thread 132362 27366758 No LOCK 실행시간 결과 1 Thread 2888071 100000000 2 Thread 5947291 100000000 4 Thread 4606754 100000000 35배의 성능차이 With LOCK
  29. 29. 2-29 고생길• EnterCriticalSection() 이라는 물건은 – 한번에 하나의 쓰레드만 실행 시킴 – Lock을 얻지 못하면 시스템 호출
  30. 30. 2-30 고생길• 해결 방법은? – Lock을 쓰지 않으면 된다. – “Sum += 2”를 Atomic하게 만들면 된다.• Atomic – 실행 중 다른 Core가 끼어들지 못하도록 한다.
  31. 31. 2-31 고생길• 결과가 옳게 나왔다. 만족하는가? 실행시간 결과1 Thread 280577 100000000 실행시간 결과2 Thread 146823 50876664 1 Thread 1001528 1000000004 Thread 132362 27366758 2 Thread 1462121 100000000 No LOCK 4 Thread 1452311 100000000 실행시간 결과1 Thread 2888071 100000000 With InterlockedOperation2 Thread 5947291 1000000004 Thread 4606754 100000000 With LOCK
  32. 32. 2-32 HELL• 정답은?
  33. 33. 2-33 고생길• 만족하는가? (i7 – 4core) 실행시간 결과 실행시간 결과 1 Thread 280,577 100000000 1 Thread 2,888,071 100000000 2 Thread 146,823 50876664 2 Thread 5,947,291 100000000 4 Thread 132,362 27366758 4 Thread 4,606,754 100000000 No LOCK With LOCK 실행시간 결과 실행시간 결과 1 Thread 1,001,528 100000000 1 Thread 28,7776 100000000 2 Thread 1,462,121 100000000 2 Thread 15,6394 100000000 4 Thread 1,452,311 100000000 4 Thread 96,925 100000000 With InterlockedOperation 정답
  34. 34. 2-34 고생길• 만족하는가? (XEON E5405, 2CPU) 실행시간 결과 실행시간 결과 1 Thread 1,798 100000000 1 Thread 27,073 100000000 2 Thread 2,926 520251348 2 Thread 83,586 100000000 4 Thread 1,692 203771150 4 Thread 69,264 100000000 8 Thread 3,699 193307522 8 Thread 67,156 100000000 No LOCK With LOCK 실행시간 결과 실행시간 결과 1 Thread 11,307 100000000 1 Thread 2055 100000000 2 Thread 24,194 100000000 2 Thread 1798 100000000 4 Thread 20,292 100000000 4 Thread 834 100000000 8 Thread 18,699 100000000 8 Thread 419 100000000 With InterlockedOperation 정답
  35. 35. 2-35 고생길• 지금까지 – Visual Studio의 마수에서 벗어나기 • Volatile을 잘 쓰자 – 경쟁상태 해결하기. • Lock을 최소화 하자 • Lock대신 atomic operation을 사용하자
  36. 36. 2-36 고생길• 그러나 – 절대로 모든 문제가 정답처럼 풀리지 않는다. – Interlocked로 구현 가능하면 다행 (atomic) • Interlock이 가능한 것은 일부 Instruction – 일반적인 자료구조를 Lock없이 Atomic하게 구현하는 것은 큰 문제다. – Lock말고도 다른 문제가 있다.
  37. 37. 2-37 HELL• 멀티 코어에서는 Data Race말고도 다른 문제점이 있다.• “상상한 것 그 이상을 보여준다”, 충공깽
  38. 38. 2-38 HELLEnterCriticalSection()이 문제가 있으니나만의 Lock을 구현해 볼까?
  39. 39. 2-39 HELL• 다음의 프로그램으로 Lock과 Unlock이 동작할까? – 피터슨 알고리즘 volatile int victim = 0; volatile bool flag[2] = {false, false}; – 두개의 쓰레드에서 Lock구현 Lock(int threadId) – 운영체제 교과서에 실려 있음 { int i = threadId; – threadId는 0과 1이라고 가정 j = 1 – i; flag[i] = true;• 실행해 보자 victim = i; while(flag[j] && victim == i) {}• 결과는? } Unlock (int threadId) { int i = threadId; flag[i] = false; }
  40. 40. 2-40 HELL• 이유는?
  41. 41. 2-41 HELL• 이유는? – CPU는 사기를 친다. • Line Based Cache Sharing • Out of order execution • write buffering – CPU는 프로그램을 순차적으로 실행하는 척만한다. • 싱글코어에서는 절대로 들키지 않는다.
  42. 42. 2-42 HELL• Out-of-order 실행 a = fsin(b); f = 3; a = b; // a,b는 cache miss c = d; // c,d는 cache hit
  43. 43. 2-43 HELL• 문제는 메모리 – 프로그램 순서대로 읽고 쓰지 않는다. • 읽기와 쓰기는 시간이 많이 걸리므로. • 옆의 프로세서(core)에서 보면 보인다.• 어떠한 일이 벌어지는가?
  44. 44. 2-44 HELL• 아래의 두 개의 실행결과는 서로 다르다 어떠한 것이 정확한 결과인가? thread a thread b write (x, 1) write(x, 2) Type-A read(x, 2) read(x, 2) thread a thread b Type-B write (x, 1) write(x, 2) read(x, 1) read(x, 1)
  45. 45. 2-45 HELL• 그러면 이것은? thread a thread b write (x, 1) write(x, 2) Type-C!! read(x, 2) read(x, 1) thread a thread b Type-D !! write (x, 1) read(y, 1) write (y, 1) read(x, 0)
  46. 46. 2-46 HELL• 그러면 이것은? thread a thread b write (x, 1) write(y, 1) Type-E read (y, 0) Read(x, 0) thread a thread b thread c thread dType-F write (x, 1) write (x, 2) read(x, 1) read(x, 2) read(x, 2) read(x, 1)
  47. 47. 2-47 HELL• 현실 – 앞의 여러 형태의 결과는 전부 가능하다.• 부정확해 보이는 결과가 나오는 이유? – 현재의 CPU는 Out-of-order실행을 한다. – Cache가 프로세서마다 따로 존재 하며 Cache는 64byte line별로 따로 관리된다. – 위의 2가지가 없다면 Computer는 몇백배 느려진다.
  48. 48. 2-48 HELL• 메모리 쓰기 순서는 모든 코어에서 일치 하는가?#define THREAD_MAX 2 int main()#define SIZE 10000000 {volatile int x,y; DWORD addr;int trace_x[SIZE], trace_y[SIZE]; HANDLE hThread[THREAD_MAX];DWORD WINAPI ThreadFunc0(LPVOID a){ .. // Thread 2개 실행 for(int i = 0; i <SIZE;i++) { x = i; int count = 0; trace_y[i] = y; for (int i=0; i< SIZE;++i) } return 0; if (trace_x[i] == trace_x[i+1])} if (trace_y[trace_x[i]] == trace_y[trace_x[i] + 1]) { if (trace_y[trace_x[i]] != i) continue;DWORD WINAPI ThreadFunc1(LPVOID a) printf ("Error x : %d, trace[x]%d --", i, trace_x[i]);{ printf ("y : %d : trace[y] : %d n", for(int i = 0; i <SIZE;i++) { trace_x[i], trace_y[trace_x[i]]); y = i; trace_x[i] = x; } } printf("Total Memory Inconsistency:%dn", count); return 0; return 0;} }
  49. 49. 2-49 HELL• 프로그램 설명 2 7 6 1 8보다 3이 먼저 write!! 3 7 7 2 4 8 8 2 3보다 8이 먼저 write!!! 5 8 9 3 6 8 10 5 7 9 11 6 x traceY y traceX
  50. 50. 2-50HELL 공황상태…
  51. 51. 2-51 HELL• 메모리에는 유령이… bool done = false; volatile int *bound; int error; DWORD WINAPI ThreadFunc1(LPVOID lpVoid) { for (int j = 0; j<= 25000000; ++j) *bound = -(1 + *bound); done = true; return 0; } DWORD WINAPI ThreadFunc2(LPVOID lpVOid) { while (!done) { int v = *bound; if ((v !=0) && (v != -1)) error ++; } return 0; }
  52. 52. 2-52 HELL• 어떻게 실행했길래?volatile int ARR[256];bound = (int *) ((((((int) ARR) + 128) >> 7 ) << 7) - 2) ;error = 0;DWORD addr;HANDLE hThread2 = CreateThread(NULL, 0, ThreadFunc1, (LPVOID) 0, 0, &addr);HANDLE hThread3 = CreateThread(NULL, 0, ThreadFunc2, (LPVOID) 1, 0, &addr);
  53. 53. 2-53 HELL• 어떻게 실행했길래? bound 2byte 2byte Cache line
  54. 54. 2-54 HELL• 결과가…. – 중간값 • write시 최종값과 초기값이 아닌 다른 값이 도중에 메모리에 써지는 현상 short buf[256] – 이유는? buf[0] = length; buf[1] = OP_MOVE; • Cache Line Size Boundary *((float *)(&buf[2])) = x; *((float *)(&buf[4])) = y; – 대책은? *((float *)(&buf[6])) = z; *((float *)(&buf[8])) = dx; • Pointer를 절대 믿지 마라. *((float *)(&buf[10])) = dy; *((float *)(&buf[12])) = dz; • Byte 밖에 믿을 수 없다. *((float *)(&buf[14])) = ax; *((float *)(&buf[16])) = ay; • Pointer가 아닌 변수는 Visual C++가 잘 해준다. *((float *)(&buf[18])) = az; *((int *)(&buf[20])) = h; … send( fd, buf, (size_t)buf[0], 0 );
  55. 55. 2-55 HELL• 이러한 현상을 메모리 일관성(Memory Consistency) 문제라고 부른다. http://en.wikipedia.org/wiki/Memory_ordering
  56. 56. 2-56 HELL• 어떻게 할 것인가? – 강제로 원하는 결과를 얻도록 한다. • 모든 메모리 접근을 Lock/Unlock으로 막으면 가능 – 성능저하!!! – Lock은 어떻게 구현? – 위의 상황을 감안하고 프로그램 작성 • 프로그래밍이 너무 어렵다. – 피터슨이나 빵집 알고리즘도 동작하지 않는다. 어쩌라고???
  57. 57. 2-57 HELL• 정리 – 멀티쓰레드에서의 공유 메모리 • 다른 코어에서 보았을 때 업데이트 순서가 틀릴 수 있다. • 메모리의 내용이 한 순간에 업데이트 되지 않을 때 도 있다. – 일반적인 프로그래밍 방식으로는 멀티쓰레드에서 안정적으로 돌아가는 프로그램을 만들 수 없다.
  58. 58. 2-58 내용• 도입• 현실• Welcome to the Hell.• 새로운 희망• 우리의 나아갈 길• 실제성능
  59. 59. 2-59 희망• 메모리에 대한 쓰기는 언젠가는 완료 된다.• 자기 자신의 프로그램 순서는 지켜진다.• 캐시의 일관성은 지켜진다. – 한번 지워졌던 값이 다시 살아나지는 않는다. – 언젠가는 모든 코어가 동일한 값을 본다• 캐시라인 내부의 쓰기는 중간 값을 만들지 않는다.
  60. 60. 2-60 희망• 우리가 할 수 있는 것 – CPU의 여러 삽질에도 불구 하고 Atomic Memory를 구현 할 수 있다. 더군다나 Locking없이 SW적인 방법 만으로 – 증명은 교재(아까 그 책) 참조• Atomic – 접근(메모리는 read, write)의 절대 순서가 모든 쓰레드에서 지켜지는 자료구조 – 프로그래머가 필요로 하는 바로 그 자료구조 – 싱글코어에서는 모든 메모리가 Atomic Memory이다.
  61. 61. 2-61 희망• Atomic Memory 만 있으면 되는가? – NO – 우리가 진짜로 필요로 하는 것은 멀티쓰레드에서 동작하는 효율적인 자료구조들이다. • Queue, Stack, List, Map, Tree…… – 즉 Atomic한 자료구조가 필요하다.• STL은??? – 못 쓴다.• STL + LOCK은??? – 느려서 못쓴다.• 근데 “효율적인” 이라니?
  62. 62. 2-62 Lock없는 프로그램• 효율적인 구현 – Lock없는 구현 • 성능 저하의 주범이므로 당연 – Lock이 없다고 성능저하가 없는가?? • 상대방 쓰레드에서 어떤 일을 해주기를 기다리는 한 동시실행으로 인한 성능 개선을 얻기 힘들다. – while (other_thread.flag == true); – lock과 동일한 성능저하 • 상대방 쓰레드의 행동에 의존적이지 않는 구현방식이 필요하다.
  63. 63. 2-63 Lock없는 프로그램• 효율적인 구현 – 블럭킹 (blocking) • 다른 쓰레드의 진행상태에 따라 진행이 막힐 수 있음 – 예) while(lock != 0); • 멀티쓰레드의 bottle neck이 생긴다. • Lock을 사용하면 블럭킹 – 넌블럭킹 (non-blocking) • 다른 쓰레드가 어떠한 삽질을 하고 있던 상관없이 진행 – 예) 공유메모리 읽기/쓰기, Interlocked Operation
  64. 64. 2-64 Lock없는 프로그램• 넌블럭킹의 등급 – 무대기 (wait-free) • 모든 메소드가 정해진 유한한 단계에 실행을 끝마침 • 멈춤 없는 프로그램 실행 – 무잠금 (lock-free) • 무한히 많은 메소드가 유한한 단계에 실행을 끝마침 • 무대기이면 무잠금이다 • 기아(starvation)을 유발하기도 한다. • 성능을 위해 무대기 대신 무잠금을 선택하기도 한다.
  65. 65. 2-65 Lock없는 프로그램• 넌블럭킹의 등급 – 제한된 무대기 (bounded wait-free) • 유한하지만 쓰레드의 개수에 비례한 유한일 수 있다. – 무간섭 (obstruction-free) • 한 개를 제외한 모든 쓰레드가 멈추었을 때, 멈추지 않은 쓰레드의 메소드가 유한한 단계에 종료할 때. • 충돌 중인 쓰레드를 잠깐 멈추게 할 필요가 있다.
  66. 66. 2-66 Lock없는 프로그램• 정리 – Wait-free, Lock-free • Lock을 사용하지 않고 • 다른 쓰레드가 어떠한 행동을 하기를 기다리는 것 없이 • 자료구조의 접근을 Atomic하게 해주는 프로그램
  67. 67. 2-67 병행성과 정확성• 그러면, Atomic Memory로 그런 자료구조를 만들면 되지 않는가?• Atomic Memory만으로는 다중 쓰레드 무대기 큐를 만들 수 없다!!!!!! – (증명) : 아까 그 책
  68. 68. 2-68 병행성과 정확성• 다중 쓰레드 무대기 큐를 만들려면? – 합의 (Consensus)객체가 필요하다. • 합의 – 여러 개의 쓰레드 중 하나만 골라내어 그 선택자를 모두에게 알려주는 API – 당연히 무대기 – 실제 컴퓨터에 존재하는가? • 존재한다. • 특별한 CPU 명령으로 구현된다.
  69. 69. 2-69 합의(Consensus)• <넘어가자>• 동기화 연산의 능력을 알아보는데 사용되는 알고리즘• 합의 객체 의 정의 <PASS> – value_t decide(value_t value) 메소드를 구현 – n 개의 스레드가 decide를 호출한다. – 하나의 스레드는 한번 이하로만 호출한다. – decide는 모든 호출에 대해 같은 값을 반환한다. – decde가 반환하는 값은 어떠한 스레드가 입력한 값이다.
  70. 70. 2-70 합의(Consensus)• 구현 (Windows API) – InterlockedCompareExchange() LONG __cdecl InterlockedCompareExchange( __inout LONG volatile *Destination, __in LONG Exchange, __in LONG Comparand ); – Destination에 저장된 값과 Comparand가 같은 값이면 Destination에 Exchange를 쓴다. – Destination에 저장되어 있던 값을 리턴한다. – Atomic하다.
  71. 71. 2-71 합의(Consensus)• 실제 x86 CPU상의 구현 – lock prefix와 cmpxchg 명령어로 구현 – lock cmpxchg [A], b 기계어 명령으로 구현 • eax에 비교값, A에 주소, b에 넣을 값• EnterCriticalSection()도 위와 같은 어셈블리를 사용해서 구현되어 있다. – 하지만 lock을 얻지 못하면 Windows kernel 호출크리.
  72. 72. 2-72 합의(Consensus)• 합의의 위용 – 모든 자료구조를 멀티쓰레드 무대기자료구조로 만들 수 있다. – 바꿔주는 프로그램이 있다. – STL도 OK!
  73. 73. 2-73 합의(Consensus)Response Apply(Invocation Invoc, int threadId) {Node *prefer = new Node(Invoc);while (prefer->seq == 0) { Node *before = tail.GetMax(head); LONG decide_prefer = reinterpret_cast<LONG>(prefer); Node *after = reinterpret_cast<Node*>( before->decideNext.Decide(decide_prefer) ); before->next = after; after->seq = before->seq + 1; head[threadId] = after;}SeqQueue myObject;Node *current = tail.next;while (current != prefer) { myObject.Apply(current->invoc); current = current->next;}return myObject.Apply(current->invoc);
  74. 74. 2-74 희망• Happy End???? – NO• 합의를 가지고 자료구조를 <효율적인> 무대기나 무잠금으로 구현하는 것은 매우 어려운 일이다. – 구현은 어렵지 않으나 성능은 안습이다.
  75. 75. 성능 비교• 네할렘 XEON, E5520, 2.3GHz, 2CPU (8 core)• STL의 queue를 무잠금, 무대기로 구현한 것과, CriticalSection으로 atomic하게 만든것의 성능 비교. – Test조건 : 16374번 Enqueue, Dequeue (결과는 mili second) – EnterCriticaSection()을 사용한 것은 테스트 데이터의 크기가 128배 – 따라서 50000배 성능 차이 (4개 thread의 경우) 쓰레드 갯수 1 2 4 8 16 무잠금 만능 5091 7445 6811 10004 6846 무대기 만능 5021 7257 7026 9170 6466 EnterCritical 222 238 219 254 257• EnterCriticalSection을 사용해야 하는가? – No : 멀티쓰레드에서의 성능향상이 없다.
  76. 76. 2-76 희망• 결론 – CPU가 제공하는 Consensus를 사용하면 모든 싱글쓰레드 알고리즘을 Lock-free한 멀티쓰레드 알고리즘으로 변환할 수 있다.• 현실 – 비효율 적이다.
  77. 77. 2-77 희망• 대안 – 자료구조에 맞추어 최적화된 lock-free알고리즘을 일일이 개발해야 한다. • 멀티쓰레드 프로그램은 힘들다. => 연봉이 높다.• 다른 데서 구해 쓸 수도 있다. – Intel TBB, VS2010 PPL – 인터넷 – 하지만 범용적일 수록 성능이 떨어진다. 자신에게 딱 맞는 것을 만드는 것이 좋다.
  78. 78. 2-78 내용• 도입• 현실• Welcome to the Hell.• 새로운 희망• 우리의 나아갈 길• 실제성능
  79. 79. 2-79 실제 성능• 속도 비교 예제) – Set의 구현 • 중복 불가 • 정렬되어 있음 – Add, Remove, Contains 메소드 – 여러 가지 병렬화 기법 사용 • Course, Fine, Optimistic, Fine, Lock-Free – 인용) 아까 그 책
  80. 80. 2-80 실제 성능• Course Set EnterCriticalSection(&lock); pred = &head; curr = pred->next; – 모든 메소드에 Locking while (curr->key < key) { pred = curr; curr = curr->next; } if (key == curr->key) { pred->next = curr->next; delete curr; LeaveCriticalSection(&lock); return true; } else { LeaveCriticalSection(&lock); return false; }
  81. 81. 2-81 실제 성능• Fine Set head.Lock(); pred = &head; curr = pred->next; – 수정이 필요한 curr->Lock(); while (curr->key < key) { pred->Unlock(); Node만 Locking pred = curr; curr = curr->next; curr->Lock(); } if (curr->key == key) { pred->next = curr->next; curr->Unlock(); pred->Unlock(); return true; } curr->Unlock(); pred->Unlock(); return false;
  82. 82. 2-82 실제 성능 while (true) {• Optimistic Set Node *pred = &head; Node *curr = pred->next; while (curr->key < key) { – 수정이 필요한 pred = curr; curr = curr->next; Node검색을 } pred->Lock(); curr->Lock(); Locking없이 하고 if (validate(pred, curr)) { if (curr->key == key) { – Locking 후 확인 pred->next = curr->next; pred->Unlock(); curr->Unlock(); // delete curr; return true; } else { pred->Unlock(); curr->Unlock(); return false; } } pred->Unlock(); curr->Unlock(); }
  83. 83. 2-83 실제 성능• Lazy Set while (true) { Node *pred = &head; Node *curr = head.next; while (curr->key < key) { – 지운 노드를 직접 pred = curr; curr = curr->next; 지우지 않고 } pred->Lock(); curr->Lock(); marking만 한다. if (validate(pred, curr)) { if (curr->key != key) { curr->Unlock(); pred->Unlock(); – Contain()에서 return false; } else { 잠금을 없앰 curr->marked = true; pred->next = curr->next; curr->Unlock(); pred->Unlock(); – Optimistic의 최적화 } return true; curr->Unlock(); pred->Unlock(); } curr->Unlock(); pred->Unlock(); }
  84. 84. 2-84 실제 성능• Lock-free Set bool snip; while (true) { – Lock()사용안함 Window *window = find(&head, key); Node *pred = window->pred; – Next필드와 marked필드를 Node *curr = window->curr; 동시에 atomic하게 if (curr->key != key) return false; else { 업데이트 하는 것을 구현 Node *succ = GetReference(curr->next); – Pointer의 LSB를 marked로 snip = curr->attemptMark(succ, true); if (!snip) continue; 사용 pred->CompareAndSet(curr, succ, false, false); return true; } }
  85. 85. 2-85 실제 성능• Lock-free Set – CompareAndSet의 구현 bool CompareAndSet(Node *old_node, Node *next_node, bool old_mark, bool next_mark) { DWORD old_addr = reinterpret_cast<DWORD>(old_node); if (old_mark) old_addr = old_addr | 0x1; else old_addr = old_addr & 0xfffffffe; DWORD next_addr = reinterpret_cast<DWORD>(next_node); if (next_mark) next_addr = next_addr | 0x1; else next_addr = next_addr & 0xfffffffe; int prev_addr = InterlockedCompareExchange(reinterpret_cast<long *>(&next), next_addr, old_addr); return (prev_addr == old_addr); }
  86. 86. 2-86 속도 비교• Intel i7 920 (단위: 초) – 32767번 랜덤한 숫자 추가, 삭제 반복 쓰레드 개수 Course Fine Optimistic Lazy LockFree 1 0.715 3.350 1.800 0.914 0.864 2 0.992 2.723 1.267 0.668 0.589 4 0.972 1.575 0.691 0.355 0.350 8 0.970 1.199 0.463 0.278 0.247 16 0.999 1.180 0.552 0.250 0.273
  87. 87. 2-87 실제 성능• 우리의 목적 – 고성능 – 쉬운 프로그래밍• 번역하면 – 무대기 (wait-free) – 멀티쓰레드 자료구조 (Queue, Stack, List~~~)
  88. 88. 2-88 정리• 지향하는 프로그래밍 스타일 – Lock을 사용한 프로그래밍 • Blocking • 느림 (몇 백배) – Atomic Memory를 사용한 프로그래밍 • 표현력이 떨어짐 • Queue도 만들지 못함 – 무대기 자료구조를 사용한 프로그래밍 • OK
  89. 89. 2-89 정리• 무대기(wait-free) 자료구조를 사용한 프로그래밍 – 각각의 자료구조를 Lock을 사용하지 않고 구현해야 한다. – Lock은 사용하지 않지만 합의객체는 필요하다. (합의 객체 없이는 만들 수 없음이 증명되어 있다.)
  90. 90. 2-90 정리• 멀티 스레드 프로그래밍은 공유 메모리를 사용해서 스레드간의 협업을 한다.• 공유메모리를 사용한 동기화는 사용하기 힘들다. – 일관성, 중간 값 – Atomic memory의 한계• 공유 자료 구조를 만들어서 사용하는 것이 좋다.• 좋은 공유 자료 구조는 만들기 힘들다. – 무대기(wait free)알고리즘의 작성은 까다롭다. – 상용 라이브러리도 좋다. Intel TBB, VS2010 PPL(Parallel Patterns Library)등
  91. 91. 2-91 미래• 그래도 멀티쓰레딩은 힘들다. – 멀티쓰레드 프로그래머 연봉이 높은 이유• Core가 늘어나면 지금 까지의 방법도 한계 – lock-free. wait-free overhead증가 – interlocked operation overhead증가• 예측 – Transactional Memory – 새로운 언어의 필요 • 예) Earlang, Haskell
  92. 92. 2-92 TIP• Nehalem으로 업그레이드 하라. – 좋다! QPI만세 – Opteron을 쓸지언정 절대 구형 XEON은 쓰지 말아라.• Lock을 쓸 때 Spinlock은 TTS을 사용하고 일정 회전 이후는 Sleep()으로 쓰레드를 재워라. – RWLock(Reader Write Lock)을 사용할 수 있으면 하라.• 수정 후에는 반드시 Testing – Lock이 허름한 무대기/무잠금보다 성능이 높은 경우가 많다.• 메모리관리는 따로 해라 – tsmalloc(thread cashing malloc)도 괜찮다.• 인텔 매뉴얼을 믿지 마라. – CPU 실행방식 변경가능, CPU가 Arm으로 바뀔 수도 있다. – 매뉴얼 믿고 쓰다가 뒤통수 (너무 고약하다) – 자주 바뀌는 매뉴얼• Worst Case 튜닝 – 저부하 일때 CPU낭비 상관 없음• Wait-free는 Lock-free에서 starvation을 제거한 것
  93. 93. 2-93 NEXT• 다음 강의(내년???) – Lock-free search : SKIP-LIST – 효율적인 LOCK : tts, back-off, spin-count 고찰 – Next Solution • Stateless 프로그래밍 언어 • Transactional Memory – ABA Problem, aka 효율적인 reference counting
  94. 94. 2-94 Q&A• 연락처 – nhjung@kpu.ac.kr – <제작중> http://wsl.kpu.ac.kr/~gameserver/index.html – 발표자료 : ftp://210.93.61.41 id:ndc21 passwd: <바람의나라>• 참고자료 – Herlihy, Shavit, “The Art of Multiprocesor Programming”, Morgan Kaufman, 2008 – SEWELL, P., SARKAR, S., OWENS, S., NARDELLI, F. Z., AND MYREEN, M. O. x86-tso: A rigorous and usable programmer’s model for x86 multiprocessors. Communications of the ACM 53, 7 (July 2010), 89–97. – INTEL, “Intel 64 and IA-32 Architectures Software Developer’s Manual”, Vol 3A: System Programming Guide, Part 1
  95. 95. 2-95 광고• 체계적으로 배우고 싶으신 분 – 한국산업기술대학교 게임공학과 대학원 – 회사를 설득해서 계약 체결 • JCE와 계약 경험(win-win) • 회사로 출장 강의 • 등록금 공짜 (정부지원) • 석사학위 취득

×