首页 > 解决方案 > 如何找到 C++ 虚假复制操作?

问题描述

最近,我有以下

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

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

此代码的问题在于,当创建结构时会发生副本,解决方案是改为编写return {std::move(V)}

是否有可以检测到这种虚假复制操作的 linter 或代码分析器?cppcheck、cpplint 和 clang-tidy 都做不到。

编辑:几点让我的问题更清楚:

  1. 我知道发生了复制操作,因为我使用了编译器资源管理器,它显示了对memcpy的调用。
  2. 通过查看标准是,我可以确定发生了复制操作。但我最初的错误想法是编译器会优化掉这个副本。我错了。
  3. 这(很可能)不是编译器问题,因为 clang 和 gcc 都会生成产生memcpy的代码。
  4. memcpy 可能很便宜,但我无法想象复制内存和删除原始内存比通过std::move传递指针更便宜的情况。
  5. std::move的添加是一个基本操作。我想代码分析器将能够建议这种更正。

标签: c++code-analysisstatic-code-analysiscppcheck

解决方案


我相信你有正确的观察但错误的解释!

返回值不会发生复制,因为在这种情况下,每个普通的聪明编译器都会使用(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 份!这听起来有点神秘,但原因是初始化列表的实现!简单地说,它不允许从列表中移动,因为列表中的迭代器是 a const T*,这使得无法从中移动元素。可以在此处找到有关此主题的详细答案:initializer_list and move semantics

示例 2)在这种情况下,我们得到一个初始构造和 4 个值的副本。这没什么特别的,是我们可以期待的。

示例 3) 同样在这里,我们按预期进行了构造和一些动作。在我的 stl 实现中,向量每次都会增长 2 倍。所以我们看到第一个构造,另一个构造,因为向量从 1 调整到 2,我们看到第一个元素的移动。在添加 3 时,我们看到从 2 到 4 的调整大小需要移动前两个元素。一切如预期!

示例 4) 现在我们保留空间并稍后填充。现在我们没有副本,也没有动作了!

在所有情况下,通过将向量返回给调用者,我们根本看不到任何移动或复制!(N)RVO 正在发生,此步骤无需进一步操作!

回到你的问题:

“如何找到 C++ 虚假复制操作”

如上所示,您可以在两者之间引入代理类以进行调试。

在许多情况下,将 copy-ctor 设为私有可能不起作用,因为您可能有一些想要的副本和一些隐藏的副本。如上所述,只有示例 4 的代码才能与私有复制 ctor 一起使用!我无法回答这个问题,如果示例 4 是最快的,因为我们以和平来填补和平。

抱歉,我无法在此处提供查找“不需要的”副本的通用解决方案。即使您挖掘代码以调用memcpy,您也不会发现所有内容都memcpy将被优化掉,并且您会直接看到一些汇编程序指令在不调用库memcpy函数的情况下完成这项工作。

我的提示是不要专注于这样一个小问题。如果您有真正的性能问题,请使用分析器并进行测量。有这么多潜在的性能杀手,在虚假memcpy使用上投入大量时间似乎不是一个值得的想法。


推荐阅读