어셈블리어를 배워야 하는 이유 (증감 연산자 최적화의 진실)

PenguinGod·2022년 7월 3일
1

질문의 재구성

어셈블리어를 왜 공부할까요? 라고 물어보면 그냥 "코드의 구조를 좀 더 정확히 할 수 있기 때문입니다." 라는 추상적이고 와닿지 않는 답변밖에 못할 겁니다.

그럼 좀 더 구체적인 질문을 해보죠.
왜 증감연산자를 전위와 후위에 붙일 때 서로 다르게 작동할까요?
후위 연산보다 전위 연산이 더 빠르다고 하는데 이건 사실일까요?
만약 그렇다면 왜 그런 것일까요?

이 질문에 대한 대답은 간단합니다. 바로 어셈블리어가 그렇게 동작하기 때문입니다.

그렇다면 다음 질문은 어셈블리어가 '어떻게' 동작하는지가 되겠군요.

그러면 어셈블리어를 대충 읽을 정도만 알아보고 저 '어떻게'에 대한 대답을 해봅시다.

명령어

일단 어셈블리어는 명령어가 굉장히 많습니다. 하지만 이 글에서는 2개만 다루겠습니다.

바로 mov와 add입니다.

mov rax, 0
add rax, 2

위의 2줄은 간단합니다. 우선 mov는 나중에 알아볼 rax을 0으로 설정하는 겁니다. add는 0으로 밀어버린 rax에 2를 더하는 거고요.
그럼 rax는 2가 되겠군요. 그리고 이 글에서는 저거 2개만 있으면 됩니다.

레지스터

그리고 하나 더 중요한걸 알아보겠습니다.
바로 위에서 나온 rax입니다.
저 친구는 무었일까요?
이 표를 한 번 보죠

보시면 rax가 가장 아래에 깔려 있고 위에 뭐가 추가로 있는걸 보실 수 있는데 저건 나중에 보시고 아래에 숫자를 보시죠.
아래에 숫자를 보면 범위가 0~63이라는 것이 보일 겁니다. 그리고 저기의 숫자는 비트를 의미합니다. 즉 rax는 64비트, 즉 8바이트 크기의 저장 공간을 의미합니다.

그리고 위에 있는 eax, ax, ah, al은 rax를 각각의 크기마다 쪼개논 것이죠.

저렇게 생긴게 rax하나만 있지는 않습니다.

이렇게 eax, ebx, ecx, edx 그리고 뒤가 P로 끝나는 거랑 I롤 끝나는 거랑 S로 끝나는 거까지 여러게가 있습니다.

참고로 rax가 아니라 eax로 나오는 이유는 32비트 컴퓨터 기준으로 한 이미지이기 때문입니다. rax가 64비트 크기의 저장 공간이라고 했죠? 그 이유가 64비트 컴퓨터 안에 있기 때문입니다.

그럼 저 애들은 컴퓨터 어디에 있을까요?
바로 아래 사진을 보시죠.


찾으셨나요? 사실 제가 대놓고 언급하지는 않아서 알 수 없기는 합니다.
정답은 CPU안에 '레지스터' 라고 적힌 부분이 보이시죠? 저기에 아까 본 eax, ebx 등등이 들어가 있는 것입니다.

잠시 컴퓨터 구조를 봐보죠. 하드 디스크(혹은 SSD)는 CPU와 멀리 있습니다. 하지만 메모리(렘)는 상대적으로 가까이 있죠.

일반적으로 메모리가 하드 디스크보다 연산 속도가 빠르다는 것을 저는 배그가 안돌아가던 컴퓨터에 8G짜리 렘을 꼽자 돌아가는 것을 보고 느꼈습니다.

근데 대놓고 안에 있는 레지스터는 얼마나 빠를까요?

예 겁나 빠릅니다. 그리고 당연하게도 CPU의 여러가지 계산을 위해 쓰이는 공간입니다.

실제 코드의 어셈블리어

어셈블리어에서 레지스터를 어떻게 사용하는지 살짝 알아보겠습니다.

a에 b보다 5가 큰 값을 대입하는 간단한 C# 코드입니다 작성해보았습니다.

이 코드의 어셈블리어를 보겠습니다.
우선 적당한 곳에 브레이크 포인트를 잡습니다.

그리고 시작 버튼을 누르거나 F5를 통해 디버깅 모드로 실행합니다.

실행을 하면 디버그 -> 창 -> 디스어셈블리를 통해 혹은 Ctrl+Alt+D 단축키를 통해 어셈블리 창으로 갑니다.

그러면 이렇게 성공적으로 도착한 것을 볼 수 있습니다.

보시면 저희의 코드가 어떻게 어셈블리어로 작성되었는지 볼 수 있습니다.

우선 int a; 는 별다른 코드가 없군요.
반면에 int b = 30; 은 mov 를 통해 값을 넣은 것을 볼 수 있군요.

1Eh라는 값(1E는 16진수로 30. 마지막 h는 이 값이 16진수라는 것을 의미함)을 ebp-44h 라는 주소부터 총 dword(4바이트) 만큼의 자리를 차지해 넣겠다는 뜻입니다.

이 글에서는 디테일하게 들어가지 않을 것이기에 이 정도만 하고 넘어가도록 하죠.

