메모리 베리어

CJB_ny·2022년 2월 8일
0

Unity_Server

목록 보기
8/55
post-thumbnail

1. 하드웨어 최적화

지지난 시간에 컴파일러 최적화라 해가지고

우리가 짠 코드를 컴파일러가 멋대로

튜닝을 해가지고

다른 결과물을 만들어 내는 그런 실습을 해보았다.

그것이 분명히 성능 향상을 위해서 선의로 해주었던 것인데

결과적으로 "멀티 쓰레드"환경에서는 "독"이 되었고

오묘한 버그들을 만들었던 것을 지난 번에 살펴 보았다.

그런데 컴파일러 뿐만 아니라 하드웨어도 그런 무서운 짓거리를 하고있다.

그래서 오늘은

"하드웨어 최적화"라는 실습을 해볼 것이다.

이렇게 변수를 짜주고

오늘 실습을 할 것은 굉장히 간단한데


Thread_1 쓰레드가 하는 일은
y = 1로 바꾸고, r1은 x로 밀어 넣는 것이다.

Thread_2는 x = 1로 바꿔주고 r2 = y 로 바꿔치기 하는 것이다.


오늘 해볼 테스트는 이렇게 무한 루프를 돌면서


x, y, r1, r2다 0으로 만들고
Task만들어 주고 콜백으로 Thread_1을 던져준다.(일감을 던져준다)

마찬가지로 이렇게 또 Task를 만들어 준다.

이렇게 ThreadPool에서 두개의 Taks 쓰레드를 의뢰를 받았으니까

이제 "멀티 쓰레드" 환경으로 돌아가는 상태이고,


그리고 Start()를 한다음에

t1, t2의 일이 끝날 때 까지 기다려 준다.

그리고

if (r1 == 0 && r2 == 0)

이러한 조건이라고 하면 실험을 마쳐주도록 하자.

그런데 이것이 몇번만에 빠져나오는지 궁금하니까

count라는 변수를 만들어 주자.

이렇게 코드를 짜서 몇번 만에 나오는지 보도록 하자.

자, 그럼 여기서 멈춘다음에 곰곰히 한번 생각을 해보도록 하자.

             if (r1 == 0 && r2 == 0)

과연 이 상태가 나올 수 있을지..??? 궁금하죠?

이런 상황이라 못 나올 거 같은데... 실제로 실행을 해보면

이런식으로 빠져나온다!!

어쨋든

if (r1 == 0 && r2 == 0)

이런 상황이 일어 난다는 것이다.

혹시라도 예전 시간에 배운

최적화를 막아주는 "volatile" 라는 키워드를 붙이고 실행을 해보아도


이런식으로 탈출 함.

그런데 이 결과가 이해가 되지가 않는데

이게 지금 경우의 수를 조합을 해보도록 하자.

그런데 막 이렇게 이렇게

저렇게 저렇게 조립을 해보아도 0, 0 이 나올 수 있는 경우의 수가 없다.

그래서 사실은 굉장히 이상한것이다.

도대체 무슨일이 발생하고 있는 것일까?

그리고 혹시 지난 시간에 "캐시"에 대한 내용을 공부를 해서

"혹시 캐시에 대한 내용은 아닐까" 라고 생각이 들 수 도 있지만

지금 당장은 그런 문제가 아니다.

사실은 하드웨어도 우리를 위해서 최적화를 해주고 있다.

CPU같은 경우에 우리가 일련의 명령어 들을 시키면은

CPU가 봐가지고 서로 "의존성"이 없는 명령어라고 판단이 되면은

순서를 지 멋대로 뒤바꿀 수 있다는 얘기가 된다.

CPU가 보았을 때

지금 Y = 1을 넣어 주는 작업이랑 R1 = X 를 넣어 주는 작업은 (이 두가지 작업은)

아무런 연관성이 없다.

그래서 자기가 조금 더 빠르게 실행하기 위한 최적의 수를 찾다가

지 멋대로 이렇게

바꿔버릴 수 있다는 얘기이다.

초록색 박스도 이런식으로

뒤집어서 실행 할 수 있다는 얘기가 된다.

이 상태에서는 확실히

            if (r1 == 0 && r2 == 0)

이런 상태가 튀어 나올 수 있다는 생각이 든다.

그런데 이게 뭔 말도 안되는 소리인가 싶다

왜 우리의 허락도 안 받고 지 마음대로 바꾸나? 싶은데

사실은 "싱글 쓰레드"에서는 이렇게 하더라도 "전혀" 문제가 되지 않았다.

