요약:
자바와 가상함수는 느리지 않을 수도 있다. 10억번의 함수 호출을 하는 실험 결과, 가상함수가 절대함수보다 더 빠르고 자바도 C++만큼 빠르다는 것을 알 수 있었다. 하지만 다른 많은 이론들과 실험들은 가상함수가 훨씬 느림을 보여준다. 이것이 단순히 이 실험과 같은 경우에만 국한된 것인지, 아니면 다른 경우에도 적용될 수 있는지 알기 위해서는 더 많은 실험을 해봐야한다.
배경:
가상함수의 부작용에 관하여 논한 좋은 글이 있었습니다. 주요 내용으로는 C++은 C에 비해 가상함수을 쓰기 때문에 느리고, 자바는 더욱 느리다는 것입니다. 고개를 끄덕이며 읽어가다가, 인용 자료의 원본에서 내 주의를 끄는 문장 하나. "자바의 실험 결과는 훨씬 느린 기계에서 했으므로 절대시간을 비교하지 마시오." 저는 같은 기계에서 실험해보면 어떤 결과가 나올지 문득 궁금해졌습니다. 하지만 object님께서는 "그래도 자바가 느린 것은 맞다."라고 하셨습니다. '까짓거, 직접 실험 해보지.' 라는 생각으로 실험에 돌입했습니다.
목적:
비슷한 일반적인 환경에서 자바와 C++, 그리고 가상 함수와 절대 함수(실제 함수라고 불러야 하나?)의 성능 차이를 살펴봅니다.
실험방법:
기존의 가상 함수 호출 위주로 작성된 성능 측정 코드를 고쳐서 적용했습니다. 상태를 변경하며 카운트를 세는 간단한 코드만을 무려 10억번 호출했습니다. C++ 코드는 Dev-C++에서 작성하여 기본 설정을 따릅니다. G++ 최적화 옵션이 켜진 상태입니다. Java는 Eclipse에서 작성하여 기본 설정을 따르고 VM은 단순한 Client 모드입니다. 둘 다 두세달 내에 받은 최신 버전입니다. Windows XP, Quad core, 2GB RAM입니다. 다른 많은 작은 상주 프로그램을 돌리고 있습니다. 영향이 최소화 되도록 약 10번씩 실험해서 대표값을 취합니다.
결과:
자바 가상함수: 11초 소요. 간혹 10초도 소요.
자바 절대함수: 16초 소요. 10번 모두.
C++ 가상함수: 10초 소요. 10번 모두.
C++ 절대함수: 17초 소요. 10번 모두.
해석:
1. 자바랑 C++의 실행 시간은 거의 같다. C++에는 최적화 옵션을 켜고, 자바에서는 최적화가 된 server 모드 대신에 client 모드로 돌렸습니다. 그럼에도 예상한대로, 자바와 C++의 수행 속도에는 큰 차이가 없었습니다. 가상 함수 호출에서는 5% 정도의 미미한 차이만 났습니다. 절대 함수 호출에서는 자바가 오히려 C++보다도 더 빠른 결과를 보여주었습니다. client 모드에서도 실행시간 중 최적화 기능이 작동하는 것이 아닐까, 싶을 정도입니다.
2. 자바와 C++ 모두에서 가상함수의 호출 시간이 훨씬 더 짧다. 가상 함수가 더 오래 걸린다는 이론을 가지고 실험에 돌입했으나, 그와는 정 반대로 절대함수가 60%~70%정도 더 느린 결과를 보여주었습니다.
결론:
직접 실험을 해보니, 이론과 완전히 뒤집어진 결과가 나왔습니다. 이론과 실험, 둘 중 하나는 틀렸겠지요. 아니면 어떤 조건 안에서는 특이한 결과가 나온다거나요. 제 생각에는 분명히 제 코드에 문제가 있을 것이라고 봅니다. 그것이 아니라면, 가상 함수의 호출 비용에 대하여 정말로 다시 생각해봐야할 것입니다. 하지만 눈을 씻고 찾아봐도 오류가 보이지가 않습니다. 그래서 여러분께 검토 도움을 요청합니다. (peer-review)
마지막으로, 테스트에 쓰인 소스코드를 첨부합니다. 컴파일은 각자 환경에 맞게 하시고, 실행 방법은 각기 java Final 1000000000, final.exe 1000000000 같은 식이 되겠습니다.
Perf.zip
자바와 가상함수는 느리지 않을 수도 있다. 10억번의 함수 호출을 하는 실험 결과, 가상함수가 절대함수보다 더 빠르고 자바도 C++만큼 빠르다는 것을 알 수 있었다. 하지만 다른 많은 이론들과 실험들은 가상함수가 훨씬 느림을 보여준다. 이것이 단순히 이 실험과 같은 경우에만 국한된 것인지, 아니면 다른 경우에도 적용될 수 있는지 알기 위해서는 더 많은 실험을 해봐야한다.
배경:
가상함수의 부작용에 관하여 논한 좋은 글이 있었습니다. 주요 내용으로는 C++은 C에 비해 가상함수을 쓰기 때문에 느리고, 자바는 더욱 느리다는 것입니다. 고개를 끄덕이며 읽어가다가, 인용 자료의 원본에서 내 주의를 끄는 문장 하나. "자바의 실험 결과는 훨씬 느린 기계에서 했으므로 절대시간을 비교하지 마시오." 저는 같은 기계에서 실험해보면 어떤 결과가 나올지 문득 궁금해졌습니다. 하지만 object님께서는 "그래도 자바가 느린 것은 맞다."라고 하셨습니다. '까짓거, 직접 실험 해보지.' 라는 생각으로 실험에 돌입했습니다.
목적:
비슷한 일반적인 환경에서 자바와 C++, 그리고 가상 함수와 절대 함수(실제 함수라고 불러야 하나?)의 성능 차이를 살펴봅니다.
실험방법:
기존의 가상 함수 호출 위주로 작성된 성능 측정 코드를 고쳐서 적용했습니다. 상태를 변경하며 카운트를 세는 간단한 코드만을 무려 10억번 호출했습니다. C++ 코드는 Dev-C++에서 작성하여 기본 설정을 따릅니다. G++ 최적화 옵션이 켜진 상태입니다. Java는 Eclipse에서 작성하여 기본 설정을 따르고 VM은 단순한 Client 모드입니다. 둘 다 두세달 내에 받은 최신 버전입니다. Windows XP, Quad core, 2GB RAM입니다. 다른 많은 작은 상주 프로그램을 돌리고 있습니다. 영향이 최소화 되도록 약 10번씩 실험해서 대표값을 취합니다.
결과:
자바 가상함수: 11초 소요. 간혹 10초도 소요.
자바 절대함수: 16초 소요. 10번 모두.
C++ 가상함수: 10초 소요. 10번 모두.
C++ 절대함수: 17초 소요. 10번 모두.
해석:
1. 자바랑 C++의 실행 시간은 거의 같다. C++에는 최적화 옵션을 켜고, 자바에서는 최적화가 된 server 모드 대신에 client 모드로 돌렸습니다. 그럼에도 예상한대로, 자바와 C++의 수행 속도에는 큰 차이가 없었습니다. 가상 함수 호출에서는 5% 정도의 미미한 차이만 났습니다. 절대 함수 호출에서는 자바가 오히려 C++보다도 더 빠른 결과를 보여주었습니다. client 모드에서도 실행시간 중 최적화 기능이 작동하는 것이 아닐까, 싶을 정도입니다.
2. 자바와 C++ 모두에서 가상함수의 호출 시간이 훨씬 더 짧다. 가상 함수가 더 오래 걸린다는 이론을 가지고 실험에 돌입했으나, 그와는 정 반대로 절대함수가 60%~70%정도 더 느린 결과를 보여주었습니다.
결론:
직접 실험을 해보니, 이론과 완전히 뒤집어진 결과가 나왔습니다. 이론과 실험, 둘 중 하나는 틀렸겠지요. 아니면 어떤 조건 안에서는 특이한 결과가 나온다거나요. 제 생각에는 분명히 제 코드에 문제가 있을 것이라고 봅니다. 그것이 아니라면, 가상 함수의 호출 비용에 대하여 정말로 다시 생각해봐야할 것입니다. 하지만 눈을 씻고 찾아봐도 오류가 보이지가 않습니다. 그래서 여러분께 검토 도움을 요청합니다. (peer-review)
마지막으로, 테스트에 쓰인 소스코드를 첨부합니다. 컴파일은 각자 환경에 맞게 하시고, 실행 방법은 각기 java Final 1000000000, final.exe 1000000000 같은 식이 되겠습니다.
Perf.zip



