RValue Reference에 의한 STL의 성능향상 테스트

C++0x 2010. 10. 18. 07:00 Posted by 알 수 없는 사용자

트위터에서 @All2one님을 통해서 GCC 컴파일러에서 RValue Reference Move Semantics를 사용했을 경우와 그렇지 않은 경우 STL에서 얼마만큼의 성능 차이가 나는지 테스트를 한 결과를 보았습니다.

사이트 주소는 http://cpp-next.com/archive/2010/10/howards-stl-move-semantics-benchmark/

입니다.

 

이것은 GCC 컴파일러를 사용한 경우라서 저는 VC++을 사용하여 어떤 결과가 나오는지 궁금해서 테스트 해 보았습니다.

RValue Reference을 사용하지 않는 가장 최신의 컴파일러는 VC++ 9(VS2008)이므로 VC++10 VC++9를 같은 코드로 컴파일한 후 실행하였습니다.

 

먼저 테스트한 컴퓨터의 하드웨어 사양은 아래와 같습니다.


 

테스트 코드는 아래와 같습니다(GCC로 테스트한 것과 같은 코드입니다).

#include <vector>

#include <iostream>

#include <time.h>

#include <set>

#include <algorithm>

 

const unsigned N = 3001;

 

extern bool some_test;

 

std::set<int>

get_set(int)

{

    std::set<int> s;

    for (int i = 0; i < N; ++i)

        while (!s.insert(std::rand()).second)

            ;

    if (some_test)

        return s;

    return std::set<int>();

}

 

std::vector<std::set<int> >

generate()

{

    std::vector<std::set<int> > v;

    for (int i = 0; i < N; ++i)

        v.push_back(get_set(i));

    if (some_test)

        return v;

    return std::vector<std::set<int> >();

}

 

float time_it()

{

    clock_t t1, t2, t3, t4;

    clock_t t0 = clock();

    {

    std::vector<std::set<int> > v = generate();

    t1 = clock();

    std::cout << "construction took " << (float)((t1 - t0)/(double)CLOCKS_PER_SEC) << std::endl;

    std::sort(v.begin(), v.end());

    t2 = clock();

    std::cout << "sort took " << (float)((t2 - t1)/(double)CLOCKS_PER_SEC) << std::endl;

    std::rotate(v.begin(), v.begin() + v.size()/2, v.end());

    t3 = clock();

    std::cout << "rotate took " << (float)((t3 - t2)/(double)CLOCKS_PER_SEC) << std::endl;

    }

    t4 = clock();

    std::cout << "destruction took " << (float)((t4 - t3)/(double)CLOCKS_PER_SEC) << std::endl;

    std::cout << "done" << std::endl;

    return (float)((t4-t0)/(double)CLOCKS_PER_SEC);

}

 

int main()

{

    std::cout << "N = " << N << '\n';

    float t = time_it();

    std::cout << "Total time = " << t << '\n';

}

 

bool some_test = true;

 

 

< 결과 >


 


 


이 결과를 보면 생성과 알고리즘을 사용했을 때 많은 차이가 나는 것을 알 수 있습니다.

기존에 STL의 알고리즘을 자주 사용한 경우라면 VC++ 10으로 컴파일만 해도 어느 정도의 성능 향상을 얻을 수 있을 것 같습니다.

 

이 글은 MSDN 글, "Solving The Dining Philosophers Problem With Asynchronous Agents"를 참고하여 작성되었습니다.

Asynchronous Agents Library로 Dining Philosophers 문제 해결하기 - 1

자, 이제 본격적으로 코드를 살펴보기 전에 메시지 블록이 무엇인지 먼저 짚고 넘어가겠습니다. AAL액터모형을 사용한다고 말씀드렸습니다. 또한, 액터모형에서 액터들은 메시지만으로 통신한다고 말씀드렸죠. 이 때 메시지를 받는 대상 혹은 메시지의 출처의 역할을 하는 것이 메시지 블록입니다. 전자의 경우 목적(target) 블록이라 하고, 후자는 원천(source) 블록이 됩니다.

