1

假设ptr是指向类型对象的指针T1并且inst是类型的实例T2

T1* ptr(new T1);
T2 inst;

我设计了相应的方法T1T2这意味着T1我几乎只有对对象void进行操作的函数,而在内部我将拥有访问实际成员的方法。所以我终于像这样打了两个电话:thisT2

ptr->doSomething();
inst.doSomething();

考虑到这两个主要区别(指针 vs 实例和实际调用->vs .)以及 vs 的使用thismember values在多线程和高性能环境中,施加的内存模型ptr是否inst相同?上下文切换、堆栈创建/分配、访问值等的成本如何?

编辑:

奇怪的是,没有人提到分配器是一个新玩家,可以改变分配或地方的游戏。

我想把重点放在内存模型上,以及硬件内部的工作方式(主要是 x86 和 ARM)。

4

2 回答 2

3

您的问题似乎很简单:调用“ptr->something()”和“instance.something()”有什么区别?

从功能“某物”的角度来看,绝对没有。

#include <iostream>

struct Foo {
    void Bar(int i) { std::cout << i << "\n"; }
};

int main() {
    Foo concrete;
    Foo* dynamic = new Foo;

    concrete.Bar(1);
    dynamic->Bar(2);

    delete dynamic;
}

编译器只发出一个必须处理这两种情况的 Foo::Bar() 实例,因此没有任何区别。

唯一的变化(如果有的话)是在呼叫站点。调用dynamic->Bar()编译器时,将发出等效于this = dynamic; call Foo0Bar将“动态”的值直接传输到“this”所在的位置(寄存器/地址)的代码。在 的情况下concrete.Bar,concrete 将在堆栈上,因此它会发出稍微不同的代码来将堆栈偏移量加载到相同的寄存器/内存位置并进行调用。函数本身将无法分辨。

- - 编辑 - -

这是来自“g++ -Wall -o test.exe -O1 test.cpp && objdump -lsD test.exe | c++filt”的程序集,上面的代码主要是:

main():
  400890:       53                      push   %rbx
  400891:       48 83 ec 10             sub    $0x10,%rsp
  400895:       bf 01 00 00 00          mov    $0x1,%edi
  40089a:       e8 f1 fe ff ff          callq  400790 <operator new(unsigned long)@plt>
  40089f:       48 89 c3                mov    %rax,%rbx
  4008a2:       be 01 00 00 00          mov    $0x1,%esi
  4008a7:       48 8d 7c 24 0f          lea    0xf(%rsp),%rdi
  4008ac:       e8 47 00 00 00          callq  4008f8 <Foo::Bar(int)>
  4008b1:       be 02 00 00 00          mov    $0x2,%esi
  4008b6:       48 89 df                mov    %rbx,%rdi
  4008b9:       e8 3a 00 00 00          callq  4008f8 <Foo::Bar(int)>
  4008be:       48 89 df                mov    %rbx,%rdi
  4008c1:       e8 6a fe ff ff          callq  400730 <operator delete(void*)@plt>
  4008c6:       b8 00 00 00 00          mov    $0x0,%eax
  4008cb:       48 83 c4 10             add    $0x10,%rsp
  4008cf:       5b                      pop    %rbx
  4008d0:       c3                      retq   

我们的成员函数调用在这里:

混凝土钢筋(1)

4008a2:       be 01 00 00 00          mov    $0x1,%esi
4008a7:       48 8d 7c 24 0f          lea    0xf(%rsp),%rdi
4008ac:       e8 47 00 00 00          callq  4008f8 <Foo::Bar(int)>

动态->酒吧(2)

4008b1:       be 02 00 00 00          mov    $0x2,%esi
4008b6:       48 89 df                mov    %rbx,%rdi
4008b9:       e8 3a 00 00 00          callq  4008f8 <Foo::Bar(int)>

显然“rdi”被用来保存“this”,第一个使用堆栈相对地址(因为concrete在堆栈上),第二个简单地复制“rbx”的值,它具有来自“new”的返回值早些时候(mov %rax,%rbx在调用 new 之后)

---- 编辑 2 ----

除了函数调用本身之外,对于必须在对象内构造、拆除和访问值的实际操作而言,堆栈通常更快。

{
    Foo concrete;
    foo.Bar(1);
}

通常比

Foo* dynamic = new Foo;
dynamic->Bar(1);
delete dynamic;

因为第二种变体必须分配内存,而且通常内存分配器很慢(它们通常有某种锁来管理共享内存池)。此外,为此分配的内存可能是缓存冷的(尽管大多数股票分配器会将块数据写入页面,导致在您开始使用它时它变得有点缓存热,但这可能会导致页面错误,或将其他东西推出缓存)。

使用堆栈的另一个潜在优势是通用缓存一致性。

int i, j, k;
Foo f1, f2, f3;
// ... thousands of operations populating those values
f1.DoCrazyMagic(f1, f2, f3, i, j, k);

如果内部没有外部引用DoCrazyMagic,那么所有操作都将发生在一个小的内存区域内。相反,如果我们这样做

int *i, *j, *k;
Foo *f1, *f2, *f3;
// ... thousands of operations populating those values
f1->DoCrazyMagic(*f1, *f2, *f3, *i, *j, *k);

可以想象,在复杂的场景中,变量会分布在多个页面上,并可能导致多个页面错误。

然而——如果“数千个操作”足够密集和复杂,我们放置的堆栈区域i, j, k, f1, f2 and f3可能不再是“热”的。

换句话说:如果您滥用堆栈,它也会成为一种有争议的资源,并且相对于堆使用的优势会被边缘化或消除。

于 2013-11-13T08:07:51.957 回答
0

两个实例之间的主要区别与对象生命周期有关。

T1具有动态分配,意味着它的生命周期在delete被调用时结束;T2具有自动分配,意味着它的生命周期在执行离开分配的封闭块时结束。

在动态变量或自动变量之间进行选择时,对象生命周期应该是主要的决策因素。

第二个决定因素应该是对象大小。自动对象通常存储在大小有限的“堆栈”上。相反,动态分配的对象可以有更大的大小。

遥远的第三个因素可能是引用的局部性,这可能意味着,在某些情况下,间接 ( ->) 将施加微小的性能损失。这是只有分析器才能知道的事情。

我相应地设计了 T1 和 T2 的方法,这意味着在 T1 中我几乎只有对 this 对象进行操作的 void 函数,而在 T2 中我将拥有可以访问实际成员的方法。

这真的没有多大意义。两个类都可以有成员和非空函数。

请注意,动态内存分配会产生成本,而且通常,内存分配器必须在内部获取锁。您可以尝试不同的分配器(如TCMalloc等),它们在多线程场景中提供了一些性能改进。

使用动态存储也存在真正的内存泄漏线程,忘记调用delete. 这可以通过使用智能指针来缓解,但它们会增加自己的性能损失。

总的来说,无论是否在多线程环境中,唯一真正的问题是您是否真的需要动态分配提供的(生命周期或大小)属性并愿意支付其性能成本。

(做出决定之前应该衡量的成本。完美是足够好的敌人。)

于 2013-11-13T07:43:15.907 回答