덧글
object 2008/12/11 13:53 # 답글
가상함수가 더 시간이 오래 걸린다를 약간 오해하신 것 같네요. 정확하게는 CPU차원에서 간접 분기문을 처리하는데 분기 목적지가 여러 개가 있고 이것을 예측하기 어려워지는 상황을 "가상함수 호출 비용"이라고 말한 것입니다. 가상 함수와 같은 간접 분기문 호출이 시간이 더 걸린다는 이론이 아니라 실제 현상이고요. 마이크로아키텍처 연구하는 사람들이 이거 개선하려고 많은 노력을 하고 있어요.
최종욱 2008/12/11 15:15 #
네. 저도 그렇게 생각하고 있습니다. ^^; 수업 시간에도 그렇게 배웠구요. 하지만 그에 정 반대되는 결과에 깜짝 놀랐습니다. 특수한 케이스에는 달라질 수도 있는건가? 싶습니다.
object 2008/12/11 13:56 # 답글
그런데 소스 코드 받아서 전혀 수정하지 않고 돌려봤는데요. Virtual이 항상 느립니다. 절대함수라는 말은 들어본 적이 없고 그냥 비가상함수 정도가 적당해 보이고요. VC++, ICC, GCC 윈도우 위에서 돌렸는데 모두 final이 훨씬 빠릅니다. 10억을 줬을 때 final은 2초가 걸리고 virtual은 6~7초가 걸립니다. 자바는 제가 자바를 잘 몰라서 그냥 최신 JDK에서 javac Final.java로 컴파일하고 java Final 10억 줬어요. 여기서는 final이 14초. virtual이 9초가 걸려서 virtual이 빠른데요. 그래도 C++보다는 훨씬 느립니다. 제가 뭘 잘못했나요?
최종욱 2008/12/11 15:17 #
글쎄요. 저도 잘 모르겠습니다 ^^; 앞으로 virtual 함수와 final 함수로 부르는게 나을까요?
object 2008/12/11 14:04 # 답글
자바는 제가 잘 모르지만 C++은 직접 어셈블리를 까서 보면요 (직접 확인했어요):Final로 짠 코드는 바로 함수가 짧으니 인라이닝이 됩니다. 그래서 엄청나게 빠르죠.
Virtual로 짠 코드는 일반 최적화를 하더라도 함수 호출 대상을 컴파일 시간에 알 수가 없으므로 간접분기문으로 구현이 됩니다. vtable을 읽고 레지스터에 최종 분기 목적지를 계산하고 call eax 같은 걸로 부릅니다. 그래서 6~7초 걸리죠.
그러나 여기서 PGO(Profiled Guided Optimization)을 하면, virtual 코드도 인라이닝이 가능해집니다. 지금 짜신 코드는 사실 정확한 벤치마킹을 할 수 없습니다. 그 이유는 잠시 뒤에 설명하고.. 어쨌든 지금 이 코드는 아무리 가상함수로 구현했다 하더라도 분기 목적지가 유일합니다. for 루프안에서 이뤄지는 가상함수의 목적지가 유일하죠. 그래서 프로파일링을 해보면 아무리 가상함수라도 불리는 함수가 일정하기 때문에 speculation을 할 수가 있습니다.
그래서 어떻게 바뀌냐면: "만일 분기 목적지가 Toggle::activate와 같으면 바로 인라인된 코드를 실행하라. 아니라면 그냥 간접분기문으로 호출하라" 로 최적화가 됩니다.
53: val = toggle->activate().value();
001E1075 mov edx,dword ptr [esi]
001E1077 mov eax,dword ptr [edx+4]
001E107A cmp eax,offset Toggle::activate (1E1000h)
001E107F jne $LN23+1Ah (1E1F30h)
001E1085 xor eax,eax
001E1087 cmp byte ptr [esi+4],al
001E108A sete al
001E108D mov byte ptr [esi+4],al
001E1090 mov eax,esi
001E1092 sub edi,ebx
001E1094 mov al,byte ptr [eax+4]
001E1097 jne main+65h (1E1075h)
보다시피 먼저 분기목적지가 예측값과 맞는지를 비교하죠. 맞으면 바로 인라인된 코드를 실행하고 아니라면 1E1F30h 주소로 가서 백업 코드를 실행합니다.
이렇게 PGO를 수행하면 실행시간이 약 1초가 줍니다. 그래도 6초 정도로 Final.exe보다는 3배 이상 느리죠. 왜냐면 보다시피 분기목적지와 같은지 비교하는 구문 때문에 느려지는 것입니다.
object 2008/12/11 14:05 # 답글
왜 지금 짜신 벤치마크가 정확하지 않냐면.. 제가 맨 처음 말씀드렸듯이 가상함수가 어렵다는 이야기의 본질은 가상함수의 목적지를 예측하기 어려워서 벌어지는 것입니다. 이 경우는 예측이 아주 쉽죠. PGO를 쓰지 않더라도 최신 CPU는 분기목적지를 예측하는 장치가 있고 (BTB라고 합니다) 예측이 잘 됩니다. 그러니까 PGO를 하나 안 하나 성능 차이가 별반 없는 것입니다. 정확하게는 VTune으로 프로파일링을 해봐야겠는데 (BTB miss를 체크할 수 있습니다) 이건 귀찮아서 안 해봤어요.
object 2008/12/11 14:09 # 답글
요약하면: 제 컴퓨터 C2D 듀얼 코어에서는 C++ Final이 Virtual보다 3배 이상 빠르다. 직접 어셈 까보면 빠를 수 밖에 없어요.. (Final 2초, Virtual 6~7초)자바의 경우 Final은 14초로 매우 느렸고, Virtual은 9초로 많이는 아니지만 어쨌든 C++보다는 느리긴 느리다. 그러나 이 경우도 설명했듯이 분기 목적지가 일정하기 때문에 "가상함수비용"을 테스트하기에는 적당한 벤치마킹이 아니다.
그나저나 자바의 기본 함수는 모두 버추얼이죠? Final이 어떻게 바이너리로 변환되는지 아세요? 이걸 모르고서는 정확한 분석을 할 수가 없습니다.
포스팅으로 남기려다보니 너무 시간이 걸려서 (내일까지 뭐 발표할게 있어서 바쁜데 그만 -_-) 대충 댓글로 남깁니다. 나중에 시간이 나면 정식으로 포스팅 할 수도 있습니다.
아참 시간 측정은 주어진 코드는 정밀도가 낮아서 time으로 했습니다. (cygwin)
최종욱 2008/12/11 15:20 #
저도 시험 끝나는 대로 좀 더 정교한 코드로 살펴보도록 하겠습니다. ^^ 이후에 자바 클래스 파일 디컴파일러로 해석을 해보도록 하겠습니다.
object 2008/12/11 14:19 # 답글
정말로.. 상식적으로.. C++ final.cpp가 virtual.cpp보다 빠르다는 것은 말도 안 됩니다 :)[/cygdrive/z/temp/virtual_cost/Release] time ./final.exe 1000000000
true
false
2 seconds.
real 0m2.114s
user 0m0.000s
sys 0m0.000s
[/cygdrive/z/temp/virtual_cost/Release] time ./virtual.exe 1000000000
true
false
6 seconds.
real 0m5.975s
user 0m0.000s
sys 0m0.000s
[/cygdrive/z/temp/virtual_cost/Release]
최종욱 2008/12/11 15:13 #
g++로 모든 옵션을 끄고 컴파일을 해보니 결과가 달라지는군요. 하지만 virtual.exe가 18초, final.exe가 19~20초로 여전히 virtual이 더 빠르네요. 컴파일 옵션은 무엇으로 주셨는지 궁금합니다. c++에는 제가 약해서...
object 2008/12/11 15:34 #
그냥 g++ -O3, icl -O3, VC++에서는 그냥 Relase 모드로 했습니다. 리눅스 64비트머신에서 다시 돌려봤어요. 결과에는 변함 없습니다. Final은 2초, Virtual은 12초. 파이널이 이렇게 빠른 이유는 인라이닝.-O0 최적화를 끄면 Final은 22초 Virtual은 24초가 걸리는데, 그래도 버추얼이 느릴수 밖에 없습니다. 어차피 둘이 펑션 호출 비용은 들고, 이 같이 가상함수라해도 대상이 일정한 경우에는 함수 호출 비용은 비슷하지요. 대신 버추얼은 목적지 계산 코드가 들어가서 역시 Final보다 무조건 느릴 수 밖에 없습니다. 추측이 아니라 어셈단위에서 확인한 것입니다. 따라서 C++ 코드는 실험 결과가 설명이 잘 됩니다.
같은 머신에서 java로 해봤습니다. 10억을 주니까 멈추지를 않네요 -_- Sun Java가 아니라 gij이군요. 천만을 줘도 28초가 걸리네요. 자바 성능이야 당연히 JVM에 크게 달려있으니... 이건 그냥 흘려 들으세요.. ^^
제가 인자로 준 10억을 잘못쳤나 의심하며 조심하며 몇 번 테스트한 결과입니다.
최종욱 2008/12/11 15:44 #
-O3을 하니 object님과 같은 결과가 나왔습니다. 감사합니다. :-)
object 2008/12/11 14:21 # 답글
[/cygdrive/z/temp/virtual_cost] time java Final 1000000000true
false
14 seconds.
real 0m14.706s
user 0m0.015s
sys 0m0.015s
[/cygdrive/z/temp/virtual_cost] time java Virtual 1000000000
true
false
8 seconds.
real 0m9.014s
user 0m0.000s
sys 0m0.015s
[/cygdrive/z/temp/virtual_cost] uname -a
CYGWIN_NT-6.0-WOW64 object-PC 1.5.25(0.156/4/2) 2008-06-12 19:34 i686 Cygwin
[/cygdrive/z/temp/virtual_cost]
아아.. 댓글로 주렁주렁 남겨서 죄송해요.. 저도 도저히 이럴 수는 없어! 하면서 급하게 테스트하느라.. ㅎㅎ
최종욱 2008/12/11 15:19 #
아뇨, object님의 관심에 매우 감사합니다. ^^찬찬히 더 살펴보는 것도 재미있을 듯 싶네요.
object 2008/12/11 15:35 #
저도 감사합니다. 저도 나중에 시간 좀 내서 더 자세히 테스트 해봐야겠네요.
최종욱 2008/12/11 16:05 # 답글
중간 정리) -O3 옵션을 준 경우, C++ Final의 경우 대략 2초 정도로 줄었습니다. JDK 6.0 server 모드의 경우에는 Final과 Virtual 모두 1초로 줄었습니다. 자세한 내용은 다음에 시간이 나면 더 정리하도록 하겠습니다.
DAK-DAK 2008/12/11 18:34 # 답글
우와.. 엄청난 글!!! 잘 봤습니다.. ^^;;;;