왜냐면은 애당초 R1 = X, Y = 1해주는 작업은 연관성이 없기 떄문에

사실 이렇게 해서 속도 향상이 있다면은 얼마든지 순서를 뒤바꿔서 하는게 좋다는 얘기가 된다.

그런데,

"멀티 쓰레드" 환경에서는 여전히 그렇게 쿨하게 지 마음대로 해버리면은

우리가 예상한 "로직"이 꼬이게 된다는 "문제"가 발생을 하게된다.

그래서 결국에는 문제가 되는게

이렇게 우리가 실행한 순서대로 실행이 되지가 않고

지 멋대로 순서를 뒤바꿔서

실행한것이 "큰 문제"기 되었다는 것인데

그렇다는 것은

이런식으로 선을 그어서 무조건 이 순서대로 강제로 실행을 하도록 시키면

이 문제가 해결이 될 것이다.

그래서 오늘은 "메모리 베리어"에 대한 내용을 알아보려고 이 것을 하고 있었다.

2. 메모리 베리어

메모리 베리어 가 하는 역할은

이렇게 두가지 이다.

  • 코드 재배치 억제

  • 가시성

이렇게 있는데 일단 가시성에 대한 얘기는 조금 있다가 하자.

"코드 재배치 억제" 부터 알아보면 말 그대로 코드를 재배치를 하는 것을 막아 주는 것인데

여기다가 이렇게 메모리 베리어 써주면 이것은

이렇게 선을 쫘악 그어가지고 서로 넘어 올 수 없다고 경계선을 그은 것이다.

y = 1이 아래로 넘어 올 수 없고 마찬가지고 r1 = x 도 위로 넘어 갈 수 없는 것이 되니까

하드웨어도 마찬가지고

r1 = x 를 먼저 실행 할 수 없다는 얘기가 된다.

Thread_2도 똑같이 선을 그어서 경계선을 만들어 준 것인데

이 상태에서 다시 실행을 해보면

이렇게 무한 루프를 돌고 있다.

그래서

            if (r1 == 0 && r2 == 0)

이런 상황이 나타 날 수 없다는 결론을 얻을 수 있다.

그런데 사실은

"메모리 베리어"가 이런식으로 하나만 있는 것은 아니고

"종류"가 사실 여러개 이다.

우리가 한것은 Full Memory Barrier 라 해가지고

어셈블리 언어로 치면(ASM) MFENCE이고

C#으로치면 Thread.MemoryBarrier 이다.

이것은 Store / Load 둘다 막는 것.

Store는 y = 1; 이런식으로 y라는 변수에다가 실제 값을 넣어 버리는 것을 말하는 것이고

Load 는 말 그대로 r1 = x 이렇게 있을 때 x라는 변수에서 값을 끄집어 내는 것을 Load라 하는데

우리가 처음 사용한 Full Memory Barrier는 Store / Load 둘다 막는 것이고

경우에 따라서

조금더 최적화를 하고 싶다고 하면은

반쪽짜리 베리어도 있기는 하다.

Store Memory Barrier (AMS SFENCE) : Store만 막는다.

이렇다면 짝꿍도 있어야 되는데

LFENCE라는 녀석도 이렇게 있다.

그런데 우리가 프로그래밍을 할때

SFENCE, LFENCE까지 나누면서 세분화 할 일은 없고

FULL Memory Barrier의 개념만 이해를 하고 있으면 된다.

이제 다시 PPT로 돌아와서 가시성에 대한 얘기를 해보도록 하자.

3. 가시성

실제로 보이냐 안보이냐에 대한 얘기인데

다시 문제가 되었던 고급 식당으로 와보면은

직원이 지금 두명 있는데

1번 직원이 주문을 받기는 했는데

지금 실제 주문 현황판에까지는 올리지 않고

자기 수첩에다가만 주문을 적은 상태였다.

그렇게 되면은 2번 직원의 입장에서는 2번테이블에서 콜라를 주문했다는 사실을 알지를 못하게 되니까

애당초 상황이 일치를 하지 않았다는 얘기가 된다.

그래서 결국 우리가 말하는

"가시성" 이란 뭐냐??

1번 직원이 주문을 받은 것을 다른 직원들도 1번 직원이 주문을 받은 것을 볼 수 있냐? 없냐? 그것의

가시성을 얘기하고있는 것이다.

그런데, 결국 가시성을 보장하기 위해서는

1번직원이 주문을 받은 것을 자기 수첩에다가만 놔두면 안되고

이런식으로 실제 주문 현황에다가 옮겨 놔야 한다.

