假设ptr
是指向T1
类型对象的指针,而inst
是T2
类型的实例:
T1* ptr(new T1);
T2 inst;
我相应地设计了T1
和T2
的方法,这意味着在T1
我几乎只有void
个函数可以在this
上运行对象和内部T2
我将有方法来访问实际成员。
所以我最终打了2个电话:
ptr->doSomething();
inst.doSomething();
考虑这两个主要差异(指针与实例和实际调用->
vs .
)以及可能使用this
vs member values
,在多线程和高性能环境,强加于ptr
和inst
的内存模型是一样的吗?上下文切换,堆栈创建/分配,访问值等等的成本如何?
修改
奇怪的是,没有人提到分配器是一个新的玩家,可以改变游戏的分配或地点。
我想把重点放在内存模型上,关于硬件内部的工作方式(主要是x86和ARM)。
答案 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
。这可以通过使用智能指针来缓解,但它们会增加自己的性能损失。
总的来说,在多线程环境中,唯一真正的问题是,您是否确实需要动态分配提供的(生命周期或大小)属性,并愿意支付其性能成本。
(在做出决定之前你应该衡量的成本。完美是足够好的敌人。)