【问题标题】:Limitations of Common Subexpression Elimination in C++C++ 中公共子表达式消除的局限性
【发布时间】:2015-08-11 19:45:34
【问题描述】:

我在看一个谈话,"Efficiency with Algorithms, Performance with Data Structures",然后 对以下评论感到惊讶:

#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();
}

getFooBetter() 更好。我一直相信我可以依靠 在编译器上以相同的方式执行这种转换 我希望只评估多次出现的x+y 一次。不出所料,生成的 LLVM IR 确实与 主持人。即使使用 -O9,我们仍然需要对 cache[key] 进行 3 次调用 getFoo() 版本。

我已将较长的 LLVM IR of both with c++ symbols unmangled 移到不符合要求的位置,以免造成视觉上的冒犯。

Another StackOverflow question 揭示了这里的部分答案是operator[] 假定能够修改它希望的任何全局状态,并且 因此我们不能忽略调用。 A linked proposal关于介绍一个 [[pure]] 注解讲述了它在 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]s,但不是全部 他们? (我本地的 clang 是 3.4。)

【问题讨论】:

  • 疯狂猜测(我不知道 Clang 内部原理):这可能是内联扩展后进一步优化的结果;在这种情况下,在扩展之后,编译器确实有足够的关于扩展片段的确切性质的信息,并且可以消除重复而不会有意外副作用的风险并且没有 [[pure]]。尽管如此,依赖编译器完成的任何类型的优化通常都是一个坏主意。

标签: c++ clang compiler-optimization


【解决方案1】:

llvm 中的 CSE 实现在算术表达式上运行。 llvm通用子表达式消除源码可以在llvm/lib/Transforms/Scalar/EarlyCSE.cpp中看到

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

这个调用 cache[key] 原来是 [](cache,key) 函数。因此,内联等优化可能会根据 [] 函数内联的成本起作用。 Chandler 提到了同样的问题,考虑到哈希函数的计算成本很高,因此阻止了内联,最终计算哈希函数不止一次!

如果发生内联,则首先计算位于 -O3、cache[key] 的 IR,并且鉴于 cache key 根本没有变异,这样的调用将被优化为相同的 SSA 值。

cache[key].get() 的情况下,我们通常会将IR 写为cache[key] 返回对象并使用get() 中的getelementpointer 获取字段的值。启用优化后,这个 IR 变成了我们之前计算的 'cache[key]` 的 SSA 值,其中元素访问来自唯一指针的结构。

回到getFooBetter(),在最坏的情况下,如果编译器无法跨过程进行优化,cache[key] 的更多出现将导致更多的计算,即使在 O3 上,这个调用也会按原样显示!

【讨论】:

    【解决方案2】:

    在 unordered_map 查找中有很多事情要做。有哈希计算,通过一个 bin 搜索,如果它不存在则添加到 bin,如果它现在太大,可能会增加表。这与比较代码中有两个“x+y”实例不同。您应该更惊讶的是,它实际上发现两个调用可以合并。 (我是。)

    作为一般规则,我不会指望编译器发现两个函数调用可以共享,并且当性能很重要时,我会自己在源代码中进行公共子表达式消除。在完全实现 constexpr 之前,我什至不会期望它会发现 sin(x) 是相同的。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-07-12
      • 2015-08-24
      • 1970-01-01
      • 2014-01-13
      相关资源
      最近更新 更多