【问题标题】:Efficiency of accessing a value through a pointer vs storing as temporary value通过指针访问值与存储为临时值的效率
【发布时间】:2013-12-31 19:15:33
【问题描述】:

如果有一个函数将指向struct 的指针作为参数,并且该函数有一个在每次迭代时访问成员的循环,例如:

int function_a(struct b *b)
{
    // ... do something then
    for(int i = 0; i < 500; ++i){
        b->c->d[i] = value();
    }
}

会不会每次都从内存中检索c指向和d指向的位置?

现在考虑以下情况:

int function_a(struct b *b)
{
    // ... do something then
    float *f = b->c->d;
    for(int i = 0; i < 500; ++i){
        f[i] = value();
    }
}

这样会更快吗?

【问题讨论】:

  • 循环的性能很可能被value()的性能所支配。其余的可能是过早的优化,除非您已经测量它是一种性能抑制因素。
  • @JonathanLeffler +1 提及测量。在这些情况下,它(几乎)总是更好地衡量,然后尝试推测编译器可以找出哪些优化。
  • 另外,仅从维护者的角度来看,我更喜欢看到具有正确名称的局部变量而不是一长串的取消引用...b-&gt;c-&gt;d[i] 可能更容易阅读为@987654327 @.
  • @MadScienceDreams,虽然写的是另一种语言,但 Shawn Hargreaves 在他的博客中给出的信息是一个很好的信息:Why Measure When You Can Guess?
  • 如果您可以使指针保持不变,这也可能有助于优化器解决问题。此外,对于它的价值,在声明该类型的变量时不需要重复“struct”关键字(你在 C 中,而不是在 C++ 中)。您的参数名称与类型相同也让我感到害怕 - 但我会假设原始来源不同。

标签: c++ c


【解决方案1】:

我敦促您注意 Thomas Matthews 关于分析的建议,但要回答您的问题:这取决于。

这种特殊的转换也称为代码提升,它包括在没有副作用的情况下移动代码,并且在循环外的每次调用中都具有相同的结果。如上所述,这仅在编译器能够证明的情况下执行:

  • 没有副作用
  • 每次调用都会计算出相同的结果

在这两种情况下,这基本上意味着编译器应该可以访问两者的完整代码(参见定义):

  • 表达式本身,以证明没有副作用
  • 任何可能改变表达式的东西,以证明每次都计算相同的结果

因此,它实际上不太可能执行优化除非主体循环的所有代码都包含在标头中(因此可以内联),因为任何不透明的函数都可能隐藏修改b-&gt;c(例如)通过一个邪恶的全局变量。

在您的示例中,没有任何证据证明 value() 不会更改 b-&gt;c... 所以不,编译器提升代码是错误的除非它可以访问 @ 的定义987654324@,可以排除这种可能性。

【讨论】:

    【解决方案2】:

    使用临时文件可能不会比访问临时文件快:取决于平台。

    如有疑问,请查看编译器生成的汇编语言。在 ARM 处理器上,访问内存时:

    • 一个寄存器加载了变量的地址。
    • 寄存器被取消引用以获得值(并存储在 另一个寄存器)。

    这与取消引用指针非常相似:

    • 用指针值加载寄存器。
    • 寄存器被取消引用以获得值。

    可能会从内存进行第二次加载以获取指针值。事实是汇编语言。

    这被称为微优化,只能作为在性能关键区域加速代码的最后手段。使用分析器找出瓶颈所在并首先解决这些问题.

    【讨论】:

    • 我认为这个答案错过了问题的核心,问题是b-&gt;c-&gt;d 是否会对p 产生影响,其中p 是一个指向b-&gt;c-&gt;d 值的指针。在这两种情况下,我们都在处理指针,而不是值。问题是代码是在循环之前还是在每次迭代时显式地预先计算要使用的指针。话又说回来,答案已被接受,所以我可能误解了这个问题:)
    • David Rodriguez 是正确的,问题是关于串行的多跳指针,而不是直接访问单个指针。
    • 了解的唯一方法是查看汇编语言。可以推测,在循环之前将指针b, c, d 加载到寄存器变量中可能会提高性能。编译器可能已经在更高级别的优化上执行此操作。但是,性能增益是多少,纳秒、微秒、毫秒?当程序执行 I/O 或等待 GUI 事件时,所有这些都可以忽略不计。
    • 或者可以阅读原始问题。 b-&gt;c-&gt;d[i]f[i]。因此,您答案的答案部分与问题无关(因为它忽略了间接性),只有最后的警告是相关的,应该是评论而不是答案。
    【解决方案3】:

    您没有说明为什么要关注此问题,您是否已经进行了一些性能分析并专注于该例程,或者您是否正在进行“盲人优化” - 包括查看代码和说“也许这很慢”。

    让我先解决您的前期问题:

     a->b->c[i]
    

     f[i]
    

    如果你编译这两段代码没有优化那么很有可能f[i]会更快。

    一旦您启用优化,所有赌注都将关闭。首先,您使用的架构是未知的,因此a-&gt;b-&gt;c 中的顺序提取的成本是未知的,我们也不知道有多少寄存器可用,或者编译器可能使用哪些优化。可以想象,任何单次写入的成本都可能足够高,以至于如果 CPU 使用流水线,写入需要足够长的时间,以至于我们是否花一些时间在写入之间进行指针数学运算变得无关紧要。

    作为一个经验丰富的优化器,我对“value() 做了什么?”更感兴趣。编译器能否确定value() 不会修改a、a-> 或a->b->c 中的任何值?

    如果您绝对确定地知道这些值不会改变,已经完成性能分析并发现此循环是一个瓶颈,查看汇编器以确定编译器不会发出最有效的代码,那么您可能优化如下:

    int function_a(struct b* const b)
    {
        /// optimization: we found XYC compiler for Leg architecture was
        /// emitting instructions that repeatedly fetched the array base
        /// address every iteration.
        float* const end = f + 500;
        for (float* it = b->c->d; it < end; ++it)
            *it = value();
    }
    

    但是:进行这种低级优化是有风险的。如今的 C/C++ 优化器非常聪明。阻止他们生成尽可能高效的代码的一种方法是开始手动优化。

    我们在这里做了一个高效的紧密循环,但这可能不是在组装中实现结果的最有效方式。

    i = 0; i &lt; 500 的情况下,取决于value() 的实现,实际上可能会以保持内存总线繁忙的方式跨步或向量化循环,或者它可能使用特殊的宽寄存器来执行多个操作一次。我们的优化可能会创建一个病态的场景,我们迫使编译器发出效率最低的操作顺序。

    再说一次 - 我们不知道您专注于这部分代码的原因,但在实践中,我一直发现通过手动优化循环的这一部分您不太可能获得太多收益。

    如果您在 Linux 下进行开发,您可能需要查看 valgrind 以帮助您进行性能分析。如果您在 Visual Studio 下开发,则“分析”->“性能和诊断”(ctrl-alt-f9) 将显示性能向导。点击开始并选择“仪器”。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-09-18
      • 2011-02-14
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多