【问题标题】:Loop counter & pointers循环计数器和指针
【发布时间】:2011-09-14 06:28:30
【问题描述】:

我正在为图形编写一个小型的专用 C99 库,我经常得到以下形式的循环:

for(int i = 0; i < graph->nvertices; ++i) {
  // ...
}

我想知道这是否是一种好习惯,尤其是在紧密循环的情况下。起初,我认为编译器会足够聪明,只查看一次“graph->nvertices”,而不是在每次迭代时查看它,但这似乎是不可能的,因为 graph->nvertices 可能会在循环内发生变化。 Is 写起来更聪明更快捷:

const int N = graph->nvertices;
for(int i = 0; i < N; ++i) {
  // ...
}

它似乎更快,因为它不需要多次查看指针,但它确实需要创建一个新变量。

注意:我想在这种情况下,能够阅读一些汇编代码以了解编译器实际在做什么是件好事,如果有人有很好的参考,我愿意接受建议。

【问题讨论】:

  • 一般来说,我什至不在乎,只用少一行的那个。如果有问题的循环成为热点,添加它是微不足道的,事实证明这种微优化有显着帮助。
  • 首先,如果graph-&gt;nvertices在循环过程中发生变化会发生什么?第二个版本可能更快也可能不会更快,但它在语义上有所不同。
  • @David:没错,第二个版本实际上是声明上限不会改变。
  • 请注意,假设您实际上并没有在循环中对其进行修改,那么编译器是否会优化重读graph-nvertices主要取决于(1)是否有足够的寄存器来保持缓存无论如何,以及(2)您是否在循环中使用char 指针或与nvertices 相同类型的指针(可以别名)。

标签: c performance pointers loops


【解决方案1】:

尝试使用更高的优化设置,一些编译器应该能够为您优化。你也可以向后迭代,只用表达式初始化计数器:

for (int i = graph->nvertices; i >= 0; --i) 
  ..

但是,您将对缓存性能造成严重破坏。我认为您建议的方法是最直接的,这有助于编译器和下一个人阅读您的代码。

【讨论】:

  • 这是一个有趣的想法(并避免创建变量),但为什么向后迭代会“严重破坏”缓存性能?
  • 缓存在设计时考虑了数据局部性,并且通常假设前向访问。如果你向后走,你会比向前走更多的缓存未命中。不过这纯粹是凭直觉,所以请自行测试和确认。
  • 如果任何现代处理器上的缓存都无法处理向后迭代,我会感到惊讶。许多编译器会将前向计数循环优化为后向计数循环,因为它需要少一个寄存器(用于存储graph-&gt;nvertices),并且与零比较通常比与任意整数比较更快。
  • 另外,澄清为什么这可能导致缓存未命中:大多数处理器都有一些自动预取机制。如果您对内存进行顺序访问,预取器将尝试缓存它认为您将要命中的内存,从而隐藏强制缓存未命中的延迟。这实际上效果如何取决于预取器的智能程度。如果它不能处理向后迭代,那么是的,你会注意到一些缓存未命中。
【解决方案2】:

我倾向于自己进行这些优化。有时编译器可以推断出nvertices 在整个循环中不会改变,但是如果你调用了可能改变值的其他函数怎么办?编译器无法推断,也可能无法优化代码。

此外,最好的方法始终是分析您的代码以查看两种方法之间的比较。

【讨论】:

    【解决方案3】:

    我经常用这个。

    const int N = graph->nvertices;
    int i = 0;
    for(; i < N; ++i) {
      // ...
    }
    

    【讨论】:

    • 为什么要使用额外的一行(增益、可读性或性能或其他方面绝对为零)来初始化循环计数器将其泄漏到作用域那不应该需要它?
    • @deinan。大多数 C 程序并未使用 C99 的所有功能。
    • 但是 C99 是一个给定的问题,所以答案不应该只关注其他语言版本(在它产生影响的地方,例如这里)。 (就我个人而言,我认为即使在 C89 中也不要让初始化部分为空更易读。)
    • 我对 c 没有太多经验,使用某些语言会更快。
    • @viaria:解释型语言?
    【解决方案4】:

    答案可能取决于您的编译器,但大多数编译器都会为您生成一个汇编列表,您可以学习。只需确保列出发布版本即可。

    但是,如果我真的关心性能的每一点,我可能会按照您的建议创建单独的计数变量。

    最后,我怀疑它会产生显着的差异。

    【讨论】:

      【解决方案5】:

      您要求一种查看汇编代码的方法。为此,您可以像这样使用objdump 程序:

      objdump -d executable
      

      要过滤掉主函数,请使用:

      objdump -d executable | sed -n '/<main>/,/^$/p'
      

      【讨论】:

      • 通常有一个编译器来简单地输出程序集而不是对象/机器代码(-S 带有 GCC 和 Clang),这更方便并且可能提供更易于阅读的程序集(但不会我假设包括链接时优化)。
      【解决方案6】:

      这似乎是一件相当容易测试的事情,只需查看编译器的输出即可。整个程序优化通常会捕获这些低级优化。

      另一方面,即使速度略有提高,我通常也不会进行这些类型的优化,因为我发现第一种形式更易于阅读和维护。

      【讨论】:

        【解决方案7】:

        我会去本地化变量的范围。然后优化器会自己解决所有问题:

        for(size_t i = 0, n = graph->nvertices; i < n; ++i) {
          // ...
        }
        

        【讨论】:

        • 我不知道 optimezer 在做什么,但在每个循环中机器都必须弄清楚这部分 -> 图形 -> nvertices。我知道错了吗。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-10-08
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-05-20
        相关资源
        最近更新 更多