unique_ptr vs 类实例作为成员变量

Posted

技术标签:

【中文标题】unique_ptr vs 类实例作为成员变量【英文标题】:unique_ptr vs class instance as member variable 【发布时间】:2015-04-04 13:09:17 【问题描述】:

有一个类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;

【问题讨论】:

使用指针再好不过了。但是你真的应该对它进行基准测试,看看它如何影响你的应用程序。这实际上取决于访问模式。 @juanchopanza 当然正确,因为Manager 很可能是一个稳定的对象。但是,如果 OP 的“等”,指针 可能 会更好。包括经常四处走动。 在编译时间方面,使用指针,这样可以使用 PIMPL 成语。在运行时性能方面:配置文件。 【参考方案1】:

简答

很可能,访问子对象的运行时效率没有区别。但是由于几个原因,使用指针可能会变慢(请参阅下面的详细信息)。

此外,您还应该记住其他几件事:

    使用指针时,通常需要为子对象单独分配/释放内存,这需要一些时间(quite a lot if you do it much)。 使用指针时,您可以廉价地移动子对象而无需复制。

说到编译时间,指针比普通成员好。使用普通成员,您不能删除 Manager 声明对 SomeClass 声明的依赖。使用指针,您可以使用前向声明来完成。更少的依赖可能会导致更少的构建时间。

详情

我想提供有关子对象访问性能的更多详细信息。我认为使用指针可能比使用普通成员慢,原因如下:

    普通成员的数据局部性(和缓存性能)可能会更好。 ManagerSomeClass 的数据通常是一起访问的,plain member 保证靠近其他数据,而堆分配可能会将对象和子对象放置在远离彼此的位置。 使用指针意味着多了一层间接性。要获取普通成员的地址,您可以简单地为对象地址添加一个编译时常量偏移量(通常与其他汇编指令合并)。使用指针时,您必须另外从成员指针中读取一个字以获取指向子对象的实际指针。有关详细信息,请参阅 Q1 和 Q2。 Aliasing 可能是最重要的问题。如果您使用的是普通成员,那么编译器可以假设:您的子对象完全位于内存中的对象中,并且不与对象的其他成员重叠。使用指针时,编译器通常不能假设这样的事情:您的子对象可能与您的对象及其成员重叠。结果,编译器不得不生成更多无用的加载/存储操作,因为它认为某些值可能会改变。

这是上一期的示例(完整代码为here):

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;
    
;

显然,通过指针存储子对象abc 是愚蠢的。我已经测量了对单个对象调用 Compute 方法的十亿次调用所花费的时间。以下是不同配置的结果:

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

【讨论】:

以上是关于unique_ptr vs 类实例作为成员变量的主要内容,如果未能解决你的问题,请参考以下文章

将多态成员变量实例化为适当的类型

类数据成员的破坏顺序?

JAVA 类变量和成员变量怎么理解

内部类和外部类的实例变量可以共存

静态类静态方法静态成员和实例成员的比较

如何在vs2008 MFC中添加、删除成员变量