복사 제거 및 반환 값 최적화 란 무엇입니까?


377

복제 제거 란 무엇입니까? 반환 값 최적화 란 무엇입니까? 그들은 무엇을 의미합니까?

어떤 상황에서 발생할 수 있습니까? 한계는 무엇입니까?


1
복사 제거는이를 보는 한 가지 방법입니다. 물체 제거 또는 물체 융합 (또는 혼란)은 또 다른 관점이다.
curiousguy

링크가 도움 이 되었다는 것을 알았습니다 .
8

답변:


246

소개

기술 개요- 이 답변으로 건너 뛰십시오 .

복사 제거가 발생하는 일반적인 경우- 이 답변으로 건너 뛰십시오 .

복사 제거는 특정 상황에서 추가 (잠재적으로 비싼) 복사를 방지하기 위해 대부분의 컴파일러가 구현하는 최적화입니다. 실제로는 가치에 의한 반품 또는 가치에 의한 반품이 가능합니다 (제한 사항이 적용됨).

객체 복사 / 이동에 부작용이 있더라도 as-if 규칙 복사 제거를 적용 할 수 있는 유일한 최적화 형식입니다 (ha!) .

다음 예제는 Wikipedia 에서 가져온 것입니다 .

struct C {
  C() {}
  C(const C&) { std::cout << "A copy was made.\n"; }
};

C f() {
  return C();
}

int main() {
  std::cout << "Hello World!\n";
  C obj = f();
}

컴파일러 및 설정에 따라 다음 출력 이 모두 유효합니다 .

안녕하세요 세계!
사본이 만들어졌습니다.
사본이 만들어졌습니다.


안녕하세요 세계!
사본이 만들어졌습니다.


안녕하세요 세계!

이것은 또한 더 적은 수의 객체를 생성 할 수 있다는 것을 의미하므로, 호출되는 특정 소멸자 수에 의존 할 수도 없습니다. 복사 / 이동 생성자 또는 소멸자 내부에는 호출 할 수 없으므로 중요한 논리가 없어야합니다.

복사 또는 이동 생성자에 대한 호출이 생략 된 경우 해당 생성자가 여전히 존재하고 액세스 가능해야합니다. 이렇게하면 복사 제거가 일반적으로 복사 할 수없는 개체 (예 : 개인 또는 삭제 된 복사 / 이동 생성자가 있기 때문에)를 복사 할 수 없도록합니다.

C ++ 17 : C ++ 17부터 객체가 직접 반환되면 Copy Elision이 보장됩니다.

struct C {
  C() {}
  C(const C&) { std::cout << "A copy was made.\n"; }
};

C f() {
  return C(); //Definitely performs copy elision
}
C g() {
    C c;
    return c; //Maybe performs copy elision
}

int main() {
  std::cout << "Hello World!\n";
  C obj = f(); //Copy constructor isn't called
}

2
두 번째 출력이 언제 발생하고 세 번째가 언제 발생하는지 설명 할 수 있습니까?
zhangxaochen

3
@zhangxaochen 컴파일러가 언제 어떻게 그렇게 최적화하기로 결정했는지.
Luchian Grigore 2014 년

10
@zhangxaochen, 1 번째 출력 : copy 1은 return에서 temp로, copy 2는 temp에서 obj로; 두 번째는 위의 내용 중 하나가 최적화되었을 때 아마도 reutnr 사본이 생략 된 것입니다. 이 세 가지 모두 제거되었습니다
빅터

2
흠,하지만 제 생각에는 이것이 우리가 신뢰할 수있는 기능이어야합니다. 그렇게 할 수 없다면 현대 C ++에서 함수를 구현하는 방식 (RVO vs std :: move)에 심각한 영향을 미치기 때문입니다. 일부 CppCon 2014 비디오를 시청하는 동안 모든 최신 컴파일러가 항상 RVO를 수행한다는 인상을 받았습니다. 또한 최적화가없는 컴파일러가 적용 할 수있는 곳을 읽었습니다. 그러나 물론 나는 그것에 대해 확신하지 못합니다. 내가 묻는 이유입니다.
j00hi

