본문 바로가기
컴퓨터/Modern Effective C++ 정리

Modern Effective C++ item 29 : move가 항상 좋다는 편견은 버려

by 김짱쌤 2015. 5. 13.

move가 항상좋다는 편견은 버리자

move sementic은 C++11에 등장한 큼지막한 변화의 한 축이다. 임시 객체에 대해서 Copy대신 Move를 하면 정말 번개같은 속도로 같은 기능을 대체할 수 있다고 들었던거같다. 정말로 마법같은 move의 훌륭함을 우리는 마음깊이 새기고 있었다. 하.지.만 이제는 그런 환상을 깰 시점이 온 것같다.

move sementic을 지원하지 않는 경우

C++ 98에 작성한 legacy 클래스들은 별도의 move 연산자들을 작성하지 않았기 때문에 move sementic으로 받아온다고 해도 별 이득을 볼수가 없다. item 27에서 보았던 move의 까다로운 자동생성조건을 생각해본다면, move 생성자 자체가 없을 가능성이 크고, 그렇다면 그냥 copy연산이 수행되기 때문이다.

std::array에서 move

STL의 컨테이너들은 모두 move연산을 지원한다. 하지만 move를 지원한다고 해서 다 효율이 좋은것은 아니다. 컨테이너의 경우 컨테이너 안에 들어있는 element들의 move 효율이 별로라면 그리 이득을 못본다는 이유도 있고, 자체적인 move 동작이 그렇게 효율적이지 못하다는 이유도 있다.

그중에서 std::array의 move는 그리 효율적이지 못한 경우가 많다. 다른 STL 컨테이너의 경우 자신들의 내용물을 힙에다 저장해둔다. 그래서 move 연산을하면 저장공간의 주소만 swap하여 빠르게 동작할 수 있다. std::vector의 예시를 보면 쉽게 이해할 수 있다.

std::vector<Widget> vw1; 
//Widget을 vw1에 마구 push
//vw1은 heap공간에 데이터들을 축적
auto vw2 = std::move(vw1);
//move 연산을 진행하면 그냥 vector의 저장 힙의 주소만 swap해준다.

하지만 std::array의 경우 그러한 힙 영역에 대한 포인터가 없다! 왜냐하면 std::array의 원소들은 std::array 객체 자체에 저장되기 때문이다. 그렇다면 std::array의 move 동작은 저장된 원소들 하나하나를 move하는 작업이 필요하다.

std::array<Widget, 1000> aw1; 
//Widget을 aw1에 마구 push
//vw1은 자신이 가진 공간에 데이터를 축적
auto aw2 = std::move(aw1);
//move 연산을 진행하면 각 데이터를 순회하면서 move연산 수행

원소의 타입이 copy보다 효율좋은 move를 제공한다면 이런 동작자체도 분명 더 효과적이겠지만, 그렇지 않다면, move의 효율은 copy에 비해 좋을 것이 없다. 만약 move를 제공한다고 해도 time complexity로 보면 원소개수만큼 연산을 해야하므로 그리 효과적이라고 볼수는 없다.

std::string 에서의 move

std::string은 분명 move를 지원하는 클래스이다. 이 move 동작은 일반적으로 상수시간(포인터 교체)만에 이루어지는데, 그렇지 못한 경우가 있어서 문제이다. std::string은 길이가 길지 않은 문자열에 대해서 Small String Optimizatoin(SSO)라는 최적화 기법을 제공한다. 이것은 따로 힙에 데이터를 저장하지 않고, 적은 양의 데이터의 경우 객체 내부 버퍼에 저장을 하는 방법이다. 그러면 SSO로 구성된 짧은 std::string의 경우 std::array의 move 동작과 같은 이유에서 효율을 기대할 수가 없다. 실제로 문자열의 길이가 긴 경우보다 짧은 경우가 많기 때문에(일반적인 경우) std::string의 move는 전체적인 효과가 뛰어나다고 볼 수는 없을 것이다.

강력한 예외안전성을 제공하는 타입에서의 move

item 14에서 STL 컨테이너중 몇몇이 강력한 예외안전성을 제공하며, C++ 98에서 C++11로 변경하는데 어떤 문제도 발생하지 않음을 보장하고 있다고 이야기 한다. 이것들을 제공하기 위해서 해당 컨테이너들은 기존의 copy 동작이 새롭게 정의된 move 함수가 throw 되지 않는다는 것이 보장되는 경우에만 move로 대체가능하게 만들어두었다. 결과적으로 move 동작이 효과적일 수 있으려면 move 함수가 반드시 noexcept라는 조건을 충족시켜야 한다는 것이다.

move의 source가 lvalue인 경우

item 25에서 봤던 값에 의한 전달등의 몇가지 예외를 제외하면, move의 source로 lvalue를 쓴다는 것은 정말 미친짓이다. 이유는 item 23에서 상세히 설명되어 있다. 그러므로 move가 효과적일 수 있는 조건이 더 늘어난다.

결론

그러므로 move가 언제나 항상 좋다고 생각하는 것은 좋지 않다. 하지만 이런 경고들은 일반적으로 template을 사용한 코드에서 일어나는 문제들을 지적하고 있다. 왜냐면 Generic한 template 코드들을 작성하면 들어오는 타입 T가 무엇인지 알 수 없는 경우가 다반사이기 때문이다. 이러한 상황에서 move를 쓰면안되는 타입이나 써도 별 효과 없는 타입에 대해서 move를 써놓고 마술이 일어나길 기대하는 것은 좋지 않은 일이다.

하지만의 하지만 당신이 쓰는 코드에 들어올 타입이 무엇인지 분명히 알고, 그 타입들이 효과적인 move 연산을 지원하고, 그 성질이 변하지 않는다는 것이 확실하다면 앞선 가정은 다 잊어도 좋다. 그때는 copy들을 move로 대체하면서 그 빠름을 즐기기만 하면된다.