unique_ptr与类实例作为成员变量
unique_ptr vs class instance as member variable
有一个类SomeClass
,它保存一些数据和对这些数据进行操作的方法。它必须用一些参数来创建,比如:
SomeClass(int some_val, float another_val);
还有另一个类,比如Manager
,它包括SomeClass
,并且大量使用它的方法。
那么,在性能(数据位置、缓存命中率等)方面,将SomeClass
的对象声明为Manager
的成员,并在Manager
的构造函数中使用成员初始化,或者将SomeClass的对象宣布为unique_ptr,什么会更好呢?
class Manager
{
public:
Manager() : some(5, 3.0f) {}
private:
SomeClass some;
};
或
class Manager
{
public:
Manager();
private:
std::unique_ptr<SomeClass> some;
}
简短回答
最有可能的是,访问子对象的运行时效率没有差异。但由于以下几个原因,使用指针的速度可能较慢(请参阅下面的详细信息)。
此外,还有其他几件事你应该记住:
- 当使用指针时,通常必须分别为子对象分配/取消分配内存,这需要一些时间(如果做得太多,则需要相当长的时间)
- 使用指针时,可以在不复制的情况下廉价地移动子对象
说到编译时间,指针比普通成员要好。对于普通成员,您无法删除Manager
声明对SomeClass
声明的依赖关系。使用指针,您可以使用正向声明来完成此操作。更少的依赖性可能会导致更少的构建时间。
详细信息
我想提供更多关于子对象访问性能的详细信息。我认为使用指针可能比使用普通成员慢,原因有几个:
- 使用普通成员时,数据局部性(和缓存性能)可能会更好。您通常同时访问
Manager
和SomeClass
的数据,并且保证普通成员靠近其他数据,而堆分配可能会将对象和子对象放置在彼此远离的位置 - 使用指针意味着多了一个间接级别。要获得普通成员的地址,只需为对象地址添加一个编译时常数偏移量(它通常与其他汇编指令合并)。当使用指针时,您必须从成员指针中额外读取一个单词,才能获得指向子对象的实际指针。有关更多详细信息,请参见Q1和Q2
- 别名可能是最重要的问题。若您使用的是纯成员,那个么编译器可以假设:您的子对象完全位于内存中的对象中,并且它不会和对象的其他成员重叠。当使用指针时,编译器通常不能这样假设:子对象可能和对象及其成员重叠。结果,编译器不得不生成更多无用的加载/存储操作,因为它认为某些值可能会更改
下面是最后一期的例子(完整代码在这里):
struct IntValue {
int x;
IntValue(int x) : x(x) {}
};
class MyClass_Ptr {
unique_ptr<IntValue> a, b, c;
public:
void Compute() {
a->x += b->x + c->x;
b->x += a->x + c->x;
c->x += a->x + b->x;
}
};
显然,通过指针存储子对象a
、b
、c
是愚蠢的。我测量了为单个对象调用Compute
方法10亿次所花费的时间。以下是不同配置的结果:
2.3 sec: plain member (MinGW 5.1.0)
2.0 sec: plain member (MSVC 2013)
4.3 sec: unique_ptr (MinGW 5.1.0)
9.3 sec: unique_ptr (MSVC 2013)
当查看每种情况下最内部循环的生成程序集时,很容易理解为什么时间如此不同:
;;; plain member (GCC)
lea edx, [rcx+rax] ; well-optimized code: only additions on registers
add r8d, edx ; all 6 additions present (no CSE optimization)
lea edx, [r8+rax] ; ('lea' instruction is also addition BTW)
add ecx, edx
lea edx, [r8+rcx]
add eax, edx
sub r9d, 1
jne .L3
;;; plain member (MSVC)
add ecx, r8d ; well-optimized code: only additions on registers
add edx, ecx ; 5 additions instead of 6 due to a common subexpression eliminated
add ecx, edx
add r8d, edx
add r8d, ecx
dec r9
jne SHORT $LL6@main
;;; unique_ptr (GCC)
add eax, DWORD PTR [rcx] ; slow code: a lot of memory accesses
add eax, DWORD PTR [rdx] ; each addition loads value from memory
mov DWORD PTR [rdx], eax ; each sum is stored to memory
add eax, DWORD PTR [r8] ; compiler is afraid that some values may be at same address
add eax, DWORD PTR [rcx]
mov DWORD PTR [rcx], eax
add eax, DWORD PTR [rdx]
add eax, DWORD PTR [r8]
sub r9d, 1
mov DWORD PTR [r8], eax
jne .L4
;;; unique_ptr (MSVC)
mov r9, QWORD PTR [rbx] ; awful code: 15 loads, 3 stores
mov rcx, QWORD PTR [rbx+8] ; compiler thinks that values may share
mov rdx, QWORD PTR [rbx+16] ; same address with pointers to values!
mov r8d, DWORD PTR [rcx]
add r8d, DWORD PTR [rdx]
add DWORD PTR [r9], r8d
mov r8, QWORD PTR [rbx+8]
mov rcx, QWORD PTR [rbx] ; load value of 'a' pointer from memory
mov rax, QWORD PTR [rbx+16]
mov edx, DWORD PTR [rcx] ; load value of 'a->x' from memory
add edx, DWORD PTR [rax] ; add the 'c->x' value
add DWORD PTR [r8], edx ; add sum 'a->x + c->x' to 'b->x'
mov r9, QWORD PTR [rbx+16]
mov rax, QWORD PTR [rbx] ; load value of 'a' pointer again =)
mov rdx, QWORD PTR [rbx+8]
mov r8d, DWORD PTR [rax]
add r8d, DWORD PTR [rdx]
add DWORD PTR [r9], r8d
dec rsi
jne SHORT $LL3@main
相关文章:
- 将成员变量添加到共享库中的类中,不会破坏二进制兼容性吗
- 具有奇怪重复模板模式的派生类中的成员变量已损坏
- 内置函数可查看CPP中的成员变量
- 是否可以初始化不可复制类型的成员变量(或基类)
- 将包含C样式数组的对象初始化为成员变量(C++)
- 为什么我不能在一个类的不同行中声明和定义成员变量?
- 在循环中按顺序遍历成员变量
- c++类声明时,相同的例程,不同的成员变量类型
- 如何从另一个文件继承私有成员变量和公共函数
- 在C++类中,是否必须初始化作为数组的成员变量
- 如何从子成员函数修改父公共成员变量
- 我可以在 C++ 中将数据成员/变量从其定义之外添加到结构中吗?
- 从私有成员变量的成员方法返回unique_ptr
- 在派生类中使用基类的私有成员变量的最佳方法
- 静态 constexpr 类成员变量对多线程读取是否安全?
- C++:是否可以使用非静态成员变量模板?
- 打印所有继承的类成员变量和方法
- 如何在复杂继承中访问静态成员变量
- 为什么我不能在返回 const 的布尔函数中为类成员变量赋值?C++
- 成员变量与函数概念检查