그럼 그 다음 a = b + 5; 를 봐봅시다.

eax라는 레지스터에 방금 봤던 주소인 ebp-44h를 통해 b의 값을 넣었습니다.

그리고 add명령어를 통해 eax에 5를 더합니다.

마지막으로 a에 eax를 넣으면 끝입니다.

C#에서는 1줄이었던 코드가 어셈블리어에서는 3줄인 것을 볼 수 있습니다.

증감연산자 최적화의 진실

그럼 더 심한 경우를 볼까요? 바로 ++입니다.


아까 코드에서 살짝 바꿔서 b++과 ++b를 해보았습니다.

저희가 알기로는 b++을 하게 되면 a에는 그냥 b의 값이 들어가고 연산이 진행됩니다. 반면 ++b는 연산 후 1이 증가된 b의 값이 a에 대입되죠.
그 과정을 어셈블리어로 뜯어보겠습니다.


이것이 위의 C#코드를 어셈블리어를 변환한 것입니다.
그 전에 저의 거짓말에 대한 해명을 해야겠군요.
명령어 2개만 알면 된다고 했는데 사실 하나 더 알아야 합니다.
바로 inc라고 1더하는 친구입니다.
add를 이용해서 1 더한 것과 비슷한 뜻이니 봐주시길 바랍니다.

아무튼 여기서 증감 연산자의 차이가 보이시나요?
보시면 b++은 eax에 b의 값을 담고 처음 보는 새로운 주소에 eax의 값을 넣습니다.
그 후 inc를 통해 b의 값을 1을 증가시키고 그 값을 다시 eax에 넣습니다.
그리고 b의 증가 연산 전에 넣어둔 주소에 있는 값을 a에 넣습니다.

반면 ++b는 먼저 b의 값을 증가시키고 eax에 증가한 b의값을 넣고 eax를 a에 넣습니다.

a에 들어가는 값의 변화도 변화지만 b++은 뜬금없이 다른 공간의 메모리를 잡아먹는 것을 볼 수 있습니다.

특히 ++연산자는 반복문에서 수천~수만 번 이상씩 사용되기 때문에 이게 꽤나 의미가 있어 보입니다.

그럼 팩트체크를 해봅시다.


이건 반복문입니다. 위의 코드는 i++ 아래의 코드는 ++i를 사용합니다.


이게 어셈블리어 코드입니다.

근데 뭔가 이상하지 않나요? 둘 다 ++i를 사용하고 있습니다!

즉, 컴파일러에서 자동으로 최적화를 해주고 있다는 뜻입니다.
한마디로 신경쓰면서 ++i로 짜나 평소처럼 i++로 짜나 사실 성능에는 1도 의미가 없었던 겁니다.

for문이여서 그런 걸까요?
그럼 while문을 봐보죠.


어셈블리어가 복잡하긴 하지만 빨간색 박스 부분만 봅시다.
i++이나 ++i나 둘 다 inc를 사용하는 것을 볼 수 있습니다.

재밌는 사실을 하나 더 봐보죠

이건 아까의 코드를 C++에서 작성한 코드입니다. 어셈블리어를 봐보죠.

보시면 C++의 어셈블리어는 C#과 다른 것을 볼 수 있습니다.
우선 [안에] 주소가 아니라 변수의 이름이 들어가 있어 C#보다 보기가 좋군요.
그냥 C++로 포스팅할걸 그랬습니다.

아무튼 C#에서는 ++b가 3줄이었데 비해 여기서는 5줄 입니다.
구조를 보시면 C#과는 다르게 add를 하는 순서만 다르고 성능 차이가 없는 것을 볼 수 있습니다.

이건 언어마다 컴파일러가 달라서 그렇고 버전에 따라서도 다를 수 있습니다.

어셈블리어를 배워야 하는 이유

실제로 예전에는 저런 최적화가 의미가 있었을 수 있고 지금도 의미가 있는 곳이 분명 있을 겁니다.

하지만 코드를 실행하고 있는 '나의 환경'에서 어떤지가 중요합니다.
적어도 저는 별 의미가 없는 것 같습니다.

그리고 이런 내용이 어셈블리어를 배울 때의 장점입니다.

사실 컴퓨터는 저희의 직관과는 다르게 작동할 때가 많습니다.
하지만 그런 직관을 믿고 최적화를 진행할 때도 많습니다.

별거 아닌거 같아도 최적화로 인한 코딩 스타일에 변화는 개발자의 불편함, 평소라면 하지 않았을 실수로 생기는 버그 등 보이지 않는 여러 자원을 잡아먹습니다.

즉, 뭔가를 희생하고 성능을 얻는 것이죠. 하지만 컴퓨터에 정확한 이해 없이 진행하는 최적화는 얻는 것 없이 희생만 있을 수도 있습니다.

여러 불편함을 감수하고 했더니 사실 실제 성능에는 1도 의미가 없었던 것이죠.

이런 상황을 막기 위해서라도 어셈블리어를 읽기라도 할 줄 아는것은 의미가 있다고 생각합니다.

profile
수강신청 망친 새내기 개발자

1개의 댓글

comment-user-thumbnail
2022년 7월 3일

정말 재밌게 읽었습니다!!
감사합니다!!

답글 달기