8
@ j00hi : return 문에 이동을 쓰지 마십시오. rvo가 적용되지 않으면 기본적으로 반환 값이 제거됩니다.
MikeMB

96

표준 참조

기술적 인 견해와 소개가 필요하지 않으면 이 답변으로 건너 뛰십시오 .

복사 제거가 발생하는 일반적인 경우- 이 답변으로 건너 뛰십시오 .

복사 제거 는 다음 표준에 정의되어 있습니다.

12.8 클래스 객체 복사 및 이동 [class.copy]

같이

31) 특정 기준이 충족되면 객체의 복사 / 이동 생성자 및 / 또는 소멸자가 부작용이 있더라도 구현시 클래스 객체의 복사 / 이동 구성을 생략 할 수 있습니다. 이러한 경우, 구현에서는 생략 된 복사 / 이동 조작의 소스 및 대상을 동일한 오브젝트를 참조하는 두 가지 다른 방법으로 취급하며, 해당 오브젝트의 파괴는 두 오브젝트가 있었을 때 후기에 발생합니다. 최적화없이 파괴되었습니다. 123 라는 복사 / 이동 작업이 생략, 복사 생략이 (여러 복사본을 제거하기 위해 결합 될 수있다) 다음과 같은 경우에 허용된다 :

— 클래스 반환 유형이있는 함수의 return 문에서 표현식이 함수 반환 유형과 동일한 cvunqualified 유형을 가진 비 휘발성 자동 객체 (함수 또는 catch-clause 매개 변수 제외)의 이름 인 경우 자동 객체를 함수의 반환 값으로 직접 구성하여 복사 / 이동 작업을 생략 할 수 있습니다.

— throw-expression에서 피연산자가 범위가 가장 안쪽에있는 try-block의 끝을 넘어 확장되지 않는 비 휘발성 자동 객체 (함수 또는 catch-clause 매개 변수 제외)의 이름 인 경우 (있는 경우) 1) 피연산자에서 예외 객체 (15.1) 로의 복사 / 이동 작업은 자동 객체를 예외 객체로 직접 구성하여 생략 할 수 있습니다.

— 참조 (12.2)에 바인딩되지 않은 임시 클래스 객체를 동일한 cv-unqualified 유형의 클래스 객체로 복사 / 이동할 때 임시 객체를 객체에 직접 구성하여 복사 / 이동 작업을 생략 할 수 있습니다. 생략 된 복사 / 이동의 대상

— 예외 처리기의 예외 선언 (Clause 15)이 예외 개체 (15.1)와 동일한 유형 (cv-qualification 제외)의 개체를 선언하면 예외 선언을 처리하여 복사 / 이동 작업을 생략 할 수 있습니다. 예외 선언에 의해 선언 된 객체에 대한 생성자와 소멸자의 실행을 제외하고 프로그램의 의미가 변경되지 않으면 예외 객체의 별명으로 사용됩니다.

123) 두 개가 아닌 하나의 개체 만 파괴되고 하나의 복사 / 이동 생성자가 실행되지 않기 때문에 생성 된 개체마다 여전히 하나의 개체가 파괴됩니다.

주어진 예는 다음과 같습니다.

class Thing {
public:
  Thing();
  ~Thing();
  Thing(const Thing&);
};
Thing f() {
  Thing t;
  return t;
}
Thing t2 = f();

설명했다 :

여기서 제거 기준을 결합하여 클래스의 복사 생성자에 대한 두 가지 호출을 제거 할 수 있습니다 Thing. t함수의 반환 값을 위해 로컬 자동 개체 를 f() 임시 개체로 복사하고 해당 임시 개체를 개체로 복사합니다 t2. 효과적으로 로컬 객체의 구성은 t 전역 객체를 직접 초기화하는 것으로 볼 수 있으며 t2해당 객체의 파괴는 프로그램 종료시 발생합니다. Thing에 이동 생성자를 추가해도 동일한 효과가 있지만 임시 객체에서 제거 된 이동 구성입니다 t2.


