unique_ptr vs class instance as member variable

时间:2015-04-04 13:09:17

标签: c++ performance

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

1 个答案:

答案 0 :(得分:8)

简短回答

最有可能的是,访问子对象的运行时效率存在无差异。但由于多种原因,使用指针可能会变慢(请参阅下面的详细信息)。

此外,还有其他一些你应该记住的事情:

  1. 使用指针时,通常需要单独为子对象分配/释放内存,这需要一些时间(quite a lot if you do it much)。
  2. 使用指针时,您可以便宜地移动子对象而无需复制。
  3. 说到编译时间,指针优于普通成员。使用普通成员,您无法删除Manager声明对SomeClass声明的依赖性。使用指针,您可以使用前向声明来完成。较少的依赖关系可能会减少构建时间。

    详细信息

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

    1. 使用普通成员,数据位置(和缓存性能)可能会更好。您通常一起访问ManagerSomeClass的数据,并且保证普通成员接近其他数据,而堆分配可能使对象和子对象彼此远离。
    2. 使用指针意味着更多级别的间接。要获取普通成员的地址,可以简单地添加对象地址的编译时常量偏移量(通常与其他汇编指令合并)。使用指针时,您还必须从成员指针中读取一个单词以获取指向子对象的实际指针。有关详细信息,请参阅Q1Q2
    3. Aliasing也许是最重要的问题。如果您使用普通成员,那么编译器可以假设:您的子对象完全位于内存中的对象内,并且它不与对象的其他成员重叠。使用指针时,编译器通常不能假设这样的事情:您的子对象可能与您的对象及其成员重叠。因此,编译器必须生成更多无用的加载/存储操作,因为它认为某些值可能会发生变化。
    4. 以下是上一期的完整代码示例(完整代码为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是愚蠢的。我已经测量了对单个对象进行10亿次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