这两个函数调用的内存管理差异?

时间:2013-11-13 06:59:23

标签: c++ multithreading memory-management c++11

假设ptr是指向T1类型对象的指针,而instT2类型的实例:

T1* ptr(new T1);
T2 inst;

我相应地设计了T1T2的方法,这意味着在T1我几乎只有void个函数可以在this上运行对象和内部T2我将有方法来访问实际成员。 所以我最终打了2个电话:

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

考虑这两个主要差异(指针与实例和实际调用-> vs .)以及可能使用this vs member values,在多线程和高性能环境,强加于ptrinst的内存模型是一样的吗?上下文切换,堆栈创建/分配,访问值等等的成本如何?

修改

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

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

2 个答案:

答案 0 :(得分: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的代码,将“dynamic”的值直接传送到“this”所在的位置(寄存器/地址)。在concrete.Bar的情况下,具体将在堆栈上,因此它将发出稍微不同的代码以将堆栈偏移加载到相同的寄存器/存储器位置并进行调用。该功能本身无法分辨。

----编辑----

这是来自“g ++ -Wall -o test.exe -O1 test.cpp&amp;&amp; objdump -lsD test.exe | c ++ filt”的程序集,带有上面的代码,重点是main:

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   

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

concrete.Bar(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)>

dynamic-&GT;杆(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的堆栈区域可能不会再“热”。

换句话说:如果滥用堆栈,它也会成为有争议的资源,而堆使用的优势会被边缘化或消除。

答案 1 :(得分:0)

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

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

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

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

一个遥远的第三个因素可能是引用的位置,这可能意味着,在某些情况下,间接(->)会施加一分钟的性能损失。这只是一个探究者可以说的东西。

  

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

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

请注意,动态内存分配会产生成本,并且通常情况下,内存分配器必须在内部获取锁定。您可以尝试使用不同的分配器(如TCMalloc和其他分配器),这些分配器在多线程方案中提供了一些性能改进。

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

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

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