假设ptr
是指向T1
类型的对象的指针,而inst
是T2
类型的实例:
T1* ptr(new T1);
T2 inst;
我相应地设计了
T1
和T2
的方法,这意味着在T1
中,我几乎只有void
函数,这些函数将对this
对象进行操作,而在T2
内部,我将具有将访问实际成员的方法。所以我终于打了两个电话,像这样:
ptr->doSomething();
inst.doSomething();
考虑到这两个主要区别(指针与实例以及实际调用
->
与.
)以及在多线程高性能环境中使用this
与member values
,施加在ptr
和inst
上的内存模型是否相同?上下文切换,堆栈创建/分配,访问值等的成本如何?编辑:
奇怪的是,没有人提到分配者是可以改变分配或局域性的新玩家。
我想将重点放在内存模型上,以及硬件(主要是x86和ARM)内部的工作方式。
最佳答案
似乎您的问题很简单:调用“ptr-> something()”和“instance.something()有什么区别?”
从功能“某物”的 Angular 来看,绝对没有。
#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()的一个实例,该实例必须处理两种情况,因此不会有任何区别。
唯一的更改(如果有)在 call 站点。调用
dynamic->Bar()
时,编译器将发出与this = dynamic; call Foo0Bar
等效的代码,以将“dynamic”的值直接传输到保存“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
我们的成员函数调用在这里:
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)>
动态->酒吧(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”的返回值”(在调用new之后的mov %rax,%rbx
之前)----编辑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
的堆栈区域就不再“热”了。换句话说,如果您滥用堆栈,它也将成为有争议的资源,并且相对于堆使用的优势将被边缘化或消除。