본문 바로가기

CS/멀티쓰레딩

왜 유저모드 동기화를 쓰라고 함?

개요

멀티스레드 프로그래밍을 공부하다보면 나오는 말이 있습니다.

"커널모드 동기화 객체(Mutex 등)는 비용이 크기 때문에 유저모드 동기화 객체를 사용하자" 라는 것입니다.

 

실제로 측정해보면 커널 동기화 객체인 Mutex와 유저 동기화 객체인 CriticalSection은 거의 10배 가까운 속도의 차이를 보입니다. (아래 포스팅의 예시 기준)

https://basaeng.tistory.com/86

 

[서버 이것저것] 로직 성능 측정해보기

개요코드(로직)의 성능을 측정하는 것이 개발에서 중요하다는 것은 개발을 배우는 모든 사람이 알고있습니다.다만 성능을 어떻게 측정하지? 또는 시간은 측정할 수 있는데 그 다음에는 어떻게

basaeng.tistory.com

 

왜 CriticalSection과 Mutex는 이렇게 큰 속도의 차이가 있을까요?

그러면 아마도 CriticalSection은 lock이 유저영역에 존재하고, 유저영역에서 lock을 획득 반환하기 때문에 Mutex에 비해 빠르다고 생각할 것입니다.

  

그런데 이런 생각해본적 없나요??

CriticalSection을 통해 lock을 획득하지 못한다면, Mutex와 똑같이 thread가 Block상태에 빠져 커널모드로 전환되는데, 그렇다면 lock을 빠르게 획득하고 반환하는 상황에서는 큰 차이가 없는거 아닌가 하는 생각이요.

 

또한 lock이 유저모드에 존재한다면, 이를 배타적으로 소유하기 위해서는 또 다른 장치가 필요할 것일텐데 어떻게 할까요? lock을 소유하기 위해 다른 lock을 사용한다면 같은 문제에 빠지겠죠

 

오늘은 이에대해 간단하게 알아보겠습니다.


CriticalSection의 SpinLock

CriticalSection은 유저영역에 존재하는 lock을 획득하기 위해 SpinLock을 사용합니다!

락프리까지 알고있다면 락프리의 형태와도 유사함을 알 수 있습니다.

7737F192 E8 59 4F FB FF       call        _RtlpSpinForCriticalSection@12 (773340F0h)

해당 코드 안에서 SpinLock을 통해 lock을 획득합니다. (EnterCriticalSection 내부의 어셈블리 명령 확인)

 

이 때 lock획득 전략은 3가지로 나뉘어집니다.


1. 처음에 먼저 락 획득 시도

먼저 락 획득을 시도해봅니다. 획득하지 못했다면 조건에 따라 다음 스텝으로 넘어갑니다.

77334104 8B 51 04             mov         edx,dword ptr [ecx+4]  
77334107 57                   push        edi  
77334108 8D 79 04             lea         edi,[ecx+4]  
7733410B 8D 04 45 01 00 00 00 lea         eax,[eax*2+1]  
77334112 89 45 F8             mov         dword ptr [ebp-8],eax  
77334115 F6 C2 01             test        dl,1  
77334118 74 19                je          _RtlpSpinForCriticalSection@12+43h (77334133h)  
7733411A 8B C8                mov         ecx,eax  
7733411C 8B C2                mov         eax,edx  
7733411E 33 CA                xor         ecx,edx  
77334120 F0 0F B1 0F          lock cmpxchg dword ptr [edi],ecx

 


2. monitor를 활용한 락 획득 시도

획득하려는 자원이 속한 캐시라인의 변화를 감지해 변화되었을 때만 락을 잡도록 시도합니다.

CPU가 멀티코어가 아니라면 의미없는 행동이므로 하지 않습니다. 또한 CPU자체에 monitor명령이 가능한지도 확인합니다.

