해당 함수를 lock-free 로 구현한 버젼이 있고, spin-lock 이나 mutex 로 구현한 버젼이 각각 있다. 어떤 함수가 더 빠를까? 게다가 쓰레드 수가 충분히 많다면? 그리고 C++ 로 구현한 lock-free 함수와 자바 언어로 구현한 함수를 비교하면?
아마도 많은 개발자들이 쉽게 아래와 같이 대답할 것 같다.
1) 병렬처리 기법 즉, Lock-Free를 이용한 함수가 더 빠를 것이다.
2) 쓰레드 수가 많아질수록 Lock-free 기법을 이용한 함수가 더 빠를 것이다.
3) 질문자 수준이 의심된다. 당연히 C언어 Lock-Free 가 월등히 빠를 것이다.
과연 그럴까?
결론부터 말하면 위의 세 개의 답은 모두 틀렸다.
아래는 테스트에 사용된 소스코드와 그 결과값이다.
물론, 아래 테스트 결과는 매우 제한된 조건에서만 유의미하다. 다만, 잘못된 상식을 벗어나나게 하는 용도로는 큰 효과가 있을 것이다.
include <Windows.h>
#include <stdio.h>
using namespace std;
CRITICAL_SECTION cs;
class SpinLock {
public:
SpinLock(int cntSpin) {
this->spin_lock = 0;
this->cntSpin = cntSpin;
}
void lock() {
int v;
while (::InterlockedCompareExchange(&spin_lock, (LONG)&v, 0) != 0) {
if (cntSpin <= 0) {
::Sleep(1);
}
else {
for (int i = 0; i < cntSpin; i ++) {
_asm nop;
}
}
}
}
void unlock() {
spin_lock = 0;
}
class Auto {
public:
Auto(SpinLock* spinLock) {
this->spinLock = spinLock;
spinLock->lock();
}
~Auto(){
spinLock->unlock();
}
private:
SpinLock* spinLock;
};
private:
volatile LONG spin_lock;
bool cntSpin;
};
static SpinLock gSlowSpinLock(0);
static SpinLock gFastSpinLock(1);
volatile LONG gSharedData[20] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, };
static int TEST_LOOP;
DWORD WINAPI test_slow_lock(LPVOID param) {
int cntTest = (int) param;
for (int i = 0; i < TEST_LOOP; i++) {
SpinLock::Auto lock(&gSlowSpinLock);
volatile LONG* p = (LONG*)gSharedData;
for (int j = 0; j < cntTest; j++) {
*p += 1;
p++;
}
}
return 0;
}
DWORD WINAPI test_fast_lock(LPVOID param) {
int cntTest = (int) param;
for (int i = 0; i < TEST_LOOP; i++) {
SpinLock::Auto lock(&gFastSpinLock);
volatile LONG* p = (LONG*)gSharedData;
for (int j = 0; j < cntTest; j++) {
*p += 1;
p++;
}
}
return 0;
}
DWORD WINAPI test_lock_free(LPVOID param) {
int cntTest = (int)param;
for (int i = 0; i < TEST_LOOP; i++) {
volatile LONG* p = gSharedData;
for (int j = 0; j < cntTest; j++) {
::InterlockedIncrement(p);
p++;
}
}
return 0;
}
DWORD WINAPI test_critical_section(LPVOID param) {
int cntTest = (int) param;
for (int i = 0; i < TEST_LOOP; i++) {
::EnterCriticalSection(&cs);
volatile LONG* p = (LONG*)gSharedData;
for (int j = 0; j < cntTest; j++) {
*p += 1;
p++;
}
::LeaveCriticalSection(&cs);
}
return 0;
}
static const int MAX_THREADS = 32;
int doLockTest(int cntThread, LPTHREAD_START_ROUTINE test_fn, int cntAtomicUpdate) {
HANDLE th[MAX_THREADS];
memset((void*)gSharedData, 0, cntAtomicUpdate*sizeof(int));
ULONGLONG t0 = ::GetTickCount64();
for (int i = 0; i < cntThread; ++i)
th[i] = CreateThread(NULL, NULL, test_fn, (void *)cntAtomicUpdate, NULL, NULL);
WaitForMultipleObjects(cntThread, th, TRUE, INFINITE);
ULONGLONG t1 = ::GetTickCount64();
for (int i = 0; i < cntAtomicUpdate; ++i) {
if (gSharedData[i] != TEST_LOOP * cntThread) {
printf("!!! Invalid test function");
exit(-1);
}
}
return (int)(t1 - t0);
}
int _tmain(int argc, _TCHAR* argv[])
{
::InitializeCriticalSection(&cs);
for (int cntThread = 1; cntThread <= MAX_THREADS; cntThread *= 2) {
TEST_LOOP = 12800 / cntThread * 1000;
printf("\n\n=========== Threads = %d, Test loop = %d*1000 ===============\n",
cntThread, TEST_LOOP/1000);
printf("atomic_update | lock_free | slow_spin | fast_spin | crit_sect \n");
for (int cntAtomicUpdate = 1; cntAtomicUpdate <= 5; cntAtomicUpdate ++) {
int t1 = doLockTest(cntThread, test_lock_free, cntAtomicUpdate);
int t2 = doLockTest(cntThread, test_slow_lock, cntAtomicUpdate);
int t3 = doLockTest(cntThread, test_fast_lock, cntAtomicUpdate);
int t4 = doLockTest(cntThread, test_critical_section, cntAtomicUpdate);
printf("% 7d % 14d % 11d % 11d % 11d\n", cntAtomicUpdate, t1, t2, t3, t4);
}
}
return 0;
}
#include <stdio.h>
using namespace std;
CRITICAL_SECTION cs;
class SpinLock {
public:
SpinLock(int cntSpin) {
this->spin_lock = 0;
this->cntSpin = cntSpin;
}
void lock() {
int v;
while (::InterlockedCompareExchange(&spin_lock, (LONG)&v, 0) != 0) {
if (cntSpin <= 0) {
::Sleep(1);
}
else {
for (int i = 0; i < cntSpin; i ++) {
_asm nop;
}
}
}
}
void unlock() {
spin_lock = 0;
}
class Auto {
public:
Auto(SpinLock* spinLock) {
this->spinLock = spinLock;
spinLock->lock();
}
~Auto(){
spinLock->unlock();
}
private:
SpinLock* spinLock;
};
private:
volatile LONG spin_lock;
bool cntSpin;
};
static SpinLock gSlowSpinLock(0);
static SpinLock gFastSpinLock(1);
volatile LONG gSharedData[20] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, };
static int TEST_LOOP;
DWORD WINAPI test_slow_lock(LPVOID param) {
int cntTest = (int) param;
for (int i = 0; i < TEST_LOOP; i++) {
SpinLock::Auto lock(&gSlowSpinLock);
volatile LONG* p = (LONG*)gSharedData;
for (int j = 0; j < cntTest; j++) {
*p += 1;
p++;
}
}
return 0;
}
DWORD WINAPI test_fast_lock(LPVOID param) {
int cntTest = (int) param;
for (int i = 0; i < TEST_LOOP; i++) {
SpinLock::Auto lock(&gFastSpinLock);
volatile LONG* p = (LONG*)gSharedData;
for (int j = 0; j < cntTest; j++) {
*p += 1;
p++;
}
}
return 0;
}
DWORD WINAPI test_lock_free(LPVOID param) {
int cntTest = (int)param;
for (int i = 0; i < TEST_LOOP; i++) {
volatile LONG* p = gSharedData;
for (int j = 0; j < cntTest; j++) {
::InterlockedIncrement(p);
p++;
}
}
return 0;
}
DWORD WINAPI test_critical_section(LPVOID param) {
int cntTest = (int) param;
for (int i = 0; i < TEST_LOOP; i++) {
::EnterCriticalSection(&cs);
volatile LONG* p = (LONG*)gSharedData;
for (int j = 0; j < cntTest; j++) {
*p += 1;
p++;
}
::LeaveCriticalSection(&cs);
}
return 0;
}
static const int MAX_THREADS = 32;
int doLockTest(int cntThread, LPTHREAD_START_ROUTINE test_fn, int cntAtomicUpdate) {
HANDLE th[MAX_THREADS];
memset((void*)gSharedData, 0, cntAtomicUpdate*sizeof(int));
ULONGLONG t0 = ::GetTickCount64();
for (int i = 0; i < cntThread; ++i)
th[i] = CreateThread(NULL, NULL, test_fn, (void *)cntAtomicUpdate, NULL, NULL);
WaitForMultipleObjects(cntThread, th, TRUE, INFINITE);
ULONGLONG t1 = ::GetTickCount64();
for (int i = 0; i < cntAtomicUpdate; ++i) {
if (gSharedData[i] != TEST_LOOP * cntThread) {
printf("!!! Invalid test function");
exit(-1);
}
}
return (int)(t1 - t0);
}
int _tmain(int argc, _TCHAR* argv[])
{
::InitializeCriticalSection(&cs);
for (int cntThread = 1; cntThread <= MAX_THREADS; cntThread *= 2) {
TEST_LOOP = 12800 / cntThread * 1000;
printf("\n\n=========== Threads = %d, Test loop = %d*1000 ===============\n",
cntThread, TEST_LOOP/1000);
printf("atomic_update | lock_free | slow_spin | fast_spin | crit_sect \n");
for (int cntAtomicUpdate = 1; cntAtomicUpdate <= 5; cntAtomicUpdate ++) {
int t1 = doLockTest(cntThread, test_lock_free, cntAtomicUpdate);
int t2 = doLockTest(cntThread, test_slow_lock, cntAtomicUpdate);
int t3 = doLockTest(cntThread, test_fast_lock, cntAtomicUpdate);
int t4 = doLockTest(cntThread, test_critical_section, cntAtomicUpdate);
printf("% 7d % 14d % 11d % 11d % 11d\n", cntAtomicUpdate, t1, t2, t3, t4);
}
}
return 0;
}
아래가 위 소스코드를 실행한 결과이다.
=========== Threads = 1, Test loop = 12800*1000 ===============
atomic_update | lock_free | slow_spin | fast_spin | crit_sect
1 109 125 125 281
2 203 124 141 265
3 296 141 140 297
4 374 140 141 296
5 453 156 140 296
=========== Threads = 2, Test loop = 6400*1000 ===============
atomic_update | lock_free | slow_spin | fast_spin | crit_sect
1 359 125 452 749
2 609 140 437 733
3 827 140 531 592
4 1061 141 499 702
5 1139 140 936 686
=========== Threads = 4, Test loop = 3200*1000 ===============
atomic_update | lock_free | slow_spin | fast_spin | crit_sect
1 344 124 1014 874
2 608 141 1014 874
3 873 141 1076 593
4 1107 141 1045 874
5 1060 141 1310 624
=========== Threads = 8, Test loop = 1600*1000 ===============
atomic_update | lock_free | slow_spin | fast_spin | crit_sect
1 312 125 1747 874
2 546 125 1778 952
3 795 141 1778 593
4 1092 140 1794 1045
5 1092 156 1935 593
=========== Threads = 16, Test loop = 800*1000 ===============
atomic_update | lock_free | slow_spin | fast_spin | crit_sect
1 296 140 3573 920
2 531 140 2793 936
3 811 140 2808 593
4 1076 141 3167 1060
5 1092 156 3339 593
=========== Threads = 32, Test loop = 400*1000 ===============
atomic_update | lock_free | slow_spin | fast_spin | crit_sect
1 296 156 5881 921
2 530 156 4945 952
3 811 156 4883 577
4 1092 156 6209 998
5 1108 172 7004 593
아마 위 결과를 보고 믿지 못하는 분도 계실지 모르겠다. 그런 경우 소스코드를 직접 실행해 보시기 바란다.
보다시피 쓰레드 수가 2 이상인 경우, 단 한 개의 변수를 변경할 때도 SlowSpinLock 은 다른 모든 방식보다 보다 2~3배 이상 빠르다. (첨고로, FastSpinLock 은 다른 모든 방식보다 매우 큰 차이로 느리다.)
이러한 차이는 SlowSpinLock 이 매우 효과적으로 쓰레드간 경쟁상태를 최소화하기 때문에 발생하는 것으로 판단된다. CriticalSection은 FastSpinLock을 일부 포함하고 있어서 어정쩡한 결과가 나오는 것이라면, SlowSpinLock 과 유사하게 구현된 java synchronized 는 어떤 결과가 나올까?
아래가 그 결과이다. Windows 용 JDK 1.6.3 에서 테스트한 것이다.
놀랍게도 Thread 수에 관계없이 atomic 하게 변경하는 변수가 2 또는 3개 이상이면 java synchronized 가 C++ 로 구현한 Lock-free 코드보다 더 빨라지기 시작한다.
=========== Threads = 1, Test loop = 12800*1000 ===============
atomic_update = 1 : java_synchronized = 181
atomic_update = 2 : java_synchronized = 181
atomic_update = 3 : java_synchronized = 179
atomic_update = 4 : java_synchronized = 180
atomic_update = 5 : java_synchronized = 184
=========== Threads = 2, Test loop = 6400*1000 ===============
atomic_update = 1 : java_synchronized = 414
atomic_update = 2 : java_synchronized = 572
atomic_update = 3 : java_synchronized = 482
atomic_update = 4 : java_synchronized = 472
atomic_update = 5 : java_synchronized = 426
=========== Threads = 4, Test loop = 3200*1000 ===============
atomic_update = 1 : java_synchronized = 576
atomic_update = 2 : java_synchronized = 555
atomic_update = 3 : java_synchronized = 559
atomic_update = 4 : java_synchronized = 490
atomic_update = 5 : java_synchronized = 513
=========== Threads = 8, Test loop = 1600*1000 ===============
atomic_update = 1 : java_synchronized = 493
atomic_update = 2 : java_synchronized = 501
atomic_update = 3 : java_synchronized = 586
atomic_update = 4 : java_synchronized = 526
atomic_update = 5 : java_synchronized = 530
=========== Threads = 16, Test loop = 800*1000 ===============
atomic_update = 1 : java_synchronized = 530
atomic_update = 2 : java_synchronized = 538
atomic_update = 3 : java_synchronized = 524
atomic_update = 4 : java_synchronized = 522
atomic_update = 5 : java_synchronized = 569
=========== Threads = 32, Test loop = 400*1000 ===============
atomic_update = 1 : java_synchronized = 574
atomic_update = 2 : java_synchronized = 571
atomic_update = 3 : java_synchronized = 563
atomic_update = 4 : java_synchronized = 557
atomic_update = 5 : java_synchronized = 568
과연 자바는 최강의 멀티쓰레드 플랫폼이라 불릴만하다.
참고로, pthread_mutex 를 사용하면 CriticalSection 보다 2~3배 느려진다. Linux에서 Java 버젼도 이런 영향을 받아선지 성능이 뚝 떨어지지만 그래도 pthread_mutex 보다는 2배 이상 빠르다.
결론은 멀티쓰레드 코딩 방식에는 절대 강자는 없으며, 병렬컴퓨팅이 명확히 필요한 경우가 아니라면 lock-free 에 집착할 필요가 없다는 것이다. 위 테스트 결과처럼, 때로는 매우 단순한 CAS 함수와 Sleep 함수 하나 만으로도 매우 탁월한 퍼포먼스를 얻어낼 수 있다. Thread 간 충돌이 빈번히 발생하는 상황에서 Thread 블로킹을 최소화하려면 Lock-Free가 필요하지만 오히려 Lock을 적절히 사용하여 Thread 간 충돌을 최소화하여 퍼포먼스를 더 높일 수 있는 방법도 존재하는 것이다. 즉, 때로는 Lock-free 가, 때로는 FastSpinLock 이, 때로는 CriticalSection 이 가장 빠를 때가 있으며, 그때 그때 상황에 맞춰 해결하여야 한다. 다만, 자바 프로그래머라면 이런 고민에서 완전히 해방감을 느껴도 무방하다. ^^
누군가 C++11 의 mutex 관련 테스트자료도 올려주시면 고맙겠는데...
참고로, 아래에 자바 소스 코드도 첨부합니다.
누군가 C++11 의 mutex 관련 테스트자료도 올려주시면 고맙겠는데...
참고로, 아래에 자바 소스 코드도 첨부합니다.
public class TestMain {
static int gSharedData[] = new int[20];
static int TEST_LOOP;
static class JSync extends Thread {
JSync(int c) {
this.cntTest = c;
start();
}
public void run() {
for (int i = 0; i < TEST_LOOP; i++) {
synchronized (gSharedData) {
for (int j = 0; j < cntTest; j++) {
gSharedData[j] ++;
}
}
}
}
int cntTest;
}
static final int MAX_THREADS = 32;
static int doLockTest(int cntThread, int cntAtomicUpdate, boolean testSync) throws InterruptedException {
Thread th[] = new Thread[MAX_THREADS];
for (int i = 0; i < cntAtomicUpdate; i ++) {
gSharedData[i] = 0;
}
long t0 = System.currentTimeMillis();
for (int i = 0; i < cntThread; ++i) {
th[i] = new JSync(cntAtomicUpdate);
}
for (int i = 0; i < cntThread; ++i) {
th[i].join();
}
long t1 = System.currentTimeMillis();
for (int i = 0; i < cntAtomicUpdate; ++i) {
if (gSharedData[i] != TEST_LOOP * cntThread) {
System.out.print("!!! Invalid test function");
System.exit(-1);
}
}
return (int)(t1 - t0);
}
public static void main(String[] args) {
try {
for (int cntThread = 1; cntThread <= MAX_THREADS; cntThread *= 2) {
TEST_LOOP = 12800 / cntThread * 1000;
System.out.print("\n\n=========== Threads = " + cntThread + ", Test loop = " + TEST_LOOP/1000 + "*1000 ===============\n");
doLockTest(cntThread, 1, true);
for (int cntAtomicUpdate = 1; cntAtomicUpdate <= 5; cntAtomicUpdate += 1) {
int t1 = doLockTest(cntThread, cntAtomicUpdate, true);
System.out.print("atomic_update = " + cntAtomicUpdate
+ " : java_synchronized = " + t1 + "\n");
}
}
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
static int gSharedData[] = new int[20];
static int TEST_LOOP;
static class JSync extends Thread {
JSync(int c) {
this.cntTest = c;
start();
}
public void run() {
for (int i = 0; i < TEST_LOOP; i++) {
synchronized (gSharedData) {
for (int j = 0; j < cntTest; j++) {
gSharedData[j] ++;
}
}
}
}
int cntTest;
}
static final int MAX_THREADS = 32;
static int doLockTest(int cntThread, int cntAtomicUpdate, boolean testSync) throws InterruptedException {
Thread th[] = new Thread[MAX_THREADS];
for (int i = 0; i < cntAtomicUpdate; i ++) {
gSharedData[i] = 0;
}
long t0 = System.currentTimeMillis();
for (int i = 0; i < cntThread; ++i) {
th[i] = new JSync(cntAtomicUpdate);
}
for (int i = 0; i < cntThread; ++i) {
th[i].join();
}
long t1 = System.currentTimeMillis();
for (int i = 0; i < cntAtomicUpdate; ++i) {
if (gSharedData[i] != TEST_LOOP * cntThread) {
System.out.print("!!! Invalid test function");
System.exit(-1);
}
}
return (int)(t1 - t0);
}
public static void main(String[] args) {
try {
for (int cntThread = 1; cntThread <= MAX_THREADS; cntThread *= 2) {
TEST_LOOP = 12800 / cntThread * 1000;
System.out.print("\n\n=========== Threads = " + cntThread + ", Test loop = " + TEST_LOOP/1000 + "*1000 ===============\n");
doLockTest(cntThread, 1, true);
for (int cntAtomicUpdate = 1; cntAtomicUpdate <= 5; cntAtomicUpdate += 1) {
int t1 = doLockTest(cntThread, cntAtomicUpdate, true);
System.out.print("atomic_update = " + cntAtomicUpdate
+ " : java_synchronized = " + t1 + "\n");
}
}
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
댓글 없음:
댓글 쓰기