1
C ++ 17 표준이나 이전 버전에서 온 것입니까?
Nils

90

일반적인 형태의 복사 제거

기술 개요- 이 답변으로 건너 뛰십시오 .

기술적 인 견해와 소개가 필요하지 않으면 이 답변으로 건너 뛰십시오 .

(명명 된) 반환 값 최적화는 일반적인 형태의 복사 제거입니다. 메소드에서 값으로 리턴 된 오브젝트에 사본이 생략 된 상황을 나타냅니다. 표준에 제시된 예제 는 오브젝트의 이름이 지정되므로 이름 지정된 리턴 값 최적화를 보여줍니다 .

class Thing {
public:
  Thing();
  ~Thing();
  Thing(const Thing&);
};
Thing f() {
  Thing t;
  return t;
}
Thing t2 = f();

임시가 리턴 될 때 정기적 인 리턴 값 최적화 가 발생합니다.

class Thing {
public:
  Thing();
  ~Thing();
  Thing(const Thing&);
};
Thing f() {
  return Thing();
}
Thing t2 = f();

복사 제거가 발생하는 다른 일반적인 장소는 임시 값이 값으로 전달되는 경우입니다 .

class Thing {
public:
  Thing();
  ~Thing();
  Thing(const Thing&);
};
void foo(Thing t);

foo(Thing());

또는 예외가 발생하여 값으로 잡힐 때 :

struct Thing{
  Thing();
  Thing(const Thing&);
};

void foo() {
  Thing c;
  throw c;
}

int main() {
  try {
    foo();
  }
  catch(Thing c) {  
  }             
}

복사 제거의 일반적인 제한 사항은 다음과 같습니다.

  • 여러 리턴 포인트
  • 조건부 초기화

대부분의 상업용 컴파일러는 복사 제거 및 (N) RVO (최적화 설정에 따라 다름)를 지원합니다.


4
"공통 제한 사항"글 머리 기호를 조금만 설명하는 데 관심이 있습니다. 이러한 제한 요소를 만드는 이유는 무엇입니까?
phonetagger

@ phonetagger 나는 msdn 기사와 관련이 있었으므로 일부 내용이 지워지기를 바랍니다.
Luchian Grigore

54

복사 제거는 불필요한 객체 복사 / 이동을 제거하는 컴파일러 최적화 기술입니다.

다음과 같은 상황에서 컴파일러는 복사 / 이동 작업을 생략 할 수 있으므로 연관된 생성자를 호출하지 않아도됩니다.

  1. NRVO (Named Return Value Optimization) : 함수가 값으로 클래스 유형을 리턴하고 리턴 명령문의 표현식이 자동 저장 기간 (함수 매개 변수 아님)을 갖는 비 휘발성 오브젝트의 이름 인 경우 복사 / 이동 비 최적화 컴파일러에 의해 수행되는 것은 생략 될 수 있습니다. 그렇다면 반환 된 값은 함수의 반환 값이 이동되거나 복사 될 스토리지에 직접 구성됩니다.
  2. RVO (Return Value Optimization) : 함수가 순진한 컴파일러에 의해 목적지로 이동 또는 복사 될 이름없는 임시 오브젝트를 리턴하는 경우, 복사 또는 이동은 1에 따라 생략 될 수 있습니다.
#include <iostream>  
using namespace std;

class ABC  
{  
public:   
    const char *a;  
    ABC()  
     { cout<<"Constructor"<<endl; }  
    ABC(const char *ptr)  
     { cout<<"Constructor"<<endl; }  
    ABC(ABC  &obj)  
     { cout<<"copy constructor"<<endl;}  
    ABC(ABC&& obj)  
    { cout<<"Move constructor"<<endl; }  
    ~ABC()  
    { cout<<"Destructor"<<endl; }  
};

