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

Differences in memory management with these 2 function calls?

本文关键字:管理 内存 函数调用      更新时间:2023-10-16

>假设ptr是指向类型为 T1 的对象的指针,inst 是类型 T2 的实例:

T1* ptr(new T1);
T2 inst;

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

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

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

编辑:

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

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

似乎你的问题很简单:调用"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 的情况下,混凝土将在堆栈上,因此它将发出略有不同的代码以将堆栈偏移量加载到相同的寄存器/内存位置并进行调用。函数本身将无法分辨。

----编辑----

这是来自"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   

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

混凝土。酒吧(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的堆栈区域可能不再"热"。

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

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

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

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

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

遥远的第三个因素可能是参考位置,这可能意味着,在某些情况下,间接寻址(->)将施加一分钟的性能损失。这是只有探查器才能知道的。

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

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

请注意,动态内存分配

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

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

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

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