【问题标题】:Differences in memory management with these 2 function calls?这两个函数调用在内存管理方面的差异?
【发布时间】:2013-11-13 06:59:23
【问题描述】:

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

T1* ptr(new T1);
T2 inst;

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

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

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

编辑:

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

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

【问题讨论】:

  • 请注意,两者都在内部使用this,无论是显式还是隐式。
  • 注意:你的第二个“实例”不是;它是一个名为inst 的函数的函数声明,它返回一个T2 类型的按值对象,并且不带任何参数。我想你的意思是T2 inst;
  • 我不确定这与多线程有什么关系,但是如果您想了解生成的内容,请编写一些代码并生成 asm 并查看它们是否相同,如果不同你可能会想出一些。或者你可以回来发布示例代码
  • @WhozCraig 谢谢,我已经解决了。
  • 你的问题标题中的两项无关,内存分配的方法与函数的调用方式没有显着差异,如kfsone所示。

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


【解决方案1】:

您的问题似乎很简单:调用“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-&gt;Bar() 时,编译器将发出等效于this = dynamic; call Foo0Bar 的代码,以将“动态”的值直接传输到“this”所在的位置(寄存器/地址)。在concrete.Bar 的情况下,具体将在堆栈上,因此它会发出稍微不同的代码来将堆栈偏移量加载到相同的寄存器/内存位置并进行调用。函数本身将无法判断。

---- 编辑----

这是来自“g++ -Wall -o test.exe -O1 test.cpp && 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   

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

混凝土.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)>

动态->条形图(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 只是为了添加 - 堆栈更快,堆更大,它们之间的决定取决于使用特定和超出范围
  • @GlennTeitelbaum 好点 - 对 edit2 有任何反馈吗? [注:我只看到了'and out of scope',呵呵]
  • 保存这篇文章并在有人以直接且易于搜索的方式明确提出问题时重复使用它
【解决方案2】:

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

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

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

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

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

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

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

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

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

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

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

【讨论】:

  • 只有指针值在多个线程共享的情况下才真正需要锁定内存,如果分配、使用和删除都发生在单个线程中,则不需要锁定内存。
猜你喜欢
  • 1970-01-01
  • 2017-03-26
  • 1970-01-01
  • 1970-01-01
  • 2014-05-27
  • 2013-10-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多