【问题标题】:gcc complex constant foldinggcc 复杂常量折叠
【发布时间】:2013-10-07 20:34:30
【问题描述】:

gcc 似乎对复杂的常量折叠有一些限制。这是一个例子:

static inline unsigned int DJBHash(const char *str)
{
   int i;
   unsigned int hash = 5381;

   for(i = 0; i < strlen(str); i++)
   {
      hash = ((hash << 5) + hash) + str[i];   
   }

   return hash;
}

int f(void)
{   
    return DJBHash("01234567890123456");
}

当以 -O3 优化级别 (gcc 4.8) 运行时,它会在 DJBHash 中很好地展开循环,并在编译期间计算该字符串的哈希值。

但是,当使字符串长一个字符return DJBHash("012345678901234567"); 时,它不再折叠它并生成一个带有条件跳转指令的循环。

我想将任意长度的文字字符串折叠为其哈希值作为编译时间常数。
这个可以吗?

澄清

我的问题是关于 gcc 的常量折叠优化(请参阅标题 - 请不要删除 gcccompiler 标签) 这里的许多答案都尝试使用模板或 constexpr 来解决问题。很高兴了解这些选项,并感谢您发布它们以造福所有人。但是,他们没有直接回答我的问题。

实际上,我正在开发一个 gcc 端口,因此如果需要,我可以更改和构建 gcc 源代码。但我仅限于 C,我想在这个范围内解决这个问题。

【问题讨论】:

  • GCC 中可能有一个选项可以让您设置一些阈值,它将决定展开某些内容。不断展开是一个可能无穷无尽的过程。此外,决定展开是否会终止可能是暂停问题。
  • @Mysticial 我查看了optimization command line options,但找不到与常量折叠相关的任何内容。我尝试更改循环展开参数max-unroll-times,但这似乎没有任何效果。
  • 那么您可能无能为力。您只能依靠编译器来做很多事情。这绝对是在突破界限。在某些时候,你需要明确。也许 C++ TMP 或许能够做到。不过我不确定。
  • 在 C++ 中,您可以使用 constexpr在编译时计算它
  • 顺便说一句,优化器可以将33 * hash转换为自己的移位和添加。

标签: c gcc compiler-construction compiler-optimization constantfolding


【解决方案1】:

这是使用constexpr 的版本。它在一个方面与其他的略有不同——因为是递归的,可以这么说,最容易将字符串哈希回前面。例如,它为“abc”提供的值将是您通常期望从“cba”获得的值。我认为这不会对使用产生任何真正的影响,只要您始终使用其中一种(但考虑到散列的变幻莫测,我可能是错的)。

它确实在编译时进行评估——例如,我们可以将结果用作switch 语句中的标签:

#include <iostream>

unsigned constexpr const_hash(char const *input) {
    return *input ?
           static_cast<unsigned>(*input) + 33 * const_hash(input + 1) :
           5381;
}

int main(int argc, char **argv) {
    switch (const_hash(argv[1])) {
    case const_hash("one"): std::cout << "one"; break;
    case const_hash("two"): std::cout << "two"; break;
    }
}

显然,可能会发生冲突,因此您通常不希望将其用作 case 语句标签——我这样做主要是为了强制一种情况,如果结果不是编译,它将无法编译——时间常数。

编辑:如果您关心哈希算法是否“正确”,我想这更准确(感谢@Abyx):

unsigned constexpr const_hash(char const *input, unsigned hash = 5381) {
    return *input ?
        const_hash(input + 1, hash * 33 + static_cast<unsigned>(*input)): 
        hash;
}

【讨论】:

  • 您可以使用尾递归辅助函数获得正确的结果,请参阅我的答案。
  • @FredOverflow:是的,你可以,但我不确定这是否足以证明额外工作/代码的合理性。
  • 为什么会有第一个static_cast
  • 尾递归版本不需要辅助函数。它看起来与@AdamBurry 的DJBHash2 完全一样,但hash 应该具有默认值= 5381。这个版本更好是有原因的。当您在运行时使用const_hash 时,它会运行得更快并且不会吃堆栈。 link
  • 赞成。请注意,此代码的保质期很短:一旦 C++14 constexpr(已在 Clang 3.4 SVN 中可用)变得更广泛可用,您就可以使用我的答案。
【解决方案2】:

OP 对 C 中的常量折叠感兴趣,但仅针对其 C++ 兄弟:在 C++14 中,您可以简单地将 constexpr 放在两个函数前面,并修改循环以补偿 @987654323 @不是constexpr

#include<iostream>

static inline constexpr unsigned int DJBHash(const char *str)
{
   unsigned int hash = 5381;

   for(auto i = 0; i < 512; ++i) {
      if (*str == '\0') return hash;
      hash = ((hash << 5) + hash) + static_cast<unsigned int>(*str);   
   }

   return hash;
}

constexpr unsigned int f(void)
{   
    return DJBHash("01234567890123456");
}

int main()
{
    constexpr auto h = f(); 
    std::cout << std::hex << h << "\n"; // 88a7b505
}

Live Example 使用带有-std=c++1y 的 Clang 3.4 SVN。

注意:当前的 Clang 实现无法使用 while(*str != '\0') 正确运行。相反,内部带有返回条件的 512 的有限循环可以完成这项工作。

【讨论】:

  • 在 constexpr 的情况下,Clang 如何计算最终值?通过直接解释 AST 或 jit 编译函数?
  • @Stringer 前者,该语言保证h 在启动程序之前被评估
  • 对不起,内联在 constexpr 上是多余的,因为它隐含地暗示了这一点。
【解决方案3】:

也许 C++ TMP 或许可以做到。不过我不确定。

如果您不介意使用可变字符文字列表而不是字符串文字,这是可能的:

#include <type_traits>
#include <iostream>

template<unsigned acc, char... values>
struct DJBhash_helper
     : std::integral_constant<unsigned, acc> {};

template<unsigned acc, char head, char... tail>
struct DJBhash_helper<acc, head, tail...>
     : DJBhash_helper<(acc << 5) + acc + head, tail...> {};

template<char... str>
struct DJBhash
     : DJBhash_helper<5381, str...> {};

int main()
{
    std::cout << DJBhash<'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
                         '0', '1', '2', '3', '4', '5', '6', '7'>::value << '\n';
}

ideone live demo

【讨论】:

  • 你可以使用boost::mpl::string
【解决方案4】:

不是答案,只是另一个数据点。

下面的实现更糟糕。 GCC 4.7.3 正确地应用了 TCO 来将此实现变成一个循环,但它在编译时只能评估为“0”!

static inline unsigned int DJBHash2(const char *str, unsigned int hash) {
   return *str ? DJBHash2(str + 1, 33 * hash + *str) : hash; }

从好的方面来说,递归版本短了 7 个字节。

其他人提到了 clang,所以这里是 clang 3.1 -O3 的结果。它为两个版本的 DJBHash 生成不同的代码,但它们的字节数相同。有趣的是,它将原始版本的移位和加法转换为乘法。它将两个版本都优化为最多 100 个字符的字符串的常量。最后,clang 代码比最短的 GCC 代码短 5 个字节。

【讨论】:

    猜你喜欢
    • 2018-02-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-05-08
    • 2020-03-19
    • 1970-01-01
    • 2012-08-14
    • 1970-01-01
    相关资源
    最近更新 更多