7733416E 0F 01 FA             monitorx    eax,ecx,edx  
77334171 8B 17                mov         edx,dword ptr [edi]  
77334173 F6 C2 01             test        dl,1  
77334176 74 0F                je          _RtlpSpinForCriticalSection@12+97h (77334187h)  
77334178 8B 4D F8             mov         ecx,dword ptr [ebp-8]  
7733417B 8B C2                mov         eax,edx  
7733417D 33 CA                xor         ecx,edx  
7733417F F0 0F B1 0F          lock cmpxchg dword ptr [edi],ecx  
77334183 3B C2                cmp         eax,edx  
77334185 74 A1                je          _RtlpSpinForCriticalSection@12+38h (77334128h)  
77334187 8B 4D FC             mov         ecx,dword ptr [ebp-4]  
7733418A 0F 31                rdtsc  
7733418C 89 75 F4             mov         dword ptr [ebp-0Ch],esi  
7733418F 8B F0                mov         esi,eax  
77334191 8B C2                mov         eax,edx  
77334193 89 45 FC             mov         dword ptr [ebp-4],eax  
77334196 3B C1                cmp         eax,ecx  
77334198 72 59                jb          _RtlpSpinForCriticalSection@12+103h (773341F3h)  
7733419A 77 05                ja          _RtlpSpinForCriticalSection@12+0B1h (773341A1h)  
7733419C 3B 75 F4             cmp         esi,dword ptr [ebp-0Ch]  
7733419F 72 52                jb          _RtlpSpinForCriticalSection@12+103h (773341F3h)  
773341A1 3B 45 F0             cmp         eax,dword ptr [ebp-10h]  
773341A4 77 4D                ja          _RtlpSpinForCriticalSection@12+103h (773341F3h)  
773341A6 72 04                jb          _RtlpSpinForCriticalSection@12+0BCh (773341ACh)  
773341A8 3B F3                cmp         esi,ebx  
773341AA 73 47                jae         _RtlpSpinForCriticalSection@12+103h (773341F3h)  
773341AC 2B DE                sub         ebx,esi  
773341AE B9 02 00 00 00       mov         ecx,2  
773341B3 33 C0                xor         eax,eax  
773341B5 0F 01 FB             mwaitx      eax,ecx,ebx

CPU자원을 직접 반환하지는 않지만 마치 퀀텀을 모두 소모한 스레드의 상태처럼 언제든 다른 스레드로 교체될 수 있습니다.

 

여기서 언급되는 monitor는 동기화 시 사용되는 monitor와는 다르다는 것에 유의해야 합니다.

아래 링크에서 monitor명령어 설명에 대해 볼 수 있습니다. 저의 경우 AMD CPU를 사용하기 때문에 AMD문서를 첨부했습니다. 

https://docs.amd.com/v/u/en-US/24594_3.37

 

AMD64 Architecture Programmer's Manual Volume 3: General Purpose and System Programming Instructions (PUB) (24594) - 24594

 

docs.amd.com

 


3. pause를 이용한 대기

일반적인 스핀락입니다.

다만 pause를 사용하면 의도적인 스핀락이라고 CPU가 인지해 파이프라인을 다소 완화합니다.

773341D0 8B 17                mov         edx,dword ptr [edi]  
773341D2 F6 C2 01             test        dl,1  
773341D5 74 13                je          _RtlpSpinForCriticalSection@12+0FAh (773341EAh)  
773341D7 8B 4D F8             mov         ecx,dword ptr [ebp-8]  
773341DA 8B C2                mov         eax,edx  
773341DC 33 CA                xor         ecx,edx  
773341DE F0 0F B1 0F          lock cmpxchg dword ptr [edi],ecx
Improves the performance of spin loops, by providing a hint to the processor that the current code is in
a spin loop. The processor may use this to optimize power consumption while in the spin loop.
Architecturally, this instruction behaves like a NOP instruction.
Processors that do not support PAUSE treat this opcode as a NOP instruction.

위는 AMD문서에서의 표현입니다. 


결론

criticalsection은 유저영역에 있는 lock 획득을 위해서 일종의 interlocked를 활용한 스핀락을 하고있습니다.

따라서 락을 짧은 시간 내에 획득 가능하다면, 굳이 스레드가 block상태에 빠지지 않고 즉시 반환될 수 있어 커널 모드 전환으로 인한 비용이 줄어듭니다.

 

추가적으로 monitor, wait명령과 pause를 통해 스핀락의 CPU과다사용 문제도 완화하려는 시도를 하고 있음도 확인할 수 있었습니다.