Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to find C++ spurious copy operations?

Recently, I had the following

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

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

The problem with this code is that when the struct is created a copy occurs and the solution is instead to write return {std::move(V)}

Are there linter or code analyzer that would detect such spurious copy operations? Neither cppcheck, cpplint, nor clang-tidy can do it.

EDIT: Several points to make my question clearer:

  1. I know that a copy operation occurred because I used compiler explorer and it shows a call to memcpy.
  2. I could identify that a copy operations occurred by looking at the standard yes. But my initial wrong idea was that the compiler would optimize away this copy. I was wrong.
  3. It is (likely) not a compiler problem since both clang and gcc produce code that produce a memcpy.
  4. The memcpy may be cheap, but I cannot imagine circumstances where copying memory and deleting the original is cheaper than passing a pointer by a std::move.
  5. The adding of the std::move is an elementary operation. I would imagine that a code analyzer would be able to suggest this correction.
like image 989
Mathieu Dutour Sikiric Avatar asked Jan 02 '20 08:01

Mathieu Dutour Sikiric


2 Answers

I believe you have the correct observation but the wrong interpretation!

The copy will not occur by returning the value, because every normal clever compiler will use (N)RVO in this case. From C++17 this is mandatory, so you can't see any copy by returning a local generated vector from the function.

OK, lets play a bit with std::vector and what will happen during the construction or by filling it step by step.

First of all, lets generate a data type which makes every copy or move visible like this one:

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; }

};

And now lets start some experiments:

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; }
}

What can we observe:

Example 1) We create a vector from a initializer list and maybe we expect that we will see 4 times construct and 4 moves. But we get 4 copies! That sounds a bit mysterious, but the reason is the implementation of initializer list! Simply it is not allowed to move from the list as the iterator from the list is a const T* which makes it impossible to move elements from it. A detailed answer on this topic can be found here: initializer_list and move semantics

Example 2) In this case, we get a initial construction and 4 copies of the value. That is nothing special and is what we can expect.

Example 3) Also here, we the the construction and some moves as expected. With my stl implementation the vector grows by factor 2 every time. So we see a first construct, another one and because the vector resizes from 1 to 2, we see the move of the first element. While adding the 3 one, we see a resize from 2 to 4 which needs a move of the first two elements. All as expected!

Example 4) Now we reserve space and fill later. Now we have no copy and no move anymore!

In all cases, we do not see any move nor copy by returning the vector back to the caller at all! (N)RVO is taking place and no further action is required in this step!

Back to your question:

"How to find C++ spurious copy operations"

As seen above, you may introduce a proxy class in between for debugging purpose.

Making the copy-ctor private may not work in many cases, as you may have some wanted copies and some hidden ones. As above, only the code for example 4 will work with a private copy-ctor! And I can not answer the question, if the example 4 is the fastest one, as we fill peace by peace.

Sorry that I can not offer a general solution for finding "unwanted" copies here. Even if you dig your code for calls of memcpy, you will not find all as also memcpy will be optimized away and you see directly some assembler instructions doing the job without a call to your library memcpy function.

My hint is not to focus on such a minor problem. If you have real performance issues, take a profiler and measure. There are so many potential performance killers, that investing much time on spurious memcpy usage seems not such a worthwhile idea.

like image 134
Klaus Avatar answered Oct 04 '22 15:10

Klaus


I know that a copy operation occurred because I used compiler explorer and it shows a call to memcpy.

Did you put your complete application into the compiler explorer, and did you enable optimizations? If not, then what you saw in the compiler explorer might or might not be what is happening with your application.

One issue with the code you posted is that you first create a std::vector, and then copy it into an instance of data. It would be better to initialize data with the vector:

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

Also, if you just give the compiler explorer the definition of data and get_vector(), and nothing else, it has to expect the worse. If you actually give it some source code that uses get_vector(), then look at what assembly is generated for that source code. See this example for what the above modification plus actual usage plus compiler optimizations can cause the compiler to produce.

like image 28
G. Sliepen Avatar answered Oct 04 '22 15:10

G. Sliepen