서적 Effective C++ 은 개발자에게 널리 알려진 좋은 책이다.

그 중 개정 3판의 20항목에 다음과 같은 이야기가 나온다.


"값에 의한 호출보다는 상수 객체 참조에 의한 호출이 대개의 경우 낫다"


물론 이 책을 이미 정독했거나 충분한 경험을 가진 사람은 왜 저기에 "대개"가 붙는지 알고 있으리라.

그 내용에 대해서는 바로 연이어 21항에서 설명하고 있다.




여기서 살펴볼 것은 20항에서 기본 타입과 STL 반복자, Functor 등에 대해 언급한 부분이다.

저자는 이 세 가지에 대해서는 값으로 넘기는 것이 참조 호출로 넘기는 것보다 낫다고 이야기하고 있다.

원문의 내용을 보지 않아 정확히 해석이 이루어진 것인지 알 수 없지만 일단 위의 내용을 수긍하고 가기 위해

예제를 만들어 테스트를 해보았다.


직접 돌려보기 귀찮은 사람은 결과 텍스트 파일만 보아도 좋다.
 
직접 돌려볼 사람은 디버그 코드가 잔뜩 삽입되는 디버그 모드로 컴파일해서 "이거 순 엉터리잖아!" 라고 하지 말고
 
릴리즈 모드로 최적화를 뺀 상태로 컴파일하기 바란다. 최적화 옵션을 주어 컴파일해버리면 컴파일러가 구조체에 대해서는

알아서 참조 호출을 해버리고 함수 내용이 몇줄 안되면 인라인으로 만들어버린다.

평소라면 똑똑하다며 좋아하겠지만, 이런 테스트로는 참 곤란한 녀석이다.
(VS2008의 컴파일러가 C/C++에 대해 그렇게 효율적인 코드를 생성하지 못한다는 속설에 대해서는 일단 잊자)




테스트는 기본 타입(int), 기본 생성자를 가진 구조체를 자료로 하는 STL 타입(vector 사용), 기본 생성자를 가진 구조체 타입, 사용자가 생성자를 정의한 구조체 타입으로 4개를 등장시켰고

좀 더 많은 대조군과 세밀한 조사가 필요하겠지만 원론적으로는 불필요할 것 같아 생략하였다.




결과 파일에 의하면 중간 중간 스위칭으로 인한 어마어마한 시간 손실을 제외한 유효한 호출값에 대해서

모든 경우에 값 호출보다 참조 호출이 속도에서 우위를 보인다. 저자의 이야기와는 살짝 다른데...?




이론적으로 파헤쳐보자.

분명한 사실은, C++ 에서는 기본 타입이라 하더라도 내부적으로 모두 객체 취급을 받으며 클래스처럼 동작한다.
(물론 진짜 클래스인 것은 아니다)

즉 int i; 라고 선언한다 해서 이것이 C에서처럼 단순하게 메모리에 4바이트 땡기는 것으로 끝나지 않는다는 소리다.

일반적인 구조체/클래스의 생성자 호출과 비슷한 과정을 거쳐 메모리가 초기화되며(0이 아닌 어떤 값, VS 컴파일러는 0xCCCCCCCC로 초기화한다) 서로 대입되는 과정에서는

C에서처럼 단순히 어셈블리 코드의 메모리 로드/스토어 과정으로 변환되는 것이 아닌 operator = 함수 호출이라든지

복사 생성자의 호출 등 <함수 오버헤드>적인 측면이 생긴다.

다만 일반적인 생성과 대입 측면에서 볼 때 최적화 옵션이 주어지면 C++의 기본 인자는 마치 C의 그것처럼 동작한다.

디버그 코드로는 엄청난 내용들이 int i = 3; 이라는 짧은 내용 안에도 미어터지게 들어가서 호출 자체 시간도 약 10배 정도 늘어날 뿐더러

값 호출과 참조 호출은 2배 가량의 격차를 보인다(기본 타입인데...). 하지만 최적화를 하면 걱정할 필요는 없다.
(여기서 우리는 C와 C++의 기본 타입에 대한 차이를 확실히 알 수 있다)

덕분에 기본 타입은 내부적으로 주어진 주소에 메모리 상의 값을 끌어다 집어넣는 1회 연산의 차이로 끝나기 때문에 둘의 속도는 비슷하다. 다음 어셈 코드를 보자.


void f1(int i)를 호출하기 직전 모함수에서의 i 값을 레지스터에 밀어넣는 어셈블리 코드

