[c++] C ++ 가짜 복사 작업을 찾는 방법은 무엇입니까?

최근에 나는 다음을 가졌다

struct data {
  std::vector<int> V;
};

data get_vector(int n)
{
  std::vector<int> V(n,0);
  return {V};
}

이 코드의 문제점은 구조체가 생성 될 때 복사가 발생하고 솔루션이 대신 {std :: move (V)} return 을 작성한다는 것입니다.

그러한 가짜 복사 작업을 감지하는 린터 또는 코드 분석기가 있습니까? cppcheck, cpplint 또는 clang-tidy는 그렇게 할 수 없습니다.

편집 : 내 질문을 명확하게하기 위해 몇 가지 사항 :

  1. 컴파일러 탐색기를 사용하여 복사 작업이 발생 했으며 memcpy에 대한 호출을 보여줍니다 .
  2. 표준 예를 보면 복사 작업이 발생했음을 알 수 있습니다. 그러나 초기 잘못된 생각은 컴파일러 가이 복사본을 최적화하는 것입니다. 내가 틀렸어.
  3. clang과 gcc는 모두 memcpy 를 생성하는 코드를 생성하기 때문에 컴파일러 문제가 아닙니다 .
  4. memcpy는 저렴할 수 있지만 std :: move 의해 포인터를 전달하는 것보다 메모리를 복사하고 원본을 삭제하는 것이 더 저렴한 상황을 상상할 수 없습니다 .
  5. std :: move 추가 는 기본 조작입니다. 코드 분석기가이 수정을 제안 할 수 있다고 생각합니다.


답변

나는 당신이 올바른 관찰이지만 잘못된 해석을 가지고 있다고 믿습니다!

이 경우 모든 일반 영리한 컴파일러는 (N) RVO 를 사용하므로 값을 반환하여 복사가 발생하지 않습니다 . C ++ 17부터는 필수이므로 함수에서 로컬 생성 벡터를 반환하여 사본을 볼 수 없습니다.

좋아, std::vector건설하는 동안 또는 단계별로 채우면서 발생하는 일을 조금씩 연주하십시오 .

우선, 모든 사본 또는 이동을 다음과 같이 표시하는 데이터 유형을 생성하십시오.

template <typename DATA >
struct VisibleCopy
{
    private:
        DATA data;

    public:
        VisibleCopy( const DATA& data_ ): data{ data_ }
        {
            std::cout << "Construct " << data << std::endl;
        }

        VisibleCopy( const VisibleCopy& other ): data{ other.data }
        {
            std::cout << "Copy " << data << std::endl;
        }

        VisibleCopy( VisibleCopy&& other ) noexcept : data{ std::move(other.data) }
        {
            std::cout << "Move " << data << std::endl;
        }

        VisibleCopy& operator=( const VisibleCopy& other )
        {
            data = other.data;
            std::cout << "copy assign " << data << std::endl;
        }

        VisibleCopy& operator=( VisibleCopy&& other ) noexcept
        {
            data = std::move( other.data );
            std::cout << "move assign " << data << std::endl;
        }

        DATA Get() const { return data; }

};

이제 몇 가지 실험을 시작할 수 있습니다.

using T = std::vector< VisibleCopy<int> >;

T Get1() 
{   
    std::cout << "Start init" << std::endl;
    std::vector< VisibleCopy<int> > vec{ 1,2,3,4 };
    std::cout << "End init" << std::endl;
    return vec;
}   

T Get2()
{   
    std::cout << "Start init" << std::endl;
    std::vector< VisibleCopy<int> > vec(4,0);
    std::cout << "End init" << std::endl;
    return vec;
}

T Get3()
{
    std::cout << "Start init" << std::endl;
    std::vector< VisibleCopy<int> > vec;
    vec.emplace_back(1);
    vec.emplace_back(2);
    vec.emplace_back(3);
    vec.emplace_back(4);
    std::cout << "End init" << std::endl;

    return vec;
}

T Get4()
{
    std::cout << "Start init" << std::endl;
    std::vector< VisibleCopy<int> > vec;
    vec.reserve(4);
    vec.emplace_back(1);
    vec.emplace_back(2);
    vec.emplace_back(3);
    vec.emplace_back(4);
    std::cout << "End init" << std::endl;

    return vec;
}

