루프 속도 테스트
글쓴이: gilgil / 작성시간: 일, 2014/02/23 - 5:58오전
다음과 같은 코드가 있습니다.
test_1 과 test_65536 중에 어느 것이 더 빠른지 테스트를 해 보았습니다.
결론적으로 실행 결과는 비슷한 것으로 보이는데, 정확한 원인이 무엇일까요?
[test_1]
for (int j = 0; j < 65536; j++) // loop를 이용하여 "a++"을 65536번 실행 { a++; }
[test_65536]
a++; a++; a++; ... // 총 65536번
테스트 프로젝트 파일은 다음에서 받으실 수 있습니다.
http://www.gilgil.net/227344
Forums:
아래쪽은 코드사이즈가 커지기때문에 느려집니다.
아래쪽은 코드사이즈가 커지기때문에 느려집니다. 너무큰함수를 인라인했을때 느려지는것과 같은이유라고 생각되네요.
gilgil.net
네, 이 테스트를 하는 이유가 바로 "function을 inline으로 하면 무조건 빨라 지는가?"에 대한 구체적인 원인을 파악하기 위해서입니다.
그런데 아직 정확한 원인은 모르겠네요.
www.gilgil.net
a++;가 65536번 반복될 경우 컴파일러가
a++;가 65536번 반복될 경우 컴파일러가 자동으로 a+=65536으로 변환할 것 같다는 느낌이 듭니다만 정확한 건 컴파일된 오브젝트 코드를 살펴봐야 할 것 같습니다.
gilgil.net
최적화를 하면 당연히 O(1)에서 a += 65536을 해 줍니다. 그래서 최적화 옵션을 빼고 컴파일을 했습니다.
링크 아래 부분의 출력된 assembly code를 보시면 확인할 수 있습니다.
www.gilgil.net
원하시는 대답은 아니겠습니다만...
이걸로 결론을 내리기에는 효용성이 없는 실험이라고 생각합니다.
만일 Assembly 수준에서 직접 작업하시면서 분석하신다면 모르겠습니다만...
뭐랄까... Compiler 최적화 과정을 만드시는게 아니라면 C 수준에서 작업하신다음 이런 실험을 해서 결과를 도출한다는 것이
모래 위의 성이라고 할까요?
설령 compiler 최적화기를 작업하시는 거라고 해도 Assembly 수준을 연구하신 후에 하시는게 좋겠죠.
추측일 뿐입니다만 원하시는 대답을 해드리자면 최근의 Intel CPU 는 loop 을 염두한 명령어 해석 및 분기예측을 하는 것으로 알고 있습니다.
저도 가끔 비슷한 작업을 하는데 그 이유는 성능에 대한 잘못된 생각(성급한 최적화)을 가진 programmer 들에게 반례를 들기 위해서죠.
그런 사람들에게는 적절한 반례만 드는 것으로 충분하다고 생각해서 더이상 분석은 하지 않습니다.
괜히 분석했다가는 그걸 가지고 또 잘못된 생각(시간이 흐르면서 변하게 될 그다지 가치없는 최적화 요령)을 하게 될 것이 더 두렵습니다.
gilgil.net
> 괜히 분석했다가는 그걸 가지고 또 잘못된 생각(시간이 흐르면서 변하게 될 그다지 가치없는 최적화 요령)을 하게 될 것이 더 두렵습니다.
동의합니다. 저 또한 어설프게 최적화를 한다고 하는 것보다는, 보편적인 방식으로 코딩을 해 놓으면(똑똑한 컴파일러가 알아서 최적화를 해 주기 때문에) 더 효율적인 경우를 많이 봐 왔었죠.
그런데 가끔 가다 보면 프로그램 성능 최적화를 위해서 아직도 이러한 것들을 고려해야 할 필요가 있는 것이 사실입니다.
제 경험상 대표적인 예가 바로 false sharing에 의한 성능 저하였습니다. 전 직장에서 이거 가지고 엄청 고생했었죠. http://www.gilgil.net/63456
가끔씩은 이런 논의를 할 필요는 있다고 봅니다. ^^
지금 이번 테스트를 하게 된 이유는 (C/C++ 프로그래머가 있는데) 클래스의 모든 member function을 inline으로만 만들려고 하는 사람을 봤기 때문입니다. 그 사람 말로는 "inline으로 해 놓으면 성능상으로는 최고 빠르다"라고 얘기를 하고 있었구요, 저는 '과연 그럴까?'라는 반문을 갖게 된 것입니다. 그래서 테스트를 해 본 겁니다.
test_1 : 코드의 크기는 작은 반면에 LOOP 처리 루틴이 들어가 있다 - cache의 장점이 부각될 수 있다.
test_65536 : 코드의 크기가 크지만, LOOP 처리 루틴이 들어가 있지 않다 - 코드가 크기 때문에 cache의 장점은 활용될 수 없다.
어설프게 이렇게 예상으로 하고 테스트를 해 본 겁니다(OP code의 테스트이지, C언어 최적화 테스트가 아님).
그런데 하면서 보니 따져야 할 부분이 한둘이 아니네요.
가면 갈 수록 미궁입니다( http://www.gilgil.net/227344#comment_227384 ).
branch prediction, pipeline, loop unrolling, ... 아직은 정확한 원인을 모르겠습니다.
www.gilgil.net
Cache 민감성은 다른 문제긴 하죠.
그것은 사실 Assembly 로도 접근이 안 되는 영역이니까요. 다만 CPU architecture 에 의해 분석은 되고, Assembly 는 CPU architecture 를 이해하는데 도움이 된다는 것 정도...
물론 실험하시는 loop 도 어느정도 cache 민감성이 들어갑니다만 영향력은 적은 편입니다.
Cache 민감성 문제는 거대한 memory 를 다룰 때 의미가 크죠.
Code 영역의 cache 민감성을 다룰려면 일단 program 이 커야 하고, jump 거리가 멀어야 합니다. 그런 것을 측정하기에는 제시한 program 이 너무 작고 단순합니다.
Inlining 에 의한 code 비대화가 발생하여 cache 효율 감소를 예상하는 것 같습니다만 이런 단순한 직선제어흐름은 prefetching 기술에 의해 대부분 해결될겁니다.
False sharing 이 발생한 program 은 어떤 것이었나요? 좀 궁금하군요.
C++ class 의 member 함수 inlining 은 개인적으로 coding 이 편하다는 장점이 가장 큰 것 같아요.
그리고 증분 build 가 되지 않아 크고 아름다운 build 시간을 안겨주지요.
개인적으로는 build 시간을 아끼는 것을 program 실행시간 아끼는 것보다 선호해서
귀찮음에도 불구하고 header 에 member 를 다 정의하지는 않습니다.
C++ 의 거지같은 특성이라고 생각합니다.
그 사항에 대한 답변을 드리자면 요새는 link 시점 최적화를 많이 해서 성능향상 기대는 크지 않다가 되겠습니다.
그나마 기대가 큰 부분은 전략 pattern 에서 활용되는 C++ 함수객체 정도 되겠군요.
물론 그 영역에서도 code 비대화에 의한 역습을 고민해볼 수 있겠습니다만...
뭐, 어쨌든 CPU architecture 를 공부하는 것이 아니고, 실전적인 효용성을 이야기하자면 성능에 대한 의미부여는 할 수 없다고 봅니다.
gilgil.net
네트워크 장비(L7 switch)를 만드는 회사에 있었습니다.
Gbps 이상을 처리해야 하니까 CPU 성능 이슈는 중요하죠(요즘은 10Gbps가 기본).
보통 패킷 수집 thread는 하나이고 패킷 처리 thread는 여러개인데,
패킷 처리를 하다 보면 패킷 처리 thread간에 동기화를 할 필요가 있는 부분이 존재합니다.
동기화를 하기 위해서는 critical section 혹은 recursive mutex가 사용이 되는데,
소스 코드 refactoring 과정에서 mutex객체에 alignment를 주지 않아서
패킷 라우팅 시간이 2배나 늘어 나는 현상이 있었죠(서로 다른 thread에서 가까이 있는 mutex 객체들에 동시 접근).
성능 저하의 이유가 소스 코드상레벨에서 나타나는 게 아니라 버그(?) 잡는데 꽤 고생을 했었습니다.
결국 mutex 객체의 alignment를 주는 것으로 해결.
www.gilgil.net
false sharing 문제는 다른 관점으로 보면 문맥화를 위한 기법이 부족하다고 할까...
http://minjang.egloos.com/1845004
이 글의 댓글 중 까막님의 code 같은 기법을 활용할 수도 있겠죠.
저는 가끔 cache 의 programmability 가 너무 떨어지는게 아닌가라는 생각을 합니다.
그렇다고 programmability 를 추가기에는 들어가는 노력이 얻는 것에 비해 너무 클테니
관련있는 programmer 가 알아서 해야겠지만 매우 아쉬운 부분이긴 하죠.
말씀하신대로 다중실행기에 의한 동작을 고민할 때는 공유영역에 대한 cache 처리를 더 고민해야겠죠.
그래서인지 전통적인 compiler 언어들의 실행 code 가 변경되지 않지요.
만일 interpreter 처럼 code 의 변형을 적극적으로 다룬다면 code 영역에 대한 cache 무효화 문제가 발생할 수도 있겠네요.
댓글 달기