unique_ptr和类实例作为成员变量的区别

10

有一个类SomeClass,它保存了一些数据和操作这些数据的方法。必须用一些参数创建它,例如:

SomeClass(int some_val, float another_val);

还有一个类,假设叫做Manager,它包括SomeClass并且大量使用其方法。

因此,在性能方面(数据局部性,缓存命中等),在Manager的构造函数中将SomeClass对象声明为成员,并使用成员初始化更好呢?还是将SomeClass对象声明为unique_ptr?

class Manager
{    
public:    
    Manager() : some(5, 3.0f) {}

private:
    SomeClass some;    
};

或者

class Manager
{
public:
    Manager();

private:
    std::unique_ptr<SomeClass> some;
}

3
使用指针可能是最好的选择,但你确实应该进行基准测试以了解它对应用程序的影响。这取决于访问模式。 - juanchopanza
1
当然正确,因为“Manager”很可能是一个稳定的对象。然而,如果OP的“等等”包括频繁移动,则使用指针可能会更好。 - Angew is no longer proud of SO
就编译时间而言,使用指针可以使用PIMPL惯用法。就运行时性能而言:进行性能分析。 - Ben
1个回答

15

简短回答

访问子对象的运行时效率可能没有区别,但使用指针可能会因为多种原因而更慢(下文详细解释)。

此外,还有几个其他事项需要记住:

  1. 使用指针时,您通常需要单独为子对象分配/释放内存,这需要一些时间(如果您经常这样做,则需要相当长的时间)。
  2. 使用指针时,可以在不复制的情况下廉价地移动子对象。

谈到编译时间,指针比纯成员更好。对于纯成员,您无法消除Manager声明对SomeClass声明的依赖关系。使用指针,您可以通过前向声明来完成。较少的依赖可能导致构建时间较短。

详细信息

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

  1. 使用纯成员时,数据局部性(和缓存性能)很可能更好。通常,您同时访问ManagerSomeClass的数据,并且保证纯成员靠近其他数据,而堆分配可能会将对象和子对象距离很远。
  2. 使用指针意味着多一层间接寻址。要获取纯成员的地址,可以简单地添加编译时常量偏移量到对象地址(通常与其他汇编指令合并)。当使用指针时,您还需要从成员指针中额外读取一个字以获取实际的子对象指针。有关详细信息,请参见Q1Q2
  3. 别名问题也许是最重要的问题。如果您使用纯成员,则编译器可以假定:您的子对象完全位于您的对象内存中,且不与您的对象其他成员重叠。使用指针时,编译器通常无法假定任何类似的内容:您的子对象可能与您的对象及其成员重叠。因此,编译器必须生成更多的无用的加载/存储操作,因为它认为某些值可能会更改。

以下是最后一个问题的示例(完整代码在这里):

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接