전회에서 이번 예제에 쓰이는 네가지 메시지 블록을 소개했었는데요. unbounded_buffer는 목적 및 원천으로 쓰이며 큐와 같이 여럿의 메시지를 담고 있을 수 있는 놈입니다. overwrite_buffer는 하나의 변수처럼 값 하나만을 지니며, 새로 메시지가 올 경우 기존 값은 덮어씌여집니다. 역시 원천으로도 쓰일 수 있으며, 이 경우 사본을 보냅니다. 반면, call목적 블록으로만 쓰여 메시지 도착 시 특정 함수개체를 불러주는 기능을 합니다. join은 이번 예제에서 핵심 역할을 하는 블록으로서 여러 메시지를 동시에 기다려 하나로 묶어 출력하는 기능을 합니다.

먼저 가장 간단한 Chopstick 클래스를 살펴보죠.

   22 class Chopstick{

   23     const std::string m_Id;

   24 public:

   25     Chopstick(std::string && Id):m_Id(Id){};

   26     const std::string GetID()

   27     {

   28         return m_Id;

   29     };

   30 };


이와 같이 젓가락 식별용의 문자열을 가질뿐입니다. 생성자에서 r-value 참조를 쓰고 있다는 것 정도가 주목할만한 사항이겠군요.

다음은 ChopstickProvider로 다음과 같이 단순히 typedef입니다.

   34 typedef Concurrency::unbounded_buffer<Chopstick*> ChopstickProvider;


unbounded_buffer 메시지 블록을 이용해 메시지로 젓가락을 받으면 담고 있다가 철학자의 요청이 있으면 제공하는 역할을 합니다. 물록 철학자가 한입 먹고 나선 다시 젓가락을 놓으면 다시 받아놓는 역할도 합니다. 이 예제에서는 unbounded_buffer의 개수무제한(unbounded) 특성이 사실 굳이 필요 없습니다만 그래도 unbounded_buffer의 move semantic이 필요하기에(이 점에서 사본을 보내는 overwrite_buffer와는 다르죠) 이를 쓰는 것입니다.

