안녕하세요, 이경문이라고 합니다. 질문이 있어서 이렇게 글을 올려 봅니다.
요즘 멀티 스레드의 performance에 관심을 가지게 되어서 테스트를 해 보았습니다.
void foo(int loopCnt) // 2000000000
{ int cnt = loopCnt; for (int i = 0; i < cnt; i++); }
foo하는 함수를 실행시켜서 함수 수행 시간을 재 봤습니다. 시간은 대략 7.8초(7878msec)가 나왔습니다.
그런데 똑같은 코드를 여러개의 스레드를 동시에 돌려서 경과 시간을 재어 봤습니다. 그 결과 다음과 같은 결과가 나왔습니다.
[thread 갯수 1개]
7878
[thread 갯수 2개]
8689
8689
[thread 갯수 3개]
8799
8830
8815
[thread 갯수 4개]
9095
9282
9345
9314
상기 foo 라는 함수는 다른 스레드와 동기화 하는 부분이 없기 때문에 관련 수행 코드의 실행 시간이 비슷할 거라 판단이 되었는데(각자 다른 CPU에서 돌아가므로), 의외로 스레드가 많아 지니 수행 속도가 느려 지네요. 이 부분에 대한 자세한 원인이 무엇일까요?
자세한 코드는 아래 사이트 확인해 보실 수 있습니다.
감사합니다.
댓글 29
-
홍문화
2011.07.15 20:27
-
백창우
2011.07.15 22:25
당연히 느려질수 밖에 없는 코드를 가지고 왜 고민들을 하시는지 이유를 모르겠네요.
본 코드는 전혀 TLP를 활용할 수 있는 코드가 아닙니다.
본 코드는 thread 개수가 많아지면 control unit, BUS, memory등에서 병목현상이 걸려 더 느려지기만 할뿐인 코드입니다.
TLP 성능 테스트를 하시려거든 spec 벤치마크를 사용하시던, EEMBC 혹은 splash2와
같은 벤치마크들을 사용해서 성능 테스트를 하시거나 아니면 TLP를 활용할 수 있는 코드를 짜서 확인하십시요.
-
백창우
2011.07.17 04:58
다들 착각했을거라 짐작은 했습니다.
약간 딴지를 건다면 NUMA 아키텍쳐와 BUS 병목 현상은 이론적으로 직접적인 관련이 없습니다.
관련이 있다면 Memory Architecture 혹은 BUS Architecture와 관련이 있겠지요.
덧붙여 단순 loop만 도는 코드일지라도 cache flash에 의해 bus 병목 현상은 언제든지 일어날수 있습니다.
물론 L1, L2, L3 사이를 왔다갔다 하는 정도이겠지만 말이죠.
-
김태훈
2011.07.16 05:05
단순 루프를 도는건 멀티코어에서 각 코어의 레지스터만 이용한 연산으로 가능하기 때문에 bus나 memory 병목이 거의 없을 것입니다. 메모리에 있는 코드를 접근을 위한 bus 병목이 있을 수 있겠네요. (NUMA가 아닌 이상...) 이것도 코드 캐싱만 잘된다면 메모리에 접근 할 일이 없어지겠죠.
질문의 요지는 질문자 분의 링크에서 첫번째 case에서 동일한 스레드를 코어 갯수까지 멀티 스레드로 동작시켜보니 유사한 성능이 나오긴 하지만, 코어마다 스레드가 할당되서 병렬 수행하면 하나의 스레드와 동일한 성능이 나와야하는게 아닌가? 라는 의문점을 질문하신 것 같습니다만...저는 그렇게 이해하고 답변을 드렸습니다.
-
백창우
2011.07.15 22:49
#include <conio.h> #include <list> #include <stdio.h> #include <windows.h> typedef struct _Param { int callCnt; int loopCnt; int threadCnt; } Param; void foo(int loopCnt) { int cnt = loopCnt; for (int i = 0; i < cnt; i++); } DWORD __stdcall threadProc(LPVOID p) { Param* param = (Param*)p; int callCnt = param->callCnt;
// TLP를 활용하는 약간 더 올바른 예 int loopCnt = param->loopCnt / param->threadCnt; DWORD begTick = GetTickCount(); for (int i = 0; i < callCnt; i++) { foo(loopCnt); } DWORD endTick = GetTickCount(); printf("%dn", endTick - begTick); return 0; } void usage() { printf("call_loop_thread_test <call count> <loop count> <thread count>n"); printf("example : call_loop_thread_test 100 100 2n"); } int main(int argc, char* argv[]) { if (argc != 4) { usage(); return 0; } Param param; param.callCnt = atoi(argv[1]); param.loopCnt = atoi(argv[2]); param.threadCnt = atoi(argv[3]); std::list<HANDLE> threadList; for (int i = 0; i < param.threadCnt; i++) { DWORD threadID; HANDLE threadHandle = CreateThread( NULL, 0, &threadProc, ¶m, 0, &threadID); threadList.push_back(threadHandle); } for (std::list<HANDLE>::iterator it = threadList.begin(); it != threadList.end(); it++) { HANDLE threadHandle = *it; WaitForSingleObject(threadHandle, INFINITE); } threadList.clear(); return 0; }
-
홍문화
2011.07.15 22:55
원인을 파악 하고자 고심 하던 중 한가지 짚히는 부분이 생겼는데 백창우님 말씀과 같은 맥락이지
않을까 해서 한번 적어봅니다. 본 쓰레드는 데이터 관점에서는 공유되는 부분이 전혀 없으므로 두개의 프로세서가
각각의 L1 D캐시를 효율적으로 사용하리라 생각합니다. 하지만 코드 관점에서는 네개의 쓰레드 모두 같은 코드를
공유하므로 캐시 정책에 따라 L2, L3 캐시에 코드가 올라간다면 심각한 구조적 해저드가 발생 할 수 있지 않을까
하는 생각을 해봤습니다. 아니면 어쩔 수 없구요. ㅜㅜ
-
백창우
2011.07.15 22:59
본 코드는 애초에 TLP를 사용하게끔 작성되어지지 않은 코드입니다.
그럼으로 본 코드는 thread 개수가 늘어나면 늘어날수록 성능이 느려질수 밖에 없는 코드입니다.
본 코드를 가지고 multi-threading의 성능에 대해 논하면 안되는 코드입니다.
-
백창우
2011.07.15 23:07
좀 놀라운데요. 답변하신 분들이 너무 쉬운 문제를 가지고 열심히 틀린 답변하시는 모습이 좀 신기하게 느껴집니다.
-
이상철
2011.07.16 00:09
제가 봤을땐 질문의 내용이 하나의 일을 여러 코어에 분산하는 것이 아니라 쿼드 코어 환경에서 스레드를 하나의 생성해서 하나의 코어에 돌렸을때의 수행시간과 동일한 스레드 소스를 각각의 코어에 하나씩 수행하여 4개의 스레드를 돌렸을 때 수행 시간의 차이를 물어보신거 같습니다.
일단 제가 드는 생각은 각각 코어에서 동일 메모리 접근 시 대기하는 것도 생각 할 수 있을 거 같고, 또는 모든 코어를 이용하기 때문에 이 루프를 수행하는 도중 이 프로세스외에 다른 데몬같은 것으로 스케줄링 되어 다시 온 경우도 생각해볼수 있을것 같습니다.
-
이상철
2011.07.16 00:13
그리고 위의 코드만 봤을 때 의존성이 있나요? 제가 봤을 땐 로컬에서 루프만 수행하고 전역 변수 등 별도의 전역 메모리를 이용하지 않는 것 같아서 각각 스레드의 의존성은 없을거 같아서요..
-
이상철
2011.07.16 01:19
흠흠.. 죄송합니다;;;;;; 링크 내용을 스크롤 하고 소스와 위의 질문만 봐서 착각했습니다.^^;;;
-
백창우
2011.07.16 15:20
그렇군요. 링크 부분을 모두 지나치셨군요.
저도 안볼려고 하다가 하도 소스가 이상해서 들어가봤습니다. ^^;
-
백창우
2011.07.16 00:29
링크 따라가보시면 질문자의 의도가 상철님께서 생각하시는것이 아니였음을 아시게 되실겁니다.
-
이경문
2011.07.17 18:15
안녕하세요, 이경문입니다. 에고~ 댓글이 많이 달려 있네요. ^^
이 문제에 제가 관심을 가지는 이유를 먼저 얘기를 해야 할 것 같군요. 저는 전직장에서 패킷 분석을 담당했었던 프로토콜 분석 팀장이었습니다. ISP 업체의 core망에 흐르는 네트워크 패킷을 분석해서 현황 파악(웹 트래픽이 얼마고 P2P 트래픽이 얼마고..)을 할 수 있도록 하는 것이 주 업무였습니다.
10Gbps이면 1초에 대략 100만개의 패킷(10Mpps)을 처리해야 합니다(하나의 패킷이 평균 600~1000 바이트). 예전까지는 CPU clock 속도가 계속 높아 져서 일괄 처리가 가능했었지만 시간이 가면 갈수록 하나의 CPU를 가지고는 분석하기가 어려워 졌습니다. 분석해야 하는 프로토콜도 많아 지고 패킷들도 많이 유입되기 때문이죠. 결국 높은 CPU clock 속도에 의존하기 보다는 multi threading을 통해서 패킷 분석을 하도록 (병렬로 처리하도록) 시스템이 바뀌어 갔죠.
가끔 가다 보면 급작스러운 패킷이 다량으로 발생되는 경우 패킷 손실이 나는 경우도 있는데, 이러한 경우가 발생하면 패킷 분실에 따라 분석이 어려워 져서 나중에 가면 왜 분석이 제대로 안되느냐고 제품이 납품된 회사로부터 클레임이 들어 오곤 했습니다. 아무튼 아무리 많은 패킷이 들어 와도 패킷 손실 없이 제대로 분석할 수 있도록 SW를 만드는 것이 중요한 관건이었습니다.
그 당시 납품이 되었던 장비는 NIC 카드 한장에서 수집되는 패킷을 2개의 thread에서 나누어서 처리를 하도록 되어 있습니다. 그러한 NIC 을 2장 꽂았으니, 패킷 분석 thread는 동시에 4개가 돌아 가도록 세팅이 되어 제풉이 납품되었습니다. 그런데 장비가 납품되고 나니 업체에서 질문이 들어 오더군요. 하나의 장비에 NIC을 2장만 꽂지 말고 몇장 더 꽂아서 처리하면 H/W 단가를 낮출 수 있지 않냐구요. 이 말도 일리가 있죠.
회사 내에서 실NIC 카드 한장당 10Gbps 트래픽을 분석하는 테스트를 해 보았습니다. NIC 1장 꽂으면 10Gbps를 충분히 처리합니다. NIC 2장 꽂으면 20Gbps도 잘 처리하구요. 그런데 3장 이상 꽂으니 30Gbps 분석을 제대로 하지 못하고 패킷 손실이 나기 시작하는 겁니다(당시 사용했던 장비의 CPU는 core가 8개였던 것으로 기억함). 결론적으로 하나의 장비에는 NIC 카드 2장을 꽂아서 사용하는 것이 제일 효율적이라는 귀납적 결과만 도출하고, 그 정확한 원인 분석은 하지를 못하였습니다.
간단하게 나타내 보자면 다음과 같이 되겠습니다.
NIC 1장 / 분석 thread 2개 / 10Gbps 처리 잘 함
NIC 2장 / 분석 thread 4개 / 20Gbps 처리 잘 함
NIC 3장 / 분석 thread 6개 / 30Gbps 처리 잘 못함
어찌 되었건 간에 정확한 원인을 알고 있는 것이, 나중에 이와 비슷한 시스템을 재설계할 때 도움이 될 것 같아서 질문을 올려 본 것입니다. Computer Architecture를 제대로 파악을 하자 못하니 정확한 원인 규명이 어렵네요.
질문의 요지는 "thread간의 자원의 동기화 코드(critical secion 혹은 mutex 등)도 없고, 자신의 thread를 blocking(I/O)시키는 부분도 없는 코드를 실행시켰을 때 thread 갯수가 늘어 날 때마다 수행속도가 늦어 지는 이유는 무엇일까?"입니다. 물론 thread 갯수는 CPU 갯수보다 작거나 같습니다. ^^
-
이경문
2011.07.17 18:26
아, 그리고 테스트를 해 보니 하나의 thread만을 돌렸을 때 해당 코드를 수행하는 CPU는 어떻게 될까 테스트를 해 봤는데, 테스트할 때마다 달라 지더군요. 하나의 CPU만 사용되기도 하고, 어떨 때에는 2개의 CPU에 나누어 실행되기도 하고... 아무래도 thread scheduling 정책에 따라, thread의 CPU 할당을 OS가 마음(?)대로 결정지어서가 아닐까 하는 생각만 듭니다. thread time slice 시간을 변경하면 context switching에 들어 가는 cost를 줄일 수 있을까 하는 생각도 해 보았는데, 이건 저 능력 밖의 일... -_-;
동영상으로 찍어 보려고 했었는데 동영상 찍는 프로그램(camtasia)자체도 CPU를 먹어 버려서 동영상으로 찍기가 쉽지는 않더군요. ^^
-
이경문
2011.07.17 21:05
아, Windows OS에서의 실행 단위는 process가 아니고 thread입니다. process는 CPU의 사용을 잡아 먹는 실행(execution)의 주체가 될 수 없습니다.
-
홍문화
2011.07.17 20:31
테스트하신 CPU가 하이퍼쓰레딩을 사용 한다면 코어가 하나라도 프로세서 사용률에서 두개의 코어가
있는것으로 보여집니다. (전자를 물리코어 후자를 논리코어라 함.)
리눅스에서는 쓰레드와 프로세스간의 근본적인 차이가 없습니다.
하지만 윈도우에서는 단일 프로세스 프로그램 하나를 실행 해도 프로세스 하나와 쓰레드 하나가 생성
되는 것으로 알고 있습니다. 이 두개의 문맥이 논리 프로세서 하나에서 모두 실행 되거나 두개의
논리 프로세서 각각에 분배되어 실행 된 것이 아닌가 추측해봅니다.
"하나의 thread만을 돌렸을 때 하나의 CPU만 사용되기도 하고, 어떨 때에는 2개의 CPU에 나누어 실행되기도 하고.." => 이 부분에 대한 답변이었습니다.
-
이경문
2011.07.17 20:00
제가 이미 회사를 퇴사해 버렸기 때문에, 회사 제품을 가지고는 테스트는 할 수가 없습니다.
제가 말씀 드린 것은(특정 thread의 CPU 할당이 제각각인 현상) 제가 만든 코드(위에 있는)에 대한 테스트를 말씀드린 겁니다. ^^
-
홍문화
2011.07.17 19:48
CPU 사용률이 어떻게 나왔는지 보셨는지 모르겠습니다.
일단 OS의 스케줄러가 다수의 쓰레드를 8개의 코어에 제대로 분배 했는지가 하나의 관건이 될것 같습니다.
Windows7 에서는 각각의 코어에 다수의 쓰레드를 공평하게 분배 해준다고 합니다.
(저는 아직도 XP를 쓰는지라... ㅋ)
더불어 CPU의 스펙을 제대로 파악 하셨는지 모르겠습니다.
비순차 명령어 처리를 지원 하는지, 슈퍼스칼라 파이프라인을 지원하는지, 하이퍼 쓰레딩을 지원하는지
캐시구조, 공유메모리(SDRAM) 구조등...
글 내용으로만 보아서는 엄청난 양의 네트워크 스트림을 실시간으로 처리 해야 했던것 같은데 L2, L3캐시,
SDRAM이 병목구간이 아니었나 생각해봅니다. 스트림 데이터의 특성상 L1 D캐시의 캐시미스율도
상당했을거라 생각합니다. 또한 하나의 SDRAM을 8개의 코어가 공유하는 구조였다면 이 또한 상당한
성능저하의 원인으로 작용 했을것입니다.
-
이경문
2011.07.17 22:04
관련 동영상 올려 봅니다. 하나의 스레드의 실행이 여러 CPU에서 돌아 가는 동영상입니다.
-
이경문
2011.07.18 15:11
위에서 김태훈님 왈 "운영체제 커널의 스레드 생성(Thread Creation)과 스케쥴링(Scheduling) 정책에 따라서 어느 CPU에 할당될지 모릅니다."라고 하셨죠. 제가 보여 드리려고 했던 것은 김태훈님 얘기의 확인 차원에서 동영상을 찍어 본 것입니다. 이상하다는 게아니고 당연히 정상적인 동작으로 볼 수가 있겠죠. ^^
저 현상(thread가 다른 CPU에 돌아 가며 할당되는 현상)을 보고 나서, time slice를 늘여 보거나 특정 thread를 특정 CPU에 할당을 할 수만 있다면 실험 결과는 어떻게 달라 질까를 고민해 봤습니다. 그런데 제 검색 능력이 모자란 건지, 아니면 원래 안되는 건지 잘 모르겠네요... ㅠㅠ -
홍문화
2011.07.17 23:28
글쎄요... 지극히 정상적인 동작이라고 생각되어집니다.
컨텍스트 스위칭에 의해 하나의 쓰레드가 두개의 물리 코어 사이를 왔다갔다 하는 것으로 판단됩니다.
테스트 프로그램을 실행하기 전에도 후에도 네개의 논리코어는 꾸준히 일을 하고 있다는 부분을 간과
하신것 같습니다.
-
이일열
2011.07.18 15:29
thread affinity로 검색하니 윈도우 API로는 이런 함수가 있네요... SetThreadAffinityMask
리눅스에도 이와같은 API가 존재할듯 하네요... 원하는 결과를 얻게 되었으면 좋겠네요...
-
이경문
2011.07.18 15:59
오, 좋은 정보 감사합니다. ^^
-
김남형
2011.07.19 23:55
man sched_setaffinity
or
man pthread_setaffinity_np
-
이경문
2011.08.03 18:39
네, 저도 SetThreadAffinityMask 로 이런 저런 테스트를 해 보았는데, 사용해 봤자 별 효용도 없고, 차라리 쓰지 않는게 더 낫다라는 결론을 내렸습니다. 근본 원인까지 파악할 수 있다면 좋겠지만, ... 쩝... ^^
-
하상은
2011.08.03 14:31
아주 예전에 SetThreadAffinityMask 를 이용한 테스트 결과를 블로깅 했었는데요..(개발내용으로 채우려다가 게을러서 때려치운 블로그인데다가 지금보니 너무 허접한 내용이 많아서 부끄럽군요) 당시에 저도 의존성이 없다고 생각되었던 코드를 CPU 의 코어갯수만큼 쓰레드를 할당하여 분할시켜 보았습니다. 그러다가 생각만큼 결과가 좋지 않아서 SetThreadAffinityMask 를 이용해서 CPU 코어 에 직접 특정 쓰레드들을 할당하도록 해보았는데요. 결과적으로는 OS가 자동으로 스케쥴링 할 때보다 오히려 더 좋지 않았습니다. 많은 이유가 있을수 있겠지만 당시에 코드를 어떻게 짰었는지 잘 기억이 안나고 이미 소스도 제 손을 떠난상태라 원인을 분석해볼수가 없네요..
-
이경문
2011.08.03 18:40
Core CPU의 갯수, Hyper Thread의 갯수, ... 등등 다양한 환경에서 테스트를 해 봐야, 좀 더 정확한 원인 규명을 할 수 있을 것 같아요.
-
하상은
2011.08.03 14:33
그러고보니 저도 당시에 캐시 문제로 인해 병목이 발생하고 자료를 어디에 분배할지 세심히 고려해야 한다는 결론을 얻고 결국엔 하드웨어빨로 해결했었네요.. :)
.
내용이 재미 있어서 블로그로 들어갔다가 게시판이 있길래 글을 따라 쭉쭉 갔는데 엄청난 경력과
실력의 소유자이심을 알아버렸습니다.ㅋ
윈도우 프로그램인거 같은데 실례가 안된다면 프로세서 사용률좀 캡쳐해서 올려주실 수 있으신가요?
답변을 드릴 수 있어서가 아니라 저도 같이 고민해보고 싶은 내용이라서요. ^^;
일단 최적화가 안되어서 그런지 어셈 코드를 보니 완전한 직렬성을 띠고 있는것으로 보여집니다.
i[0] = 0;
i[1] = i[0] + 1;
i[2] = i[1] + 1;
.
.
.
현재 연산의 결과가 바로 전 연산의 결과에 지속적으로 의존하고 있습니다.
즉, ILP 이득을 전혀 보지 못하고 있습니다.
일단 여기까지. 잘못된 내용은 다른 멤버님께서 지적 해주실겁니다. ㅋ