C++ 이야기 서른번째: boost::shared_ptr performance

Posted at 2009. 6. 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를 측정하기 위해 본문 소스에서 약간 더 수정했습니다).