004401AB  mov         eax,dword ptr [i]     // eax 레지스터에 i 변수의 값을 밀어넣기
004401B1  push        eax                        // eax 레지스터를 스택에 밀어넣기

void f2(const int& i)를 호출하기 직전 ... 어쩌구

00440245  lea           eax,[i]                   // eax 레지스터 값을 i 변수의 주소값으로 만들기
0044024B  push        eax                       // eax 레지스터를 스택에 밀어넣기

※ mov는 제2연산항의 "값"을 레지스터에 밀어넣기 때문에 load 연산(즉, 메모리 접근)이 추가적으로 필요하지만 lea는 주소를 넘기므로 추가적인 메모리 접근이 필요 없어 훨씬 빠르다. 단, lea의 경우 주소값을 써야하기 때문에 레지 대 레지 값 복사는 불가능하다

여기서 볼 수 있듯이 참조 호출이 값에 의한 호출보다 더 빠르다는 것은 일단 여지없는 정론이다.



자, 그럼 STL을 보자. STL은 내부의 모든 존재들이 클래스로 이루어져있다. 반복자도 물론 예외는 아니다.

그렇다면 반복자의 경우도 당연히 값으로 넘겨서 생성/소멸 과정을 거치는 것보다 참조로 넘기는 것이 속도에서 훨씬

빠르리라는 것은 당연히 추측할 수 있고 실제 결과로도 그러하다. 제 아무리 STL이 속도 위주로 설계되었고 최적화되었다고 해도

붕어빵 틀에 재료를 넣으면 붕어빵이 나올 수밖에 없는 것처럼 속도면에서는 값 호출이 참조 호출보다 빠를 수가 없는 것이다.

그 뒤로 이어지는 구조체 호출은 따로 분석하지 않겠다. 워낙 당연하기 때문이다.

void f3(vector< SDATA >::iterator it)를 호출하기 직전 모습

004402EA  lea         eax,[ebp-2Ch]
004402ED  push      eax 
004402EE  call        std::_Vector_iterator<SDATA,std::allocator<SDATA> >::_Vector_iterator<SDATA,std::allocator<SDATA> > (43D789h)
004402F3  mov         dword ptr [ebp-220h],eax

(와, 끔찍해라. 중간의 저 call 명령이 야기할 로스를 생각하며 잠시 묵념. 게다가 STL은 단일 클래스가 아닌 상속의 꽃과 같은 구조들이므로 안에서도 여러 차례 콜이 이루어지고 있다. 묵념을 한 3회는 더 해야할 듯. 게다가 <당연히> 레지스터엔 들어갈 수 없으니 메모리에 살포시 올려두고 함수로 간 다음 다시 찾아와야 한다. 기본 타입 int 형이 자신의 값을 레지스터 eax에 집어넣고 홀가분하게 호출했던 것과 비교하면 극과 극인 셈)

void f4(const vector::iterator&) 를 호출하기 직전 모습

0044038F  lea          eax,[ebp-2Ch]
00440392  push        eax 

(차이가 느껴지시는가?)




자 그렇다면, 여기까지 와서 생각해보자. EC++ 의 저자는 헛소리를 한 것인가?
 
아니면 일부러 교묘하게 독자의 이해력과 집중도를 오판하도록 만든 것인가?
(책의 전반부에서는 분명 "속도"에 중점을 두어 이야기하고 있었는데 후반부에 정리할 때는 다른 이유 때문에 값 호출이 더 낫다고 주장한 것인가?)

그도 아니면 그냥 해석상의 오류인가?

아니면 정말 VS2008 컴파일러가 바보라서 저렇게밖에 안되는건가?(설마 이걸로 믿는 사람은 없다고 생각하겠다)



단호하게 주장하는데, 기본 타입이든 아니든, 위의 내용들은
 
최소한 속도 면에 있어서
값에 의한 호출을 쓸 바에는 상수 참조 호출을 사용하는 것이 훨씬 낫다는 것을 보여주고 있다.

(그리고 그것이 우리의 상식에 부합한다)



그리고 속도 면이 아니라 다른 이유 때문에 위의 세 가지(기본 타입, STL 반복자, Functor)에 대해서는 값 호출이 낫다고

주장한 거라면, 역시 의도를 오해하지 않도록 가필이 필요하다.

어쨌든 책 내용을 수정하셔야 할 듯...
Posted by OOJJRS
,