다음이 대망의 Philosopher 클래스가 되겠습니다. 먼저, Concurrency::agent에서 public 상속을 받고 있는 것을 확인할 수 있습니다. 말씀드린 것처럼 각 철학자가 액터가 되어 독립적으로 동작하기 (즉, 별도 스레드로) 위함입니다.

   35 class Philosopher : public Concurrency::agent

   36 {

   37     ChopstickProvider* m_LeftChopstickProvider;

   38     ChopstickProvider* m_RightChopstickProvider;

   39 

   40 public:

   41     const std::string m_Name;

   42     const size_t  m_Bites;

   43     Philosopher(const std::string&& name, size_t bites=10):m_Name(name),m_Bites(bites){};

   44     Concurrency::unbounded_buffer<ChopstickProvider*> LeftChopstickProviderBuffer;

   45     Concurrency::unbounded_buffer<ChopstickProvider*> RightChopstickProviderBuffer;

   46     Concurrency::overwrite_buffer<PhilosopherState> CurrentState;

   47     void run()

   48     {

   49 

   50         //run에서 제일 먼저 해야하는 것은 ChopstickProvider를 초기화하는 것입니다. 여기서는 receive를 통해 public 변수에 메시지가 도착하기를 기다리게 하는 방식으로 처리합니다:

   51 

   52         //ChopstickProvider들을 초기화합니다.

   53         m_LeftChopstickProvider  = Concurrency::receive(LeftChopstickProviderBuffer);

   54         m_RightChopstickProvider = Concurrency::receive(RightChopstickProviderBuffer);

   55 

   56         //이제 생각하다가 먹기를 반복해야 합니다. 그를 위해 아직 등장하지 않은 두 함수(PickupChopsticks과 PutDownChopsticks)를 이용하려고 합니다:

   57 

   58         for(size_t i = 0; i < m_Bites;++i)

   59         {

   60             Think();

   61             std::vector<Chopstick*> chopsticks(PickupChopsticks());

   62             Eat();

   63             PutDownChopsticks(chopsticks);

   64         }

   65 

   66         //남은 일은 run 메소드를 나가기 전에 정리 작업을 하는 것인데, 다른 곳에 쓰일 수 있도록 ChopstickProvider를 반환하고 에이전트의 상태를 완료로 설정하고 있습니다.

   67         Concurrency::send(LeftChopstickProviderBufferm_LeftChopstickProvider);

   68         Concurrency::send(RightChopstickProviderBuffer, m_RightChopstickProvider);

   69 

   70         this->done(Concurrency::agent_done);

   71     }

   72 

   73     std::vector<Chopstick*> PickupChopsticks()

   74     {

   75         //join 생성

   76         Concurrency::join<Chopstick*,Concurrency::non_greedy> j(2);

   77         m_LeftChopstickProvider->link_target(&j);

   78         m_RightChopstickProvider->link_target(&j);

   79 

   80         //젓가락을 듭니다.

   81         return Concurrency::receive (j);

   82     } 

   83     void PutDownChopsticks(std::vector<Chopstick*>& v)

   84     {

   85         Concurrency::asend(m_LeftChopstickProvider,v[0]);

   86         Concurrency::asend(m_RightChopstickProvider,v[1]);

   87     }

   88 private:

   89     void Eat()

   90     {

   91         send(&CurrentState,Eating);

   92         RandomSpin();

   93     };

   94     void Think()

   95     {

   96         send(&CurrentState,Thinking);

   97         RandomSpin();

   98     };

   99 };


그 다음으로 한쌍의 젓가락을 위한 두 ChopstickProvider 포인터 변수(m_LeftChopstickProvider, m_RightChopstickProvider)가 보입니다. 철학자 이름(m_Name)과 몇번 먹을지를 나타내는 변수(m_Bites), 생성자까지는 파악하시는데 어려움이 없을 겁니다.

ChopstickProvider (이 자체도 unbounded_buffer인데) 포인터를 템플릿 인자로 가지는 unbounded_buffer 변수 한쌍이 등장하는데요. (44,45줄) 철학자가 젓가락을 소유하고 있는 상황이 아니고 철학자와는 별개로 젓가락들이 존재하는 상황이기에 필요한 변수들입니다. 이 두 public 변수들을 통해, 나중에 철학자들에게 필요할 때 젓가락을 제공해주는 ChopstickProvider를, 어딘가에서 받을 수 있습니다. 이들을 갖추고 나면 그 후부터 생각하다가 먹다가 할 수 있겠죠.

그 뒤로 run 메소드가 나옵니다. 실제 액터가 구동되면 수행될 함수입니다. 먼저, 전술한 두 변수를 통해 ChopstickProvider가 제공되기를 기다립니다. 이 때 Concurrency::receive 함수를 쓰고 있습니다. (이의 비동기 버전인 Concurrency::try_receive도 있습니다.)

58줄부터는 생각하다 먹기를 반복하는 반복문이 나옵니다. ThinkEat 함수는 89줄 이하에서 확인할 수 있는 것처럼 철학자의 현재 상태를 나타내는 overwrite_buffer 형의 변수 CurrentState를 설정하는 것 이외에는 특별히 하는 일이 없습니다. 그냥 시간을 좀 지체할 뿐입니다.

그리고 이 두 함수 호출 사이에 PickupChopsticksPutDownChopsticks 함수를 써서 실제 가장 중요한 젓가락 한 쌍을 안전하게 획득하고 다시 내려놓는 일을 합니다.


이에 대한 설명은 다음 회를 기대해주세요~ ^^