int main()
{
    auto vec1 = Get1();
    auto vec2 = Get2();
    auto vec3 = Get3();
    auto vec4 = Get4();

    // All data as expected? Lets check:
    for ( auto& el: vec1 ) { std::cout << el.Get() << std::endl; }
    for ( auto& el: vec2 ) { std::cout << el.Get() << std::endl; }
    for ( auto& el: vec3 ) { std::cout << el.Get() << std::endl; }
    for ( auto& el: vec4 ) { std::cout << el.Get() << std::endl; }
}

무엇을 관찰 할 수 있습니까?

예제 1) 이니셜 라이저 목록에서 벡터를 생성하고 4 배의 구성과 4 개의 이동을 보게 될 것입니다. 그러나 우리는 4 부를 얻습니다! 조금 신비한 것처럼 들리지만 그 이유는 초기화 목록의 구현 때문입니다! 목록의 반복자는 목록 const T*에서 요소를 이동할 수 없으므로 목록 에서 이동할 수 없습니다. 이 주제에 대한 자세한 답변은 initializer_list 및 move semantics 에서 찾을 수 있습니다.

예 2)이 경우 초기 시공과 4 개의 값을 얻습니다. 그것은 특별한 것이 아니며 우리가 기대할 수있는 것입니다.

예 3) 또한 여기서 우리는 시공과 일부 움직임이 예상대로 진행됩니다. 내 stl 구현으로 벡터는 매번 요소 2 씩 증가합니다. 그래서 우리는 첫 번째 구조, 또 다른 구조를 보았습니다. 벡터의 크기가 1에서 2로 조정되기 때문에 첫 번째 요소의 이동을 봅니다. 3을 추가하는 동안 처음 두 요소의 이동이 필요한 2에서 4로 크기가 조정됩니다. 예상대로!

예 4) 이제 공간을 확보하고 나중에 채 웁니다. 이제 우리는 더 이상 사본과 움직임이 없습니다!

모든 경우에 벡터를 호출자에게 돌려 보냄으로써 어떠한 이동이나 복사도 볼 수 없습니다! (N) RVO가 진행 중이며이 단계에서 추가 조치가 필요하지 않습니다!

질문으로 돌아 가기 :

“C ++ 가짜 복사 작업을 찾는 방법”

위에서 보았 듯이 디버깅 목적으로 프록시 클래스를 도입 할 수 있습니다.

copy-ctor를 비공개로 설정하면 원하는 복사본과 숨겨진 복사본이있을 수 있으므로 대부분의 경우 작동하지 않을 수 있습니다. 위와 같이 예를 들어 4와 같은 코드 만 개인용 copy-ctor와 함께 작동합니다! 우리가 평화로 평화를 채울 때 예제 4가 가장 빠르면 질문에 대답 할 수 없습니다.

여기서 “원치 않는”사본을 찾는 일반적인 솔루션을 제공 할 수 없습니다. 의 호출을 위해 코드를 발굴하더라도 최적화 memcpy되지 않은 것을 찾을 수 없으며 memcpy라이브러리 memcpy함수를 호출하지 않고 작업을 수행하는 어셈블러 명령어를 직접 볼 수 있습니다.

내 힌트는 그런 사소한 문제에 초점을 맞추지 않는 것입니다. 실제 성능 문제가있는 경우 프로파일 러를 사용하여 측정하십시오. 잠재적 인 성능 저하 요인이 너무 많기 때문에 허위 memcpy사용법 에 많은 시간을 투자하는 것은 그리 가치있는 생각이 아닙니다.


답변

컴파일러 탐색기를 사용하여 복사 작업이 발생했으며 memcpy에 대한 호출을 보여줍니다.

완전한 응용 프로그램을 컴파일러 탐색기에 넣고 최적화를 활성화 했습니까? 그렇지 않다면 컴파일러 탐색기에서 본 것이 응용 프로그램에서 발생한 것일 수도 아닐 수도 있습니다.

게시 한 코드의 한 가지 문제는 먼저을 std::vector만든 다음의 인스턴스에 복사한다는 것입니다 data. 벡터 로 초기화 data 하는 것이 좋습니다 .

data get_vector(int n)
{
  return {std::vector<int> V(n,0)};
}

또한 컴파일러 탐색기에 dataand 의 정의를 지정 하고 get_vector()그 밖의 다른 것을 정의 하지 않으면 더 나쁠 것으로 예상해야합니다. 실제로를 사용 하는 소스 코드를 제공하는 경우 get_vector()해당 소스 코드에 대해 어떤 어셈블리가 생성되는지 확인하십시오. 위 수정과 실제 사용 및 컴파일러 최적화로 인해 컴파일러가 생성 할 수있는 사항 은 이 예 를 참조하십시오 .


답변