C++中公共子表达式消除的局限性

Limitations of Common Subexpression Elimination in C++

本文关键字:局限性 表达式 C++      更新时间:2023-10-16

我在看一个演讲,"算法的效率,性能数据结构"对以下评论感到惊讶:

#include <string>
#include <unordered_map>
#include <memory>
struct Foo {
  int x;
};
Foo* getFoo(std::string key,
            std::unordered_map<std::string,
                               std::unique_ptr<Foo>> &cache) {
  if (cache[key])
    return cache[key].get();
  cache[key] = std::unique_ptr<Foo>(new Foo());
  return cache[key].get();
}
Foo* getFooBetter(std::string key,
                  std::unordered_map<std::string,
                                     std::unique_ptr<Foo>> &cache) {
  std::unique_ptr<Foo> &entry = cache[key];
  if (entry)
    return entry.get();
  entry = std::unique_ptr<Foo>(new Foo());
  return entry.get();
}

CCD_ 1较好。我一直相信我可以依靠关于在我希望只评估多次出现的x+y一旦不出所料,生成的LLVM IR确实与节目主持人即使使用了-O9,我们在中也只剩下3个对cache[key]的调用CCD_ 4版本。

我已经将两者的长LLVM IR与c++符号分开,以免在视觉上冒犯。

另一个StackOverflow问题揭示了这里的部分答案是operator[]假设能够修改其希望的任何全局状态,以及因此我们不能取消呼叫。关于引入CCD_ 6注释介绍了它在CSE中的应用。

如果我们只打4个电话,我会在这里结束时感到满意。然而,如果我对IR的解读是正确的,那么看起来我们已经优化了getFoo()变成好像我们写的:

Foo* getFoo(std::string key,
            std::unordered_map<std::string,
                               std::unique_ptr<Foo>> &cache) {
  if (cache[key])
    return cache[key].get();
  std::unique_ptr<Foo> &entry = cache[key];
  entry = std::unique_ptr<Foo>(new Foo());
  return entry.get();
}

有人能解释一下clang对代码的看法吗它能够合并最后两个cache[key],但不是全部他们(我的本地clang是3.4。(

llvm中的CSE实现是在算术表达式上操作的。您可以在llvm/lib/Transforms/Scalar/EarlyCES.cpp 中看到llvm Common Subexpression Elimination源代码

我们在这里面临的案例是跨过程优化。

这个调用cache[key]原来是getFooBetter()0函数。因此,诸如内联之类的优化可能会根据[]函数的内联成本而起作用。Chandler提到了同样的问题,给定的哈希函数计算成本很高,内联被阻止,一个人最终会多次计算哈希函数!

在发生内联的情况下,首先计算-O3处的IR,cache[key],并且给定cachekey根本没有突变,这样的调用将优化为相同的SSA值。

cache[key].get()的情况下,我们通常会在cache[key]返回对象并使用get()中的getelementpointer获取字段值时写入IR。打开优化后,这个IR变成了我们之前计算的"cache[key]"的SSA值,元素从唯一指针的结构访问。

回到getFooBetter(),在最坏的情况下,如果编译器无法跨过程进行优化,cache[key]的出现次数越多,计算量就会越多,即使在O3时,这个调用也会原样出现!

在无序映射查找中会发生很多事情。有散列计算,搜索一个bin,如果它不在bin中,则添加到bin中,如果表现在太大,则可能增长表。这与比较代码中有两个"x+y"实例不同。您应该更惊讶的是,它居然发现其中两个调用可以合并。(我是。(

一般来说,我不会指望编译器发现两个函数调用可以共享,当性能很重要时,我会自己在源代码中消除常见的子表达式。在constexpr完全实现之前,我甚至不会想到它会发现sin(x(是一样的。