728x90
반응형

매개변수 전달 방법을 3가지로 나누어 성능을 비교해보자.

// 오버로딩
class Widget
{
public:
  void addName(const std::string& newName)
  { names.push_back(newName); }
  
  void addName(std::string&& newName)
  { names.push_back(std::move(newName); }
  
  ...
  
  private:
    std::vector<std::string> names;
};

// 보편 참조
class Widget
{
public:
  template<typename T>
  void addName(T&& newName)
  { names.push_back(std::forward<T>(newName)); }
  
  ...
};

// 값 전달
class Widget
{
public:
  void addName(std::string newName)
  { names.push_back(std::move(newName)); }
  
  ...
};

오버로딩

왼값과 오른값 모두 참조로 전달되므로 인수로 전달되는 비용은 없다. 함수 내에서 왼값의 경우에는 복사 1회가 수행되고 오른값의 경우에는 이동 1회 수행된다.

비용 정리: 왼값의 경우 복사 1회, 오른값의 경우 이동 1회

 

보편 참조

오버로딩과 연산 시점과 비용이 똑같다. 함수로 전달하는 비용이 없고 본문에서 복사와 이동이 일어난다. 만일 std::string 이 아닌 타입의 경우에는 std::string의 복사와 이동이 0회 이상 일어날 수 있다. 그러나 여기서는 std::string 의 경우에만 생각하자.

비용 정리: 왼값의 경우 복사 1회, 오른값의 경우 이동 1회

 

값 전달

앞서 설명한 대로 왼값과 오른값으로 나누어 생각해야 한다. 왼값의 경우에는 인수로 전달하는데 복사 1회가 일어나고, 오른값의 경우에는 이동 1회가 발생한다. 함수 본문에서는 두 가지 경우 모두 이동 1회가 발생한다.

성능의 비교로 알아보았듯이 값 전달이 장점만 있는 것은 아니다. 성능뿐만 아니라 이동 전용 형식의 경우에는 우리가 값 전달을 고려할 필요가 없다. 오른값으로 매개변수를 받도록 만들었다면 인수를 전달하는데 드는 비용이 없기 때문이다. 또한 만일 addName 함수 내에 조건문에 따라 삽입을 하지 않는다면 쓸데없는 복사나 이동 연산을 하게 되는 것이다.

비용 정리: 왼값의 경우 복사 1회 + 이동 1회, 오른값의 경우 이동 2회

 

즉, 값 전달 방식의 매개변수를 사용하면 어떤 경우이든 이동이 1회씩 더 일어난다.

구체적으로 다음 네 가지 사항에 주목하자.

  • 값 전달 방식을 고려하라 일뿐이다. 값 전달 방식에는 함수를 하나만 작성하면 된다는 장점과 목적 코드에 함수 하나만 만들어진다는 장점, 그리고 보편 참조(universal reference)와 관련된 문제점이 없다는 장점이 있는 것이 사실이다. 그렇지만 다른 대안들보다 비용이 크며, 이야기하지 않은 비용이 추가되는 경우도 있다.
  • 복사 가능 매개변수에 대해서만 값 전달을 고려해야 한다.
  • 값 전달은 이동이 저렴한 매개변수에 대해서만 고려해야 한다. 이동의 비용이 크다면, 불필요한 이동을 수행하는 것은 불필요한 복사를 수행하는 것과 비슷하다.
  • 값 전달은 항상 복사되는 매개변수에 대해서만 고려해야 한다.

 

배정문을 통한 복사

매개변수를 생성이 아닌 배정('=' 연산을 사용하는 경우)을 통해 복사하는 함수의 경우, 좀 더 비효율성이 증가할 수 있다. 코드를 보자.

class Password
{
public:
  explicit Password(std::string pwd)
  : text(std::move(pwd)) {}
  
  void changeTo(std::string newPwd)
  { text = std::move(newPwd): }
  
  ...
  
private:
  std::string text;
};

메모리를 복사 생성하면서 새로 할당하고, 이동 생성자를 이용해 기존 변수의 메모리를 해제하고 재할당을 해야 하므로, 2번의 할당과 해제를 시행해야 한다. 하지만 기존 패스워드의 capacity 가 새 패스워드의 size() 보다 길다면 오버로딩 버전의 경우 메모리를 깔끔하게 재활용할 수도 있을 것이다.

결론적으로, 최대한 빨라야 하는 소프트웨어에서는 값 전달이 그리 바람직하지 않다. 항목의 마지막이 왜 '사용하라'가 아닌 '고려하라' 인지 생각해 보면, 언뜻 모순적인 이 결론이 와닿을 것이다.

 

이 외에도 값 전달은 잘림 문제(slicing problem)가 발생할 여지가 있다.

잘림 문제는 파생 클래스 타입의 객체가 기본 클래스 타입의 객체에 대입될 때, 파생 클래스 부분이 잘려 나가는(slice off) 문제이다.

 

요약

  • 이동이 저렴하고 항상 복사되는 복사 가능 매개변수에 대해서는 값 전달이 참조 전달만큼이나 효율적이고, 구현하기가 더 쉽고, 산출되는 목적 코드의 크기도 더 작다.
  • 왼값 인수의 경우 값 전달(즉, 복사 생성) 다음의 이동 배정은 참조 전달 다음의 복사 배정보다 훨씬 비쌀 가능성이 있다.
  • 값 전달에서는 잘림 문제가 발생할 수 있으므로, 일반적으로 기반 클래스 매개변수 형식에 대해서는 값 전달이 적합하지 않다.
728x90
반응형