如何找到C ++伪复制操作?


11

最近,我有以下

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

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

此代码的问题在于,当创建该结构时,会发生复制,而解决方案是写return {std :: move(V)}

是否有lint或代码分析器可以检测到这种虚假的复制操作?cppcheck,cpplint或clang-tidy都无法做到这一点。

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

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

2
我无法回答是否存在用于检测“虚假”复制操作的方法/工具,但是,老实说,我不同意std::vector以任何方式进行的复制并不是它所声称的那样。您的示例显示了一个显式副本,这是自然而正确的方法(再次请恕我直言),std::move如果您不是您想要的副本,则按照您自己的建议应用该功能。请注意,如果打开了优化标志并且向量不变,则某些编译器可能会省略复制。
马格努斯

我担心有太多不必要的副本(可能不会造成影响)以使该短绒规则可用:-/(默认情况下,使用移动,因此需要显式副本:))
Jarod42

我对优化代码的建议基本上是分解要优化的功能,您会发现额外的复制操作
camp0

如果我正确地理解了您的问题,那么您将希望检测在销毁对象之后对对象调用复制操作(构造函数或赋值运算符)的情况。对于自定义类,我可以想象在执行复制时添加一些调试标志集,在所有其他操作中重置,并检入析构函数。但是,除非您能够修改非自定义类的源代码,否则不知道该如何做。
Daniel Langr

2
我用来查找虚假副本的技术是暂时将副本构造函数设为私有,然后检查由于访问限制而导致编译器无法正常运行的地方。(对于支持这种标记的编译器,通过将复制构造函数标记为不赞成使用,可以实现相同的目标。)
Eljay

Answers:


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份!听起来有些神秘,但是原因是初始化列表的实现!完全不允许将其从列表中移出,因为列表中的迭代器是a const T*,这使得无法从列表中移出元素。可以在以下位置找到关于此主题的详细答案:initializer_list和move语义

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

例3)同样在这里,我们的构造和一些动作符合预期。通过我的stl实现,向量每次都会增长2倍。因此,我们看到了第一个构造,另一个构造,并且由于向量的大小从1调整为2,因此我们看到了第一个元素的移动。在添加3个元素的同时,我们将尺寸从2调整为4,这需要移动前两个元素。一切都如预期!

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

在所有情况下,我们都不会看到任何移动,也根本不会通过将引导程序返回给调用方来进行复制!(N)RVO正在发生,在此步骤中无需采取进一步措施!

回到您的问题:

“如何查找C ++伪复制操作”

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

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

抱歉,我无法提供在此处查找“不需要的”副本的一般解决方案。即使您为的调用而挖掘代码memcpy,也不会找到全部,因为它们memcpy也会被优化,并且直接看到一些汇编程序指令可以完成工作,而无需调用您的库memcpy函数。

我的提示不是着眼于这样一个小问题。如果您有实际的性能问题,请进行探查并进行测量。有太多潜在的性能杀手,以至于在虚假memcpy使用上花很多时间似乎不是一个值得的主意。


我的问题是学术上的。是的,有很多方法可以使代码缓慢,这对我来说不是一个直接的问题。但是,我们可以使用编译器资源管理器找到memcpy操作。因此,绝对有一种方法。但这仅适用于小型程序。我的观点是,对代码的兴趣会找到有关如何改进代码的建议。有代码分析器可以发现错误和内存泄漏,为什么不出现此类问题呢?
Mathieu Dutour Sikiric

“可以找到有关如何改进代码的建议的代码。” 这已经在编译器本身中完成并实现。(N)RVO优化仅是一个示例,并且可以如上所示完美运行。捕获memcpy并没有帮助,因为您正在搜索“不需要的memcpy”。“有一些代码分析器可以发现错误和内存泄漏,为什么不出现此类问题呢?” 也许这不是一个(常见的)问题。查找“速度”问题的通用工具也已经存在:探查器!我个人的感觉是,您正在寻找学术上的东西,而这在当今的实际软件中已不是问题。
克劳斯

1

我知道发生了复制操作,因为我使用了编译器浏览器,并且显示了对memcpy的调用。

您是否将完整的应用程序放入编译器资源管理器中,并且启用了优化功能?如果不是这样,那么您在编译器资源管理器中看到的内容可能会或可能不会与您的应用程序一起发生。

您发布的代码的一个问题是,您首先创建一个std::vector,然后将其复制到的实例中data。最好用vector 初始化 data

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

另外,如果您只是给编译器资源管理器定义data和的定义get_vector(),而没有别的,那么它肯定会更糟。如果你真的给它一些源代码的用途 get_vector(),然后看看什么组件为源代码生成。请参阅此示例,以了解上述修改加上实际使用情况以及编译器优化可能导致编译器产生的结果。


我只将上面的代码(具有memcpy)放入计算机资源管理器中,否则该问题将变得毫无意义。话虽这么说,您的答案非常擅长显示产生更好代码的不同方法。您提供两种方法:使用静态方法和将构造函数直接放在输出中。因此,代码分析器可以建议这些方式。
Mathieu Dutour Sikiric
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.