그래가지고 일단 자기 수첩에 있는 내용을 주문현황에다가 옮기면은

그 다음에 두번째 직원은

주문현황에있는 실제 정보를 가져와가지고 두번째 직원이 자기 수첩에다가 정보를

갱신을 하면은

실제 상황이랑 맞아 떨어지게 업데이트가 된다는 그런 말이 된다.

그런데 이것이 사실 굉장히 중요한데

이런식으로 커밋하고 다른애가 정보를 갱신해서 업뎃을 하는 작업이 중요한데

어떻게 보면 이게 약간 화장실이라고 생각을 하면

우리가 볼일을 본다음에 물을 내린다. => 이게 딱


이 상황이고

두번째 상황은

화장실에 들어가자마자 물을 내리고 사용을 하는 그런 느낌이다.

첫번째는 뭔가 자기 수첩에 쓴다음에 커밋을 하니까 뭔가 동기화 작업을 해야되는 것이고

그것을 두번째 경우처럼 실제로 사용할 사람은

일단 "동기화"를 먼저 한다음에

그다음에 read를 해서 그 data를 불러 읽어 드려야 정상적인 똑같은 상황이 보이게 된다는 말이 된다.

자 그래서 코드로 돌아 와보면은

메모리 베리어가 하는 첫번째 역할은 코드 재배치 억제라고 했었다.

위에 녀석이 밑으로 못 내려오고 밑에 녀석이 위로 못가고

사실 이런 역할을 하는 것이 "가시성"의 역할도 같이 하는 것이다.

그런데 나중에 우리가

        Thread.MemoryBarrier();

이런식으로 하나하나씩 이렇게 넣지 않더라도 간접적으로 들어가 있는 경우가 많다.

나중에 배울 아토믹, 락 같은 것들도 내부적으로 베리어가 구현이 되어있다.

그래서 일단 베리어 개념은 우리가 실제로 사용하면서 만드는 개념은 아니고

그냥 이렇게 작동을 하면서 원리를 학습을 하는 그런 부분이 된것이다.

그래서

오늘 기억할 것은 딱

두개인데

"메모리 베리어"는

1) 코드 재배치를 억제
2) 가시성

이거 두가지 이다.

그리고 마치기 전에 유명한 예제를 볼 것인데

이것을 이해를 하면 메모리 베리어를 거의 다 이해 한것인데

지금 A, B 둘다 실행을 하고 있다고 가정을 한것이다.

우리가 이전까지

이렇게 중간에 낀거 까지는 이해를 했었는데

마지막에도 이렇게 오고

얘는

처음에도 이렇게 오는데

아까 같은 경우에는 write를 한다음에 바로 read를 했었었는데

얘도 write이고

얘도 write이다.

그러니까 Store가 연속적으로 두번 발생 하다보니까

첫번째 메모리 베리어에서 "가시성"을 챙겨주는 것은

_answer = 123 인 아이를 챙겨 주는 것이다.

그러니까 _answer = 123;이라고 쓴다음에

Thread.MemoryBarrier라고 물을 내리는 작업을 한 것이고

그 다음에도

_complete = true라고 뭔가를 썻으니까

다시 Thread.MemoryBarrier라고 물을 내리는 작업을 해주어야만

"가시성"이 확실이 보장이 되는 것이고.


이녀석은

if문에서 읽기전에 가시성이 보이도록 위에다가 Thread.MemoryBarrier라고 가시성을 확보를 해주는 것이고,


여기서도 마찬가지로

_answer라는 것을 read를 하고 싶은데

그것이 내가 확실히 최신 정보를 긁어오는 것이 맞는지

확실하게 하기 위해서

여기다가 이렇게 메모리 베리어를 넣어 준 것이다.

그래서 결국

"메모리 베리어"가 두가지 기능을 하고 있었다.

여기서 이렇게 코드 재배치를 막는 것도 훌륭이 해주고 있었고

가기성도 이렇게 챙겨 주는 것까지

알아보았다.


그래서 아까아까 전에 y = 1; 메모리 베리어 r1 = x; 인 부분을 보면은

y = 1; 해주고 메모리 베리어를 호출을 하면은 물을 내리는 작업처럼 실제 메모리에다가 y = 1;을 올리게 되는 그런 느낌이 된다.

마찬가지로 Load하기 전에 메모리 베리어가 실행이 되었다는 것은

r1 = x ; 에서 x의 값도 따끈따끈한 실제의 값을 가져오는 얘기가 된다.


profile
https://cjbworld.tistory.com/ <- 이사중

0개의 댓글