取消引用堆栈中的变量或最近取消引用的成本?

时间:2016-09-30 22:37:00

标签: c++ pointers memory

使用现代编译器,当它指向的数据最近被取消引用时,第二次取消引用指针是否昂贵?

int * ptr = new int(); 
... lots of stuff...
*ptr = 1; // may need to load the memory into the cpu
*ptr = 2; // accessed again, can I assume this will usually be loaded and cost nothing extra?

如果指针指向堆栈中的变量,我可以假设通过指向堆栈变量的指针读取/写入与直接读取/写入变量的成本相同吗?

int var;
int * ptr = &var;

*ptr = 0; // will this cost the same as if I just said var = 0; ?

最后,这是否扩展到更复杂的事情,例如通过接口操作堆栈上的基础对象?

Base baseObject;
Derived * derivedObject = &baseObject;
derivedObject->value = 42;    // will this have the same cost as if I just--
derivedObject->doSomething()  // --manipulated baseObject directly?

编辑:我要求此人加深理解;这不是一个需要解决的问题,而是一个洞察力的要求。请不要担心"过早优化"或其他实际问题,只要给我所有绳索:)

4 个答案:

答案 0 :(得分:2)

这个问题包含许多含糊之处。

一个简单的经验法则是,取消引用某些内容总是会有相同的成本,除非它没有。

取消引用的成本有很多因素 - 是缓存中的目标,是分页还是编译器生成的代码。

代码段

Obj* p = new Obj;
// <elided> //
p->something = 1;

查看此源代码,我们无法判断可执行文件是否会加载p的值,* p是否在缓存中,或者是否甚至已访问过*。

Obj* p = new Obj;
p->something = 1;

我们仍然不能确定* p是否被分页/缓存,但是大多数现代编译器/优化器都不会发出检索p的代码并将其存储然后再次获取它。

在现代硬件的实践中,你真的不应该关心它,如果你是的话,首先要看看装配。

我将使用频谱的两端:

struct Obj { int something; int other; };

Obj* f() {
  Obj* p = new Obj;
  p->something = 1;
  p->other = 2;
  return p;
}

extern void fn2(Obj**);

Obj* h() {
  Obj* p = new Obj;
  fn2(&p);
  p->something = 1;
  fn2(&p);
  p->other = 2;
  return p;
}

produces

f():
        subq    $8, %rsp
        movl    $8, %edi
        call    operator new(unsigned long)
        movl    $1, (%rax)
        movl    $2, 4(%rax)
        addq    $8, %rsp
        ret

h():
        subq    $24, %rsp
        movl    $8, %edi
        call    operator new(unsigned long)
        leaq    8(%rsp), %rdi
        movq    %rax, 8(%rsp)
        call    fn2(Obj**)
        movq    8(%rsp), %rax
        leaq    8(%rsp), %rdi
        movl    $1, (%rax)
        call    fn2(Obj**)
        movq    8(%rsp), %rax
        movl    $2, 4(%rax)
        addq    $24, %rsp
        ret

这里编译器必须保留并恢复在调用后取消引用它的指针,但这有点不公平,因为指针可以被被调用的函数修改。

Obj* h() {
  Obj* p = new Obj;
  fn2(nullptr);
  p->something = 1;
  fn2(nullptr);
  p->other = 2;
  return p;
}

产生

h():
        pushq   %rbx
        movl    $8, %edi
        call    operator new(unsigned long)
        xorl    %edi, %edi
        movq    %rax, %rbx
        call    fn2(Obj**)
        xorl    %edi, %edi
        movl    $1, (%rbx)
        call    fn2(Obj**)
        movq    %rbx, %rax
        movl    $2, 4(%rbx)
        popq    %rbx
        ret

我们仍然看到一些注册诡计,但它并不昂贵。

至于你关于堆栈指针的问题,一个好的优化器将能够消除这些问题,但是你必须再次参考你所选择的编译器为你的特定平台生成的汇编。

struct Obj { int something; int other; };

void fn(Obj*);

void f()
{
  Obj o;
  Obj* p = &o;
  p->something = 1;
  p->other = 1;
  fn(p);
}

produces以下p已基本消除。

