Home // Blog
Home // Notice
Home // Tag Log
Home // Location Log
Home // Media Log
Home // GuestBook
C++ 이야기 서른번째: boost::shared_ptr performance
Posted at 2009/06/28 13:44 //
in S/W개발/C++ 이야기 //
by
최근 제가 진행하던 프로젝트에서 boost::shared_ptr를 상당히 많이 사용해 왔었는데, 성능이 좋게 나오질 않아서 혹시나 해서 오늘 성능을 간단하게 측정해 봤더니 속도 차이가 상당히 많이 나는군요. 성능이 좀 느리겠거니 했는데 상상 이상이었습니다.(실은 이 문제 때문에 한바탕 홍역을 치뤘죠. ㅠ.ㅠ)
다음은 테스트 프로그램입니다.
$ cat shared.cpp
#include <iostream>
#include <boost/shared_ptr.hpp>
using namespace std;
using namespace boost;
class AClass
{
public:
void Op() const
{
++i;
}
int Get() const
{
return i;
}
private:
mutable int i;
};
#ifdef SHARED_PTR
void Foo(const shared_ptr<AClass>& p)
{
p->Op();
}
#elif defined(SHARED_PTR_COPY)
void Foo(shared_ptr<AClass> p)
{
p->Op();
}
#else
void Foo(AClass* p)
{
p->Op();
}
#endif
int
main(int argc, char* argv[])
{
#if defined(SHARED_PTR) || defined(SHARED_PTR_COPY)
shared_ptr<AClass> p(new AClass());
#else
AClass* p = new AClass();
#endif
int maxCnt = 1000000000;
if (argc >= 2)
maxCnt = atoi(argv[1]);
for (int i = 0; i < maxCnt; ++i)
{
Foo(p);
}
cout << p->Get() << endl;
return 0;
}
이 테스트 프로그램은 세 가지 경우를 테스트하기 위한 것입니다.
1. raw pointer를 사용하는 경우: SHARED_PTR 또는 SHARED_PTR_COPY 가 정의되지 않은 경우입니다.
2. shared_ptr을 사용하는 경우: SHARED_PTR 이 정의된 경우입니다. Foo()를 호출할 때, const shared_ptr reference를 넘기기 때문에 복사가 일어나질 않습니다.
3. shared_ptr을 쓰면서 복사를 하는 경우: SHARED_PTR_COPY가 정의된 경우입니다. Foo()를 호출할 때, shared_ptr 객체를 넘기기 때문에 복사가 한 번 발생하고 이때, shared_ptr의 복사 생성자가 수행되면서 내부적으로 reference count를 증가시키고, 다시 함수에서 리턴될 때, reference count를 감소시키게 됩니다. 그리고 reference count 증가 및 감소는 보통 atomic operation으로 수행되기 때문에 보통의 증가/감소보다는 속도가 느린 걸로 알고 있습니다.
이 세가지에 대해 컴파일러 최적화 옵션 및 BOOST 옵션을 약간 달리 하면서 실행을 해 보았습니다. 다음은 테스트를 위한 Makefile 입니다.
$ cat Makefile
all: shared1 shared2 shared3 shared_copy1 shared_copy2 shared_copy3 plain1 plain2 plain3
#CFLAGS=-pg -g
#LDFLAGS=-pg -g
# CASE #1
CFLAGS1=
# CASE #2
CFLAGS2=-O3
# CASE #3
CFLAGS3=-O3 -DBOOST_SP_DISABLE_THREADS
LDFLAGS=
shared1: shared1.o
g++ $(LDFLAGS) -o shared1 shared1.o
shared_copy1: shared_copy1.o
g++ $(LDFLAGS) -o shared_copy1 shared_copy1.o
plain1: plain1.o
g++ $(LDFLAGS) -o plain1 plain1.o
shared1.o: shared.cpp
g++ $(CFLAGS1) -c -o shared1.o -DSHARED_PTR shared.cpp
shared_copy1.o: shared.cpp
g++ $(CFLAGS1) -c -o shared_copy1.o -DSHARED_PTR_COPY shared.cpp
plain1.o: shared.cpp
g++ $(CFLAGS1) -c -o plain1.o -DPLAIN shared.cpp
shared2: shared2.o
g++ $(LDFLAGS) -o shared2 shared2.o
shared_copy2: shared_copy2.o
g++ $(LDFLAGS) -o shared_copy2 shared_copy2.o
plain2: plain2.o
g++ $(LDFLAGS) -o plain2 plain2.o
shared2.o: shared.cpp
g++ $(CFLAGS2) -c -o shared2.o -DSHARED_PTR shared.cpp
shared_copy2.o: shared.cpp
g++ $(CFLAGS2) -c -o shared_copy2.o -DSHARED_PTR_COPY shared.cpp
plain2.o: shared.cpp
g++ $(CFLAGS2) -c -o plain2.o -DPLAIN shared.cpp
shared3: shared3.o
g++ $(LDFLAGS) -o shared3 shared3.o
shared_copy3: shared_copy3.o
g++ $(LDFLAGS) -o shared_copy3 shared_copy3.o
plain3: plain3.o
g++ $(LDFLAGS) -o plain3 plain3.o
shared3.o: shared.cpp
g++ $(CFLAGS3) -c -o shared3.o -DSHARED_PTR shared.cpp
shared_copy3.o: shared.cpp
g++ $(CFLAGS3) -c -o shared_copy3.o -DSHARED_PTR_COPY shared.cpp
plain3.o: shared.cpp
g++ $(CFLAGS3) -c -o plain3.o -DPLAIN shared.cpp
test: test1 test2 test3
test1: shared1_test shared1_copy_test plain1_test
shared1_test: shared1
time ./shared1 $(CNT)
shared1_copy_test: shared_copy1
time ./shared_copy1 $(CNT)
plain1_test: plain1
time ./plain1 $(CNT)
test2: shared2_test shared2_copy_test plain2_test
shared2_test: shared2
time ./shared2 $(CNT)
shared2_copy_test: shared_copy2
time ./shared_copy2 $(CNT)
plain2_test: plain2
time ./plain2 $(CNT)
test3: shared3_test shared3_copy_test plain3_test
shared3_test: shared3
time ./shared3 $(CNT)
shared3_copy_test: shared_copy3
time ./shared_copy3 $(CNT)
plain3_test: plain3
time ./plain3 $(CNT)
clean:
rm -rf *.o *.dSYM shared1 shared2 shared3 shared_copy1 shared_copy2 shared_copy3 plain1 plain2 plain3 core* gmon.*
1. 테스트 케이스 1: 컴파일러 옵션으로 아무것도 주지 않을 경우 --> CFLGAS1 사용
2. 테스트 케이스 2: 컴파일러 옵션으로 -O3 를 준 경우 --> CFLAGS2 사용
3. 테스트 케이스 3: 컴파일러 옵션으로 -O3 -DBOOST_SP_DISABLE_THREADS 사용. BOOST_SP_DISABLE_THREADS를 켜면 reference count 값을 변경시킬 때, atomic operation을 사용하지 않게 됩니다.
이렇게 테스트 프로그램을 작성하고, 테스트를 해 봤더니 다음과 같은 결과가 나오더군요.
$ make
$ make test
time ./shared1 1000000000
1000000000
15.66 real 15.52 user 0.04 sys
time ./shared_copy1 1000000000
1000000000
93.33 real 92.58 user 0.24 sys
time ./plain1 1000000000
1000000000
8.54 real 8.48 user 0.02 sys
time ./shared2 1000000000
1000000000
2.81 real 2.79 user 0.00 sys
time ./shared_copy2 1000000000
1000000000
37.86 real 37.62 user 0.08 sys
time ./plain2 1000000000
1000000000
0.66 real 0.66 user 0.00 sys
time ./shared3 1000000000
1000000000
2.84 real 2.79 user 0.00 sys
time ./shared_copy3 1000000000
1000000000
5.74 real 5.70 user 0.01 sys
time ./plain3 1000000000
1000000000
0.66 real 0.66 user 0.00 sys
(위 테스트는 looping overhead, function call overhead 를 정확히 측정하지 않았기 때문에 아주 정확한 수치라고는 주장할 수 없음을 미리 밝힙니다. 정확한 수치를 알아내기 위해서는 좀 더 잘 설계된 테스트 케이스를 사용해야 할 것입니다. 다만 위 테스트 결과를 사용하더라도 이 글에서 주장하고자 하는 바를 위한 충분한 근거가 되리라고 생각합니다.)
우선 테스트 케이스1와 테스트 케이스2, 테스트 케이스 3에서 공히 shared_copy > shared > plain 결과가 나온다는 걸 확인할 수 있습니다. BOOST_SP_DISABLE_THREADS 를 켜게 되면 shared_copy에만 영향을 미친다는 걸 알 수 있습니다. 이건 아까 말씀드린 대로 atomic operation이 disable되기 때문인 것으로 추측됩니다. 이상 결과로 보건대 shared_ptr을 쓰실 때는 다음과 같은 점에 유의하셔야 할 것 같습니다.
1. No free lunch. 좋은 게 있으면 그에 대한 비용이 있기 마련이라는 것이죠. shared_ptr는 일반 포인터에 비해 overhead가 있다는 점을 염두해 두셔야겠습니다.
2. 단순 접근하는 overhead도 2.81/0.66(테스트 케이스 2)으로 상당합니다.
3. 복사 생성자 overhead는 37.86/0.66(테스트 케이스 2)으로 더 크다는 것을 확인할 수 있습니다. BOOST_SP_DISABLE_THREADS를 켜더라도 5.74/0.66으로 여전히 크다는 것을 알 수 있습니다. 따라서 될 수 있으면 복사를 shared_ptr도 복사는 피하셔야 하겠습니다.
제가 테스트한 환경은 다음과 같습니다.
$ uname -a
Darwin MyMac.local 9.7.0 Darwin Kernel Version 9.7.0: Tue Mar 31 22:52:17 PDT 2009; root:xnu-1228.12.14~1/RELEASE_I386 i386
$ g++ --version
i686-apple-darwin9-g++-4.2.1 (GCC) 4.2.1 (Apple Inc. build 5564)
여러분 환경에서는 어떤 결과가 나오는지 실험해 보시고, 트랙백 한 번 남겨 주시면 감사하겠습니다. ^^
소스 코드 첨부합니다(looping overhead를 측정하기 위해 본문 소스에서 약간 더 수정했습니다).


