c++ - 如何找到 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 都做不到。
编辑:几点让我的问题更清楚:
- 我知道发生了复制操作,因为我使用了编译器资源管理器,它显示了对memcpy的调用。
- 通过查看标准是,我可以确定发生了复制操作。但我最初的错误想法是编译器会优化掉这个副本。我错了。
- 这(很可能)不是编译器问题,因为 clang 和 gcc 都会生成产生memcpy的代码。
- memcpy 可能很便宜,但我无法想象复制内存和删除原始内存比通过std::move传递指针更便宜的情况。
- std::move的添加是一个基本操作。我想代码分析器将能够建议这种更正。
解决方案
我相信你有正确的观察但错误的解释!
返回值不会发生复制,因为在这种情况下,每个普通的聪明编译器都会使用(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
使用上投入大量时间似乎不是一个值得的想法。
推荐阅读
- javascript - 搜索/过滤嵌套数组 Javascript/Lodash
- php - 当第一个数字较小时,PHP <= 发生故障
- selenium - 从 Selenium Webdriver 中的弹出窗口中选择地址值
- unity3d - unity 3d 中的 2d 纹理导入问题
- c++ - 如何将我的 const char 指针更改为唯一指针?
- mysql - 执行一个查询时出现错误语法
- html - 子div的边框长度与父div的宽度相同吗?
- bash - centos 和 debian 中的 /etc/init.d/functions 和 action
- ios - iOS:将 CMSampleBuffer 转换为 UIImage 以错误的方向和比例返回图像
- django - 单个帖子类别的重定向错误