gpt4 book ai didi

c++ - 如何发现C++虚假复制操作?

转载 作者:行者123 更新时间:2023-12-02 08:21:20 25 4
gpt4 key购买 nike

最近,我有以下情况

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. 我可以通过查看标准"is"来识别是否发生了复制操作。但我最初的错误想法是编译器会优化掉这个拷贝。我错了。
  3. 这(可能)不是编译器问题,因为 clang 和 gcc 都会生成生成 memcpy 的代码。
  4. memcpy 可能很便宜,但我无法想象复制内存和删除原始内存比通过 std::move 传递指针更便宜的情况。
  5. 添加std::move是一个基本操作。我想代码分析器能够建议这种更正。

最佳答案

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

返回值不会发生复制,因为每个正常的聪明编译器都会使用 (N)RVO在这种情况下。从 C++17 开始,这是强制性的,因此您无法通过从函数返回本地生成的 vector 来看到任何拷贝。

好的,让我们来玩一下 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)我们从初始化列表创建一个 vector ,也许我们期望看到 4 次构造和 4 次移动。但我们得到了 4 份!这听起来有点神秘,但原因是初始化列表的实现!简而言之,不允许从列表中移动元素,因为列表中的迭代器是 const T* ,这使得无法从列表中移动元素。有关此主题的详细答案可以在这里找到:initializer_list and move semantics

示例 2)在本例中,我们得到一个初始构造和该值的 4 个拷贝。这没什么特别的,也是我们可以期待的。

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

示例 4)现在我们先预留空间,稍后再填补。现在我们没有拷贝,也没有移动了!

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

回到你的问题:

"How to find C++ spurious copy operations"

如上所示,您可以在中间引入一个代理类以用于调试目的。

在许多情况下,将复制者设为私有(private)可能不起作用,因为您可能有一些想要的拷贝和一些隐藏的拷贝。如上所述,只有示例 4 的代码可以与私有(private)复制者一起使用!我无法回答这个问题,如果示例 4 是最快的,因为我们会以和平来实现和平。

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

我的提示是不要把注意力集中在这么小的问题上。如果您确实遇到性能问题,请使用分析器进行测量。有太多潜在的性能 killer ,因此在虚假的 memcpy 使用上投入大量时间似乎不是一个值得的想法。

关于c++ - 如何发现C++虚假复制操作?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/59560436/

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com