Abseil Tip 117 복사 생략과 값으로 전달하기

원래 TotW #117로 2016년 6월 8일 게시됨

작성: Geoff Romer (gromer@google.com)

“모든 것이 멀리 있다. 복사의 복사의 복사. 불면증 거리 속의 모든 것, 당신은 어떤 것도 만질 수 없고, 아무것도 당신을 만질 수 없다.” — 척 팔라니uk


다음과 같은 클래스를 가정해봅시다:

class Widget {
 public:
  

 private:
  string name_;
};

이 클래스의 생성자를 어떻게 작성하시겠습니까? 몇 년 동안 일반적으로 이렇게 작성해왔습니다:

// 첫 번째 생성자 버전
explicit Widget(const std::string& name) : name_(name) {}

그러나 최근에는 더 자주 사용되는 대안이 있습니다:

// 두 번째 생성자 버전
explicit Widget(std::string name) : name_(std::move(name)) {}

(만약 std::move()에 익숙하지 않다면, TotW #77을 참고하거나 std::swap을 사용했다고 가정하면 됩니다. 동일한 원칙이 적용됩니다.) 여기서 무슨 일이 벌어지고 있을까요? std::string을 복사해서 전달하는 것이 매우 비싸 보이지 않나요? 실제로는 그렇지 않습니다. 때로는 값으로 전달하는 것이 참조로 전달하는 것보다 훨씬 효율적일 수 있습니다.

왜 그런지 이해하려면 다음 호출 시나리오를 살펴보세요:

Widget widget(absl::StrCat(bar, baz));

첫 번째 버전의 Widget 생성자를 사용하면 absl::StrCat()은 연결된 문자열 값을 포함하는 임시 문자열을 생성하고, 이 문자열은 참조로 Widget()에 전달된 후 name_에 복사됩니다. 반면 두 번째 버전의 생성자는 임시 문자열을 값으로 Widget()에 전달합니다. 이 경우 문자열이 복사될 것처럼 보일 수 있지만, 여기서 마법이 발생합니다. 컴파일러는 임시 객체를 사용해 새 객체를 복사 생성하는 경우, 동일한 메모리 저장소를 임시 객체와 새 객체에 사용합니다. 따라서 복사가 사실상 무료로 이루어집니다. 이를 복사 생략(Copy Elision)이라고 합니다. 이 최적화 덕분에 문자열은 복사되지 않고 한 번만 이동되며, 이는 상수 시간으로 처리되는 저렴한 작업입니다.


임시 객체가 아닌 경우

다음과 같은 경우를 고려해봅시다:

string local_str;
Widget widget(local_str);

이 경우, 두 버전 모두 문자열을 복사하지만, 두 번째 버전은 문자열을 이동도 합니다. 이동은 상수 시간으로 처리되는 저렴한 작업이므로, 대부분의 경우 추가적인 비용이 가치 있을 가능성이 높습니다.


이 기법이 작동하는 핵심 원리

name 매개변수가 반드시 복사되어야 한다는 점입니다. 이 기법의 본질은 복사 작업을 함수 내부가 아닌 호출 경계에서 수행되도록 하여 복사가 생략될 수 있도록 만드는 데 있습니다. 이 과정에서 std::move()를 사용하지 않아도 됩니다. 예를 들어, 복사가 필요하지만 저장 대신 변경해야 하는 경우, 복사본을 바로 수정하면 됩니다.


복사 생략을 사용할 때

값으로 매개변수를 전달하는 방식에는 몇 가지 단점이 있습니다:

  1. 코드 복잡도 증가: 함수 본문이 복잡해져 유지 관리와 가독성이 떨어질 수 있습니다. 예제에서는 std::move()를 추가해, 이동된 값을 실수로 참조하는 위험이 생깁니다.
  2. 예상치 못한 성능 저하: 특정 워크로드에서 이 기법이 성능을 개선하지 못할 수 있습니다. 구체적인 워크로드에서 프로파일링 없이 효과를 판단하기 어려운 경우가 많습니다.

주의할 점

  • 복사가 필요한 경우에만 사용: 복사가 필요 없는 매개변수에는 사용하면 오히려 해로울 수 있습니다.
  • 추가 작업 부담: 함수 본문에서 이동할당 등 추가 작업이 필요할 수 있습니다. 이런 추가 작업이 오히려 비용을 증가시킬 가능성이 있습니다.
  • 메모리 할당: 값으로 전달하면 기존 버퍼를 재사용하지 못하고 새 메모리를 할당할 가능성이 큽니다. 이는 이후 메모리 할당 패턴에 영향을 미칠 수 있습니다.

일반적으로 더 단순하고 안전하며 읽기 쉬운 코드를 선호해야 하며, 복잡한 방법을 선택하려면 성능상의 이점이 중요하다는 구체적인 증거가 있어야 합니다. 이 기법도 마찬가지입니다. 기본값으로는 참조로 전달하는 것이 더 단순하고 안전합니다. 하지만 성능이 중요한 영역에서 작업하거나 복사로 인한 성능 문제가 나타나는 경우, 값으로 전달하는 방법은 유용한 도구가 될 수 있습니다.