f():
        subq    $24, %rsp
        movq    %rsp, %rdi
        movl    $1, (%rsp)
        movl    $1, 4(%rsp)
        call    fn(Obj*)
        addq    $24, %rsp
        ret

当然,如果我们将&p传递给某个东西,编译器就不能完全忽略它,但它仍然可能足够智能,以避免在它没有绝对时使用它必须。

答案 1 :(得分:1)

信任编译器。

非常确定编译器会生成代码以尽可能减少工作量,同时考虑到CPU架构的特性,以及编译器可以考虑的任何内容。

答案 2 :(得分:1)

  

使用现代编译器,取消引用指针a是否昂贵   第二次,当它指向的数据最近被解除引用时?

通过完全优化,编译器可能能够重新排列代码(取决于代码),或者可能将指针存储到寄存器中,或者可能将值存储在寄存器中......所以可能。

有人可能会查看生成的assy代码进行确认。

我认为这是不成熟的优化。

另外,如果你正在考虑缓存,那么可能(但不能保证),当它们在时间上靠近并且两个mem地址都在同一个缓存块中时,通过指针的两个解引用将分别访问缓存mem而不用缓存未命中。

任何写入都将被放置在缓存中,并在缓存hw到达它时传送到内存或缓存未命中导致刷新到内存。

  

如果指针指向堆栈上的变量怎么办?   通过指向堆栈变量的指针读/写成本相同   直接读/写变量?

我怀疑你能否或应该承担任何责任。您可以检查生成的组件,以查看编译器在此目标体系结构上使用此代码执行的操作以及此编译器版本和构建选项选项等。数百甚至数千个可能影响代码生成的变量。

请注意,数据缓存也适用于堆栈访问。

同样,我认为这种过早的优化。

  

最后,这会扩展到更复杂的事情,例如   通过接口操作堆栈上的基础对象?

通常,编译器做得很好。所以,在这个意义上,可能。不保证。

我认为使用移动语义(C ++特性)很有价值,但这可能与你的问题没有关系。

硬件缓存可能比您可能希望手动计数(或模拟)的任何循环次数更重要。令我印象深刻的是,有多少数据缓存(用于自动变量和动态变量)提高了嵌入式系统的性能。但代码缓存也令人印象深刻。我不想做任何一件事。

过早优化我的意思是

a)众所周知,人们无法理解(或“猜测”)其节目中热点的位置,即20%的代码消耗80%的周期想法。这就是为什么有工具来帮助指出它们。

b)我一直听说更好的算法胜过其他选择,我想这通常都是正确的。更好的算法是你应该学习的。从你的SO代表,你可能比我知道更多。

但是,我认为可读性是更合适的评估标准。我喜欢听的前同事的反馈是,“......我们仍然使用你的代码。”因为该陈述表明它有效,没有给它们太多麻烦,足够快,而且(最重要的)可读。

c)任何代码中的计数周期只能通过模拟来尝试。我已经为嵌入式军用处理器做过。

要模拟缓存操作,您实际上必须有可用的代码来评估,必须了解处理器和缓存之间的交互,并且必须知道数据和指令缓存的缓存块大小。

选择更快(作为标准)是......如果第一个版本符合要求,您的客户(老板,团队负责人等)可能不愿意等待/支付更快的代码。

我一直在做一个大项目来修复被认为太慢的“系统”(前任选择,而不是我的)......管理层选择安全理解和成本估算路径:重新设计处理器卡大约10倍的处理周期和32倍的内存。软件团队重构了代码,并尽可能地添加了最新功能。从过载开始,新的“系统”以1/3工作周期运行。在每个命令响应时间都可见的重大改进。

答案 3 :(得分:0)

对于示例1和2,它取决于上下文。如果您不使用它们只是做商店,编译器将忽略这些。对于最后一个示例,这是编译错误,您无法从Base

指向Derived*对象

自己检查:

https://godbolt.org/g/pMiOfj

如果Base扩展,派生案例(来自评论中的示例): 答案再次取决于编译器知道多少信息。如果它可以看到它只是堆栈中的对象,它将优化它们两者 将是等同的。 https://godbolt.org/g/jIKQJv

注意:您不必担心此类细节,编译器会更好地进行优化。使用更具可读性的概念。这是不成熟的优化。