pthread를 이용한 병렬처리 시 속도 저하 문제
글쓴이: Hoewon Kim@Google / 작성시간: 목, 2017/08/10 - 1:46오전
pthread를 이용한 병렬처리에 대하여 공부하고 있습니다.
아래 소스코드와 같이 서로 다른 4개의 메모리에 대한 동일한 처리를 4개의 thread를 생성하여 처리하고, 4번에 걸쳐 순차적으로 처리한 결과를 비교해 보았습니다.
#include <stdio.h> #include <iostream> #include <pthread.h> #include <time.h> #include <cpuid.h> #define MULTITHREAD_NUM 4 #define SIZE_OF_DATA 4000*3000 #define SIZE_OF_ONETHR SIZE_OF_DATA/MULTITHREAD_NUM #define CPUID(INFO, LEAF, SUBLEAF) __cpuid_count(LEAF, SUBLEAF, INFO[0], INFO[1], INFO[2], INFO[3]) #define GETCPU(CPU) { \ uint32_t CPUInfo[4]; \ CPUID(CPUInfo, 1, 0); \ /* CPUInfo[1] is EBX, bits 24-31 are APIC ID */ \ if ( (CPUInfo[3] & (1 << 9)) == 0) { \ CPU = -1; /* no APIC on chip */ \ } \ else { \ CPU = (unsigned)CPUInfo[1] >> 24; \ } \ if (CPU < 0) CPU = 0; \ } struct threadwork_arg { unsigned char* src; unsigned char* dst; unsigned char* gain; int i_start; int i_end; int thread_id; }; void* test_func(void* arg) { clock_t begin_t, end_t; threadwork_arg* param = (threadwork_arg*)arg; int i; unsigned char* src = param->src; unsigned char* dst = param->dst; unsigned char* gain = param->gain; int i_start = param->i_start; int i_end = param->i_end; int loop_size = i_end - i_start; int cnt = 0; int cpu_id; unsigned int temp_data_dst; unsigned int temp_data_src; unsigned int temp_data_gain; GETCPU(cpu_id); begin_t = clock(); printf("thread%d start %lu\n", param->thread_id, begin_t); for(i=0; i<loop_size; i++) { temp_data_src = (unsigned int)*src; temp_data_gain = (unsigned int)*gain; temp_data_dst = temp_data_src*temp_data_gain>>8; *dst = (unsigned char)(temp_data_dst&0xFF); dst++;src++;gain++;cnt++; } end_t = clock(); printf("thread%d end %lu\n", param->thread_id, end_t); printf("thread%d %d loop processing time %4.5fms cpu[%d]\n", param->thread_id, cnt, (double)(end_t - begin_t)*1000/CLOCKS_PER_SEC, cpu_id); return arg; } int main(int argc, const char * argv[]) { clock_t begin_t, end_t; threadwork_arg thread_param[MULTITHREAD_NUM]; pthread_t threadwork_hello[MULTITHREAD_NUM]; int idx = 0; int thr_id[MULTITHREAD_NUM]; int status[MULTITHREAD_NUM]; unsigned char* src_data[MULTITHREAD_NUM]; unsigned char* dst_data[MULTITHREAD_NUM]; unsigned char* gain_data[MULTITHREAD_NUM]; int cpu_id; GETCPU(cpu_id); printf("%s cpu[%d]\n", __func__, cpu_id); for(int i=0; i<MULTITHREAD_NUM; i++) { src_data[i] = new unsigned char[SIZE_OF_DATA]; dst_data[i] = new unsigned char[SIZE_OF_DATA]; gain_data[i] = new unsigned char[SIZE_OF_DATA]; memset(src_data[i], 64, SIZE_OF_DATA); memset(dst_data[i], 0, SIZE_OF_DATA); memset(gain_data[i], 128, SIZE_OF_DATA); } begin_t = clock(); printf("whole loop start %lu\n", begin_t); for(idx=0; idx<MULTITHREAD_NUM; idx++) { thread_param[idx].src = src_data[idx]; thread_param[idx].dst = dst_data[idx]; thread_param[idx].gain = gain_data[idx]; thread_param[idx].i_start = 0; thread_param[idx].i_end = thread_param[idx].i_start + SIZE_OF_ONETHR; thread_param[idx].thread_id = idx; } for(idx=0; idx<MULTITHREAD_NUM; idx++) { GETCPU(cpu_id); printf("%s create thread%d cpu[%d]\n", __func__, idx, cpu_id); thr_id[idx] = pthread_create(&threadwork_hello[idx], NULL, test_func, (void*)&thread_param[idx]); if (thr_id[idx] < 0) { perror("thread create error : "); exit(0); } } for(idx=0; idx<MULTITHREAD_NUM; idx++) { pthread_join(threadwork_hello[idx], (void **)&status); } end_t = clock(); printf("whole loop end %lu\n", end_t); printf("whole loop time %4.5fms\n", (double)(end_t - begin_t)*1000/CLOCKS_PER_SEC); begin_t = clock(); printf("no thread loop start %lu\n", begin_t); for(idx=0; idx<MULTITHREAD_NUM; idx++) { thread_param[idx].thread_id = idx - MULTITHREAD_NUM; test_func((void*)&thread_param[idx]); } end_t = clock(); printf("no thread loop end %lu\n", end_t); printf("no thread loop time %4.5fms\n", (double)(end_t - begin_t)*1000/CLOCKS_PER_SEC); for(int i=0; i<MULTITHREAD_NUM; i++) { delete[] src_data[i]; delete[] dst_data[i]; delete[] gain_data[i]; } return 0; }
실행 결과
main cpu[4] whole loop start 97671 main create thread0 cpu[4] main create thread1 cpu[4] thread0 start 97736 main create thread2 cpu[4] thread1 start 97761 main create thread3 cpu[4] thread2 start 97927 thread3 start 98037 thread2 end 104471 thread2 3000000 loop processing time 6.54400ms cpu[5] thread0 end 107325 thread0 3000000 loop processing time 9.58900ms cpu[0] thread3 end 108334 thread3 3000000 loop processing time 10.29700ms cpu[7] thread1 end 108485 thread1 3000000 loop processing time 10.72400ms cpu[3] whole loop end 108530 whole loop time 10.85900ms no thread loop start 108538 thread-4 start 108539 thread-4 end 109466 thread-4 3000000 loop processing time 0.92700ms cpu[2] thread-3 start 109479 thread-3 end 110503 thread-3 3000000 loop processing time 1.02400ms cpu[6] thread-2 start 110517 thread-2 end 111441 thread-2 3000000 loop processing time 0.92400ms cpu[6] thread-1 start 111451 thread-1 end 112356 thread-1 3000000 loop processing time 0.90500ms cpu[6] no thread loop end 112370 no thread loop time 3.83200ms
단순히 생각하기에는 각각의 thread가 서로 다른 core에서 실행되었으니 적어도 각 thread에 할당된 test_func의 동작 시간 만큼은 thread를 생성하지 않고 처리했을 때의 test_func의 동작 시간과 동일한 수준이어야 할 것 같습니다.
그러나 실행 결과를 보면 각 thread에서 동작한 test_func의 동작 시간이 상대적으로 매우 느립니다. 그 이유가 무엇인지 도저히 감이 오지 않네요...
Forums:
우선 pthread_create()을 시작하는데
우선 pthread_create()을 시작하는데 상당한 시간이 소요됩니다. 시간 측정 시작을 pthread_join() 이전으로 옮긴다면 보다 공평한(?) 시간 비교가 가능할 것 같고요.
쓰레드 내부에서 printf()와 같은 출력 함수는 정밀한 시간 측정을 방해할 수 있으니 참고하세요.
printf나 create의 오버헤드는 감안하고
printf나 create의 오버헤드는 감안하고 있습니다. 제가 신경 쓰이는 부분은 test_func() 가 동작하는 시간이 너무 심하게 다르다는 점입니다. printf의 오버헤드를 감안하더라도 10ms와 1ms의 차이는 너무한 것 같아서요...
위에 하신 것을 보면 test_func() 만
위에 하신 것을 보면 test_func() 만 차이점을 보도록 되어 있지 않습니다. 중간에 create(), join() 함수가 끼어들고 있습니다. 이것은 영향을 제거하고 난 후 비로서 test_func()의 실행 시간 차이를 비교할 수 있을 것입니다.
test_func 내부에도 시간 측정하고 있습니다.
test_func 내부에도 시간 측정하고 있습니다.
음. 제가 잘못 보았었네요.
음. 제가 잘못 보았었네요.
그럼 한번 순서를 바꾸어서 해보시죠? no thread test 다음에 whole loop 테스트를요.
캐쉬 효과도 좀 있을 것 같은 상황입니다.
감사합니다. 시도해 봐야겠군요!
감사합니다. 시도해 봐야겠군요!
함수 측정 시간
소스를 보고 시간이 저렇게 큰 갭으로 나오는 이유가 도무지 짐작이 안되어
무려!!! 2시간 이상을 투자해서 살펴 보았습니다(ㅜㅜ 업무시간인데..)
갭이 너무너무 큰 이유는 시간 측정 미스로 보입니다.
전 이때까지 함수 시간 측정 할 때 gettimeofday 를 사용했는데..
clock(); 은 처음 봅니다?
검색해보니 함수 측정할 때 clock 을 많이 사용한다고 했는데
오픈소스를 그렇게 많이 아는 것은 아니지만
clock 으로 시간 측정 하는 놈은 보질 못했네요
https://github.com/tomxuetoy/Linux_calculate_time/blob/master/calculate_time.c
그외로
struct itimerval e1, e2;
double elapsed;
setitimer(ITIMER_REAL, &(struct itimerval){{1000,0},{1000,0}}, NULL);
getitimer(ITIMER_REAL, &e1);
getitimer(ITIMER_REAL, &e2);
elapsed = (e1.it_value.tv_sec - e2.it_value.tv_sec) + ((e1.it_value.tv_usec - e2.it_value.tv_usec) / 1000000.0);
이렇게도 사용합니다.
https://github.com/Seagate/SMR_FS-EXT4/blob/master/hdparm/hdparm.c
(hdparm 프로그램 시간 측정 방법 입니다! 명성이 있으니 믿을만 하죠?)
그래도 시간 차이가 나는건 좀더 스터디를 해봐야 할 것 같네요..
main cpu[3]
whole loop start 64508
main create thread0 cpu[10]
main create thread1 cpu[10]
thread0 start 66223
main create thread2 cpu[10]
thread1 start 68315
main create thread3 cpu[10]
thread2 start 71631
thread3 start 79648
thread0 end 128511
thread0 3000000 loop processing time 62.28800ms cpu[3]
thread0 3000000 running time : (gettimeofday)17860us, (getitimer)12.90500ms
thread1 end 130594
thread1 3000000 loop processing time 62.27900ms cpu[5]
thread1 3000000 running time : (gettimeofday)16860us, (getitimer)13.52900ms
thread2 end 139556
thread2 3000000 loop processing time 67.92500ms cpu[0]
thread2 3000000 running time : (gettimeofday)18109us, (getitimer)16.44400ms
thread3 end 143490
thread3 3000000 loop processing time 63.84200ms cpu[13]
thread3 3000000 running time : (gettimeofday)19399us, (getitimer)19.40100ms
whole loop end 143540
whole loop time 79.03200ms
no thread loop start 143558
thread-4 start 143563
thread-4 end 161954
thread-4 3000000 loop processing time 18.39100ms cpu[10]
thread-4 3000000 running time : (gettimeofday)18401us, (getitimer)18.40100ms
thread-3 start 161966
thread-3 end 175299
thread-3 3000000 loop processing time 13.33300ms cpu[10]
thread-3 3000000 running time : (gettimeofday)13335us, (getitimer)13.33500ms
thread-2 start 175312
thread-2 end 188756
thread-2 3000000 loop processing time 13.44400ms cpu[10]
thread-2 3000000 running time : (gettimeofday)13446us, (getitimer)13.44600ms
thread-1 start 188768
thread-1 end 201802
thread-1 3000000 loop processing time 13.03400ms cpu[10]
thread-1 3000000 running time : (gettimeofday)13060us, (getitimer)13.06000ms
no thread loop end 201812
no thread loop time 58.25400ms
아...그렇군요... 소중한 시간 투자해 주셔서
아...그렇군요... 소중한 시간 투자해 주셔서 감사합니다ㅠㅠ
좀더 살펴보니...
쓰레드에서 호출하는 함수가 시간이 많이 걸리는 이유는
쓰레드 개수를 확 늘리니 체감이 오는데요
#define MULTITHREAD_NUM 30
#define SIZE_OF_DATA 1024*1024*10
#define SIZE_OF_ONETHR SIZE_OF_DATA
이렇게 한번 수행해 보세요~ 병렬이 얼마나 빠른지 바로 알 수 있네요..
다음과 같이 추측 됩니다.
쓰레드로 돌리면 수행하는 시점에 동시에 30*30MB access 가 일어나고
싱글로 돌리면 수행하는 시점에 동시에 30MB access 가 시차적으로 30번 일어 납니다.
메모리와 CPU 간에 병렬이 아니니 30*30MB 가 직렬로 전송되므로
시간이 걸리는 쓰레드안 테스트 함수는 메모리 제어권을 기다림 인한 것 아닐까 싶네요
하지만 총 전송량 대비 수행 시간을 보면
쓰레드로 돌린것은 2초에 끝났고
싱글로 돌린것은 15초만에 끝났습니다.
(초 시간은 단순하게 어림 잡아 예로 든것입니다.. ㅎㅎ)
말그대로 create 이후 join 다음 시간을 보시면 될 것 같네요...
(모두 추측임... ㅎㅎ)
아... 그럴 수도 있을 것 같네요. 앞으로
아... 그럴 수도 있을 것 같네요. 앞으로 multi-threading 설계를 하는데에 참고가 될 것 같습니다. 감사합니다!
제 경험으로는 프로세스'가 빠릅니다.
예전에 윈도우에서 해봤었는데.
스레드가 반복하는게 느리더라구요. ㅇ_ㅇ;;
----------------------------------------------------------------------------
젊음'은 모든것을 가능하게 만든다.
매일 1억명이 사용하는 프로그램을 함께 만들어보고 싶습니다.
정규 근로 시간을 지키는. 야근 없는 회사와 거래합니다.
각 분야별. 좋은 책'이나 사이트' 블로그' 링크 소개 받습니다. shintx@naver.com
저도 프로세스를 써야 하나... 근데 프로세스를
저도 프로세스를 써야 하나... 근데 프로세스를 쓴다고 나아질까... 하는 생각을 했었는데, 말씀 들어보니 시도해 봐야겠네요. 김사합니다!
댓글 달기