这 2 个函数调用的内存管理差异
Differences in memory management with these 2 function calls?
>假设ptr
是指向类型为 T1
的对象的指针,inst
是类型 T2
的实例:
T1* ptr(new T1);
T2 inst;
我相应地设计了T1
和T2
的方法,这意味着在T1
中,我几乎只有void
个函数可以在this
对象上运行,并且在T2
内部我将拥有访问实际成员的方法。所以我终于打了两个这样的电话:
ptr->doSomething();
inst.doSomething();
考虑到这两个主要区别(指针与实例以及实际调用->
vs .
),也许使用 this
与 member values
,在多线程和高性能环境中,强加在ptr
和inst
上的内存模型是相同的?上下文切换、堆栈创建/分配、访问值等的成本如何?
编辑:
奇怪的是,没有人提到分配器是可以改变分配或位置游戏的新玩家。
我想把重点放在内存模型上,放在硬件内部的工作方式(主要是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
。这可以通过使用智能指针来缓解,但它们会增加自己的性能损失。
总的来说,在多线程环境中,唯一真正的问题是你是否真的需要动态分配提供的(生存期或大小)属性,并愿意支付其性能成本。
(在做出决定之前应衡量的成本。完美是足够好的敌人。
- 当vector是tje全局变量时,c++中vector的内存管理
- 当分配一个字符串值并稍后通过分配另一个值进行更改时C++如何管理内存?
- 我有一个线程 1:EXC_BAD_ACCESS(代码 = 1,地址 = 0x8)错误.我认为这是由于内存管理不好.我可以
- 可以通过非原始指针来增强容器矢量管理内存
- 如何使用 std::vector<std::tuple<A,B>> 来管理内存(调整大小、保留,...),但实际上将 As 保留在 B 之前,连续
- 线程管理内存泄漏
- 通过读取文件创建映射<字符串,矢量>时如何管理内存<string>
- 管理内存C++
- ptr_vector如何管理内存
- 在C++Builder/Firemonkey中使用表单创建来管理内存
- 如何正确管理内存(运行时)C++
- 在Node.js中使用Native Abstractions时,如何管理内存
- ostream是如何管理内存的
- C++中管理内存泄漏的问题
- OpenCL:在 CPU 上而不是在 GPU 上更正结果:如何正确管理内存
- deque是如何管理内存的
- 用c++/cli互操作管理内存
- 如何衡量管理内存所花费的时间
- (加速C++)章节管理内存
- 如何在此场景中管理内存