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

T1* ptr(new T1);
T2 inst;

我相应地设计了T1T2的方法,这意味着在T1中,我几乎只有void函数,这些函数将对this对象进行操作,而在T2内部,我将具有将访问实际成员的方法。
所以我终于打了两个电话,像这样:
ptr->doSomething();
inst.doSomething();

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

编辑:

奇怪的是,没有人提到分配者是可以改变分配或局域性的新玩家。

我想将重点放在内存模型上,以及硬件(主要是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的堆栈区域就不再“热”了。

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

08-18 00:16
查看更多