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


11

최근에 나는 다음을 가졌다

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 추가 는 기본 조작입니다. 코드 분석기가이 수정을 제안 할 수 있다고 생각합니다.

2
"스퓨리어스"복사 작업을 감지하기위한 방법 / 도구가 있는지 여부에 대해서는 답변 할 수 없지만, 솔직히 말해서, std::vector어떤 방법 으로든 복사 하는 것이 의도 한 것이 아니라는 것에 동의하지 않습니다 . 귀하의 예는 명시 적 사본을 보여 주며 std::move, 사본이 원하는 것이 아니라면 자신이 제안한대로 기능 을 적용하는 것은 자연스럽고 올바른 접근 방식 (다시 말해서) 입니다. 최적화 플래그가 켜져 있고 벡터가 변경되지 않으면 일부 컴파일러는 복사를 생략 할 수 있습니다.
magnus

/ (- :이 린터 규칙이 사용할 수 있도록하기 위해 너무 많은 불필요한 복사 (어떤이 영향을 미치는되지 않을 수 있습니다)가 두려워 사용 기본적으로 이동할 수 있도록 명시 적으로 사본이 필요합니다 :))
Jarod42

최적화 코드에 대한 나의 제안을 최적화 할 기능을 분해 기본적으로 당신은 여분의 복사 작업 발견 할 것이다
camp0을

문제를 올바르게 이해하면 객체가 파괴 된 후 복사 작업 (생성자 또는 할당 연산자)이 호출되는 경우를 감지하려고합니다. 사용자 정의 클래스의 경우 복사가 수행 될 때 디버그 플래그 세트를 추가하고 다른 모든 작업에서 재설정하고 소멸자를 체크인한다고 상상할 수 있습니다. 그러나 소스 코드를 수정할 수 없으면 사용자 정의가 아닌 클래스에 대해서도 동일한 작업을 수행하는 방법을 모릅니다.
다니엘 랭거

2
내가 가짜 사본을 찾는 데 사용하는 기술은 일시적으로 사본 생성자를 개인용으로 만든 다음 액세스 제한으로 인해 컴파일러가 어디에 있는지 확인하는 것입니다. (태그를 지원하는 컴파일러의 경우 복사 생성자를 더 이상 사용되지 않는 것으로 태그 지정하여 동일한 목표를 달성 할 수 있습니다.)
Eljay

답변:


2

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

이 경우 모든 일반 영리한 컴파일러는 (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 작업을 찾을 수 있습니다 . 따라서 확실한 방법이 있습니다. 그러나 소규모 프로그램에만 적합합니다. 내 요점은 코드를 개선하는 방법에 대한 제안을 찾는 코드에 관심이 있다는 것입니다. 버그와 메모리 누수를 찾는 코드 분석기가 있는데 왜 그런 문제가 아닌가?
Mathieu Dutour Sikiric

"코드를 개선하는 방법에 대한 제안을 찾는 코드" 이는 컴파일러 자체에서 이미 수행되고 구현 된 것입니다. (N) RVO 최적화는 단지 하나의 예일 뿐이며 위에 표시된대로 완벽하게 작동합니다. "원치 않는 memcpy"를 검색 할 때 memcpy 잡기가 도움이되지 않았습니다. "버그와 메모리 누수를 찾는 코드 분석기가 있는데 왜 그런 문제가 아닌가?" 어쩌면 그것은 (일반적인) 문제가 아닙니다. "속도"문제를 찾기위한 훨씬 더 일반적인 도구도 이미 존재합니다 : 프로파일 러! 내 개인적인 느낌은, 당신이 오늘날 실제 소프트웨어에서 문제가되지 않는 학문적 인 것을 찾고 있다는 것입니다.
클라우스

1

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

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

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

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

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


위의 코드 ( memcpy 가있는 ) 만 컴퓨터 탐색기에 넣으십시오. 그렇지 않으면 질문이 이해가되지 않습니다. 그것은 당신의 대답이 더 나은 코드를 생성하는 다른 방법을 보여주는 데 뛰어나다는 것입니다. 두 가지 방법을 제공합니다. 정적 사용 및 생성자 직접 출력에 배치. 따라서 이러한 방법은 코드 분석기에서 제안 할 수 있습니다.
Mathieu Dutour Sikiric
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.