ABC fun123()  
{ ABC obj; return obj; }  

ABC xyz123()  
{  return ABC(); }  

int main()  
{  
    ABC abc;  
    ABC obj1(fun123());//NRVO  
    ABC obj2(xyz123());//NRVO  
    ABC xyz = "Stack Overflow";//RVO  
    return 0;  
}

**Output without -fno-elide-constructors**  
root@ajay-PC:/home/ajay/c++# ./a.out   
Constructor    
Constructor  
Constructor  
Constructor  
Destructor  
Destructor  
Destructor  
Destructor  

**Output with -fno-elide-constructors**  
root@ajay-PC:/home/ajay/c++# g++ -std=c++11 copy_elision.cpp -fno-elide-constructors    
root@ajay-PC:/home/ajay/c++# ./a.out   
Constructor  
Constructor  
Move constructor  
Destructor  
Move constructor  
Destructor  
Constructor  
Move constructor  
Destructor  
Move constructor  
Destructor  
Constructor  
Move constructor  
Destructor  
Destructor  
Destructor  
Destructor  
Destructor  

복사 제거가 발생하고 복사 / 이동 생성자가 호출되지 않더라도 존재하고 액세스 할 수 있어야합니다 (최적화가 전혀 발생하지 않은 것처럼) 그렇지 않으면 프로그램이 잘못 구성됩니다.

소프트웨어의 관찰 가능한 행동에 영향을 미치지 않는 장소에서만 그러한 복사 제거를 허용해야합니다. 복사 제거는 관찰 가능한 부작용을 갖도록 허용 된 유일한 최적화 형태입니다. 예:

#include <iostream>     
int n = 0;    
class ABC     
{  public:  
 ABC(int) {}    
 ABC(const ABC& a) { ++n; } // the copy constructor has a visible side effect    
};                     // it modifies an object with static storage duration    

int main()   
{  
  ABC c1(21); // direct-initialization, calls C::C(42)  
  ABC c2 = ABC(21); // copy-initialization, calls C::C( C(42) )  

  std::cout << n << std::endl; // prints 0 if the copy was elided, 1 otherwise
  return 0;  
}

Output without -fno-elide-constructors  
root@ajay-PC:/home/ayadav# g++ -std=c++11 copy_elision.cpp  
root@ajay-PC:/home/ayadav# ./a.out   
0

Output with -fno-elide-constructors  
root@ajay-PC:/home/ayadav# g++ -std=c++11 copy_elision.cpp -fno-elide-constructors  
root@ajay-PC:/home/ayadav# ./a.out   
1

GCC는 -fno-elide-constructors복사 제거를 비활성화 하는 옵션을 제공합니다 . 가능한 복사 제거를 피하려면을 사용하십시오 -fno-elide-constructors.

이제 거의 모든 컴파일러는 최적화가 활성화 된 경우 (및 다른 옵션이 비활성화되지 않은 경우) 복사 제거를 제공합니다.

결론

각각의 카피 제거시, 카피의 하나의 구성 및 하나의 매칭 파괴가 생략되어, CPU 시간을 절약하고, 하나의 객체가 생성되지 않아서, 스택 프레임상의 공간을 절약한다.


6
성명 ABC obj2(xyz123());은 NRVO 또는 망막 정맥 폐쇄입니까? 다음과 같은 임시 변수 / 객체를 얻지 못함 ABC xyz = "Stack Overflow";//RVO
Asif Mushtaq

3
RVO를보다 구체적으로 설명하기 위해 컴파일러가 생성하는 어셈블리를 참조 할 수 있습니다 (diff를 보려면 컴파일러 플래그 -fno-elide-constructors 변경). godbolt.org/g/Y2KcdH
Gab 是 好人
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.