为什么编译器不优化集合元素上的空范围循环?

Why doesn't the compiler optimize an empty ranged-for loop over the elements of a set?

本文关键字:范围 循环 元素 编译器 优化 集合 为什么      更新时间:2023-10-16

在测试我的代码时,我注意到当空的范围for loop被删除与否时,执行时间显着增加。 通常,我会认为编译器会注意到 for 循环没有任何用途,因此会被忽略。作为编译器标志,我正在使用-O3(gcc 5.4(。我还用向量而不是集合对其进行了测试,这似乎有效,并且在两种情况下都提供了相同的执行时间。似乎迭代器的增量会花费所有额外的时间。

第一种情况,远程 for 循环仍然存在(慢(:

#include <iostream>
#include <set>
int main () {
    long result;
    std::set<double> results;
    for (int i = 2; i <= 10000; ++i) {
        results.insert(i);
        for (auto element : results) {
            // no operation
        }
    }
    std::cout << "Result: " << result << "n";
}

删除范围 for 循环的第二种情况(快速(:

#include <iostream>
#include <set>
int main () {
    long result;
    std::set<double> results;
    for (int i = 2; i <= 10000; ++i) {
        results.insert(i);
    }
    std::cout << "Result: " << result << "n";
}

在内部std::set迭代器使用某种指针链。这似乎是问题所在。

以下是与您的问题类似的最小设置:

struct S
{
    S* next;
};
void f (S* s) {
    while (s)
        s = s->next;
}

这不是复杂的集合实现或迭代器开销的问题,而只是优化器无法优化的指针链模式。

不过,我不知道优化器在这种模式下失败的确切原因。

另外,请注意,此变体已优化:

void f (S* s) {
    // Copy paste as many times as you wish the following two lines
    if(s)
        s = s->next;
}

编辑

正如@hvd所建议的,这可能与编译器无法证明循环不是无限的有关。如果我们像这样编写 OP 循环:

void f(std::set<double>& s)
{
    auto it = s.begin();
    for (size_t i = 0; i < s.size() && it != s.end(); ++i, ++it)
    {
        // Do nothing
    }
}

编译器优化了所有内容。

基于循环的范围并不像看起来那么简单。它在编译器内部转换为基于迭代器的循环,如果迭代器足够复杂,则标准甚至可能不允许编译器删除这些迭代器操作。

您可以使用 clang 优化报告。在启用save-optimization-record的情况下编译代码,因此优化报告将被转储到 main.opt.yaml

clang++ -std=c++11 main.cpp -O2 -fsave-optimization-record


您将看到循环存在几个问题:

Clang认为,在这个循环中修改了一个值。

- String: value that could not be identified as reduction is used outside the loop

此外,编译器无法计算循环迭代的次数。

- String: could not determine number of loop iterations

请注意,编译器成功地内联了beginendoperator++operator=

Range-for 是"语法糖",这意味着它所做的只是为可以用更详细的方式表达的东西提供速记符号。例如,range-for 转换为类似的东西。

for (Type obj : container) ->

auto endpos = container.end();
for ( auto iter=container.begin(); iter != endpos; ++iter)
{
     Type obj(*iter);
     // your code here
}

现在的问题是开始/结束/*iter/++iter/(obj = (是函数调用。为了优化它们,编译器需要知道它们没有副作用(对全局状态的更改(。编译器是否可以执行此操作是实现定义的,并且取决于容器类型。不过我能说的是,在大多数情况下,您不需要(obj =(函数,因此更喜欢

for (const auto& X: cont) 

或。。。

for (auto& X: cont)

自。。。

for (auto X : cont)

您可能会发现这足以简化它,以便启动优化。