shared.tar.gz

2009/07/01 01:37 [수정/삭제] [답글]
좋은거 하나 배우고 갑니다. :)
2009/07/02 21:15 [수정/삭제]
어느새 다녀갔네?
2009/07/01 22:32 [수정/삭제] [답글]
아는대로 써 보는 거랑, 실제 프로파일링 등으로 눈으로 성능을 확인하는거랑 상당히 차이가 나는 경우가 많더라구요.
저 같은 경우 STL map 이 의외로 성능이 안 좋아서 많이 놀랬던 적이 있습니다.(VC 에서지만요) 랩핑도 가려 써야겠네요.. 에고 참 신경 써야 할 게 많습니다.
2009/07/02 21:17 [수정/삭제]
그러게요. 저도 쓰면서 성능이 안 좋으면 어떡하지 하고 있었는데... 아니나 다를까 성능 차이가 꽤 되더라구요. 근데 그게 몇 번 불리지 않는거라면 괜찮지만 워낙 C++에서 포인터를 많이 쓰다 보니 자연스레 전체 실행시간에서 차지하는 시간이 또 상당하더라구요. 저도 이번에 좋은 거 하나 배웠습니다.
2009/07/07 06:43 [수정/삭제] [답글]
아~ 다윈. 부럽다~~~
2009/07/08 22:49 [수정/삭제] [답글]
역시 뭐든지 트레이드 오프로군요. 포인터가 흘러가는 범위가 프로그래머가 감당 가능한 수준이라면 가급적 일반 포인터와 레퍼런스로 버티는게 좋을 것 같네요.
이에 관련해서 궁금한게 있는데, 자바나 C#의 가비지 컬렉팅과 부스트의 shared_ptr를 비교하면 성능적으로 어느 정도의 차이가 나는지 궁금합니다.
2009/08/24 11:37 [수정/삭제] [답글]
감사합니다. 항상 잘 보고 있습니다. :)
2009/08/29 00:08 [수정/삭제]
댓글 남겨 주셔서 감사합니다
2009/09/03 21:07 [수정/삭제] [답글]
말씀하신 프로젝트에서, boost::shared_ptr 을 제거하는 것으로 성능문제가 해결되셨나요?
적으신 결과를 보면, 배율로 따지자면 최대 150배정도의 차이지만, 시간으로 따지자면 10억번 루프 돌려서 100초 미만의 차이가 나고있습니다.
돌아가는 동안 포인터 복사하고 참조하는 연산을 10억번 하는 프로그램이라면, 전체 런타임에서 100초는 미미한 시간이 아닌가 하는 생각이 들어서 질문드립니다.
2009/09/04 18:29 [수정/삭제]
shared_ptr이 성능에 영향을 미친다는 것을 알아낸 건 gprof 결과를 보고 알아낸 것이기 때문에 shared_ptr 을 안쓰고 나서 성능이 개선되긴 했습니다. 그런데 전체 소프트웨어로 봤을 때는 shared_ptr만 영향을 미친게 아니라 다른 것들도 영향을 미치고 있었기 때문에 문제가 해결된 것이라고 보기는 힘들구요. 그리고 제가 작성한 library 및 그걸 기반으로한 app이 shared_ptr 및 vector<shared_ptr>을 많이 쓸 수 밖에 없는 구조이기 때문에 당연히 성능 향상은 있었습니다. 그런데 shared_ptr을 intrusive_ptr 이란 걸로 교체해서 성능을 향상 시켰습니다. 아예 없애진 않구요.
2009/09/04 20:54 [수정/삭제]
vector에 넣고 쓸정도로 많으면 문제가 될수도 있겠네요.
감사합니다. 덕분에 intrusive_ptr 도 알게 되었네요.