본문으로 바로가기

LR Value의 이해 - ④ Copy elision과 RVO

category Computer Science/C++ 2021. 10. 10. 19:18

Copy elision과 RVO(Return value optimization)

Copy elision optimization

Copy elision이란 특정 상황에서 Copy와 심지어 Move까지도 생략하는 컴파일러 최적화를 뜻한다. 이전 글에서 Copy elision 최적화가 발생하는 한 가지 상황을 살펴보았다.

class Cat {
public:
    void setName(std::string s)
    {
        name = std::move(s);
    }
private:
    std::string name;
};
int main()
{
    Cat kitty;

    // R-value 전달 : 0 copy
    kitty.setName("kitty");
    return 0;
}

위 코드에서 컴파일러는 컴파일 시간에 setName의 인자로 R-Value가 넘어오는것을 알수 있다. 그래서 컴파일러는 "kitty" 라는 문자열은 setNames의 메모리 공간에 바로 저장한다. 그렇게 되어서 Copy는 물론이고 Move 조차 발생하지 않는 것이다. 이는 함수를 반환하는 상황에서도 발생한다.

RVO(Return value optimization)

RVO(Return value optimization)는 함수를 반환할 때 발생하는 Copy elision optimization을 말한다. Copy elision optimization은 앞서 살펴본것 처럼 copy연산과 심지어 move연산 까지도 생략하는 최적화를 뜻 한다.

RVO가 없다면?

RVO를 이해하기 위해서 RVO가 없는 상황을 한번 살펴보자.

std::string getString()
{
    std::string s = "abc";
    return s;
}
int main()
{
    std::string a = getString();
    return 0;
}

위와 같은 상황에서 만약 RVO가 없다면 다음과 같이 동작할 것이다.

  • 메모리 공간에 a가 할당됨
  • 메모리 공간에 s가 할당됨
  • s"abc" 대입 후 s 반환
  • as복사
  • s 메모리에서 삭제
  • a 메모리에서 삭제

 

이 과정을 살펴보면 값을 반환하기 위해서 copy가 발생함을 확인할 수 있다. copy가 발생하지 않기 위해서 다음과 같이 코드를 작성할 수도 있을 것이다. RVO 코드를 다음과 같이 수정해 보자.

std::string getString()
{
    std::string s = "abc";
    return std::move(s);
}
int main()
{
    std::string a = getString();
    return 0;
}

이번에도 RVO가 없다면 이 코드는 다음과 같이 동작할 것이다.

  • 메모리 공간에 a가 할당
  • 메모리 공간에 s가 할당
  • s"abc" 대입 후 s 반환
  • as이동
  • s 메모리에서 삭제
  • a 메모리에서 삭제

 

이번에는 값을 반환하는데 copy를 하는 대신 소유권을 이전하는 move가 발생했다. std::move 연산을 통해서 기존보다 분명 더 효율 적으로 개선되었다. 하지만 실제 컴파일러에서는 RVO가 동작하기 때문에 이 보다 더 최적화된다.

RVO가 있는 상황

RVO가 개입되는 상황을 살펴보자

std::string getString()
{
    std::string s = "abc";
    return s;               // 0 copy, 0 move
    // return std::move(s); // 0 copy, 1 move
}
int main()
{
    std::string a = getString();
    return 0;
}

RVO가 개입되는 상황에서 위 코드는 아래와 같이 동작한다.

  • 메모리 공간에 a가 할당
  • a 공간을 s로 간주.
  • s( == a) 에 "abc" 대입.

 

이것이 가능한 이유는 getString 함수가 call stack에 올라갈 때 값이 반환될 주소인 a의 주소가 함께 올라간다. 그리고 컴파일 타임에 컴파일러는 s가 곧 a에 대입될 것을 알고 있다. 그래서 지역변수 s를 위해 새로운 메모리 공간을 할당하는 것이 아니라, call stack에 올라온 a의 주소를 s의 주소인 것 마냥 사용하기 때문에 copy가 일어날 일도 없고, move도 일어나지 않는 것이다. 이렇게 RVO가 개입될 경우 std::move(s) 로 반환하는 것보다 더 효율적이게 동작한다. 이렇게 RVO가 개입하기 때문에 값을 반환할 때는 그냥 return by value로 코드를 작성하는 것이 가장 성능이 좋다.