编译器优化能否消除在 for 循环的条件中反复调用的函数?

Can compiler optimization elminate a function repeatedly called in a for-loop's conditional?

本文关键字:条件 调用 函数 循环 优化 for 编译器      更新时间:2023-10-16

我正在阅读有关哈希函数的信息(我是一名中级CS学生),并遇到了这个:

int hash (const string & key, int tableSize) {
    int hasVal = 0;
    for (int i = 0; i < key.length(); i++)
        hashVal = 37 * hashVal + key[i];
    .....
    return hashVal;
}

我正在查看这段代码,并注意到如果在 for 循环中而不是每次执行此操作时都调用 key.length() 会更快:

int n = key.length();
for (int i = 0; i < n; i++)

我的问题是,由于这是稍微提高性能的明显方法,编译器会自动为我们执行此操作吗?我对编译器还不太了解,但我很好奇这个问题的答案。在编写代码以使用较少的操作时,人们经常指出我做的事情通常已经由编译器为我完成,所以我在浪费时间,但做诸如内联函数之类的事情。我关心这一点,因为我正在编写一个游戏,其中物理处理需要高效,这样事情就不会感觉笨拙。

简短的回答:它有时可以...

长答案:

如果编译器可以根据循环本身确定 key.length() 是一个"常量"值,那么它将能够优化调用。这反过来又取决于所用类的定义(在本例中为 string ,我们可以预期它是"写得很好"的)。它还依赖于编译器的理解,即循环不会以改变key.length()的方式更改key

要做到这一点的关键要素是函数是inline的(或者是一个模板函数,inline需要允许它多次包含在不同的编译单元中 - 或在同一源文件中可用),并且源代码位于编译单元包含的头文件中。

当然,C++标准中没有要求编译器这样做。每次调用函数完全符合标准。

只有当key.length()是一个纯函数时,才安全地将它移出循环 - 也就是说,一个没有副作用的函数。如果编译器可以检测到这种情况,那么我会说它很可能会执行优化。

不幸的是,与Fortran不同,C++没有标准的方式来将某些东西标记为纯净。如果函数是inline的(或可内联的),那么编译器就有了定义,并且可能可以自己解决,或者至少消除函数调用。

将成员函数标记为 const 可以保证它不会影响当前实例,但原则上没有理由key.length()不能更改某些全局变量,所以我怀疑这本身就足够了。

(有多种编译器特定的方法来声明函数纯 - 如GCC和兼容编译器(Clang,Intel)中的__attribute__((pure))。也许尝试一下这些,看看它们是否有任何不同?

这是一个非常依赖于编译器的答案。确定的唯一方法是生成汇编程序并戳一下它创建的内容(我建议学习如何使用编译器执行此操作并尝试一下,这很有启发性*)。

优化不是那么明显 - 编译器可以执行该优化,但前提是它确定.length()不会更改 - 即没有其他线程可以访问数据,并且它能够确定循环内的任何内容都不会改变.length() 后者很容易检查,因为它是一个常量字符串, 但前者则不然。话虽如此,我希望编译器假设它在中等优化级别上无处不在。

*双关语故意;对不起。

有两个地方可以进行优化。

  1. 更聪明的编译器会内省短函数,看看它们是否对if子句有影响。可能大多数编译器会inline调用,使其等效于您编写的内容。

  2. 在CPU级别,分支预测也将使其(大大)比更"随机"if更快。关于SO的最重要的问题之一展示了很好的硬核例子。

编辑:我之前假设声明方法 length() const 就足够了。但它太弱了。方法仍然可以在不更改对象状态的情况下抛出随机内容。但是,我 100% 确定,像我描述的那样,体面的编译器可以执行内省函数和方法。