【问题标题】:constexpr depth limit with clang (fconstexpr-depth doesnt seem to work)带有clang的constexpr深度限制(constexpr-depth似乎不起作用)
【发布时间】:2014-08-26 19:21:24
【问题描述】:

无论如何配置 constexpr 实例化深度? 我正在运行 -fconstexpr-depth=4096(使用 clang/XCode)。

但仍然无法编译此代码并出现错误: constexpr 变量 fib_1 必须由常量表达式初始化。 无论选项 -fconstexpr-depth=4096 是否设置,代码都会失败。

这是 clang 的错误还是预计会以这种方式运行。 注意:这在 fib_cxpr(26) 之前一直有效,27 是它开始失败的时候。

代码:

constexpr int fib_cxpr(int idx) {
    return idx == 0 ? 0 :
           idx == 1 ? 1 :
           fib_cxpr(idx-1) + fib_cxpr(idx-2); 
}

int main() {
    constexpr auto fib_1 = fib_cxpr(27);
    return 0; 
}

【问题讨论】:

  • @Brian:谢谢。不知何故,我无法正确格式化它。
  • 嗯,GCC 处理得很好……
  • 另一方面,Clang 对传统的模板式元程序非常满意。
  • @KerrekSB:是的,我注意到了。它确实很好地完成了 TMP。

标签: c++ c++11 clang constexpr c++14


【解决方案1】:

TL;DR:

对于clang,您需要命令行参数-fconstexpr-steps=1271242,并且不需要超过-fconstexpr-depth=27


计算斐波那契数的递归方法不需要太多的递归深度。 fib(n) 所需的深度实际上不超过n。这是因为最长的调用链是通过fib(i-1) 递归调用。

constexpr auto fib_1 = fib_cxpr(3); // fails with -fconstexpr-depth=2, works with -fconstexpr-depth=3
constexpr auto fib_1 = fib_cxpr(4); // fails with -fconstexpr-depth=3, works with -fconstexpr-depth=4

因此我们可以得出结论,-fconstexpr-depth 不是重要的设置。

此外,错误消息也表明存在差异:

constexpr auto fib_1 = fib_cxpr(27);

使用-fconstexpr-depth=26 编译,以确保我们达到该限制,clang 产生消息:

note: constexpr evaluation exceeded maximum depth of 26 calls

但是使用足够深度的-fconstexpr-depth=27 编译会产生消息:

note: constexpr evaluation hit maximum step limit; possible infinite loop?

所以我们知道 clang 区分了两种失败:递归深度和“步数限制”。


“clang 最大步数限制”的热门 Google 搜索结果指向有关实现此功能的 clang 补丁的页面,包括命令行选项的实现:-fconstexpr-steps。进一步谷歌搜索此选项表明没有用户级文档。

因此,对于 fib(27),clang 算作一个“步骤”或 clang 需要多少“步骤”没有任何文档。我们可以把这个设置得很高,但我认为这是个坏主意。相反,一些实验表明:

n : steps
0 : 2
1 : 2
2 : 6
3 : 10
4 : 18

这表示steps(fib(n)) == steps(fib(n-1)) + steps(fib(n-2)) + 2。稍微计算一下,根据这个fib(27)应该需要1,271,242个clang的steps .所以用-fconstexpr-steps=1271242 编译应该允许程序编译,这确实是it does。使用-fconstexpr-steps=1271241 编译会导致与以前相同的错误,因此我们知道我们有一个确切的限制。

另一种不太精确的方法是从补丁中观察到默认步数限制为 1,048,576 (220),这对于fib(26) 显然是足够的。直觉上,加倍应该足够了,从前面的分析中我们知道,200 万已经足够了。严格的限制是 ⌈φ·steps(fib(26))⌉(恰好是 1,271,242)。


要注意的另一件事是,这些结果清楚地表明,clang 没有对 constexpr 评估进行任何记忆。 GCC does,但似乎这根本没有在 clang 中实现。尽管记忆化增加了内存需求,但有时,就像在这种情况下一样,它可以大大减少评估所需的时间。我从中得出的两个结论是,编写需要记忆以获得良好编译时间的 constexpr 代码对于可移植代码来说不是一个好主意,并且可以通过支持 constexpr 记忆和启用/禁用它的命令行选项来改进 clang。

【讨论】:

  • 那么steps本身就是一个斐波那契式的序列?
  • @BenVoigt 是的,我想这有点好笑,但很有意义。 (您可以在我链接到的编译示例中看到,我只是修改了原始函数的副本来计算步数,而不是计算出封闭形式或做任何类似的聪明事情。)
  • @bames53:感谢您的详细解释。我想我错过了扩展报告的语义问题。 XCode 很好地解释了它是如何出错的..
  • 似乎步数限制是作为 C++1y 扩展的一部分添加的:llvm.org/klaus/clang/commit/…
  • 我前段时间看了一下clang中的实现。编译器在 constexpr 函数调用上解释 AST。每次评估节点时,步骤都会增加。这意味着成功的最小步数很大程度上取决于函数体和应用于 AST 的可能优化。观察到没有缓存也令人惊讶(与由于类型缓存而在线性时间运行的模板递归版本相比)。无论如何,一个好的斐波那契实现不需要递归:)
【解决方案2】:

您还可以重构您的 Fibonacci 算法以包括在 clang 中工作的显式记忆。

// Copyright 2021 Google LLC.
// SPDX-License-Identifier: Apache-2.0

#include <iostream>

template <int idx>
constexpr int fib_cxpr();

// This constexpr template value acts as the explicit memoization for the fib_cxpr function.
template <int i>
constexpr int kFib = fib_cxpr<i>();

// Arguments cannot be used in constexpr contexts (like the if constexpr),
// so idx is refactored as a template value argument instead.
template <int idx>
constexpr int fib_cxpr() {
    if constexpr (idx == 0 || idx == 1) {
        return idx;
    } else {
        return kFib<idx-1> + kFib<idx-2>;
    }      
}

int main() {
    constexpr auto fib_1 = fib_cxpr<27>();
    std::cout << fib_1 << "\n";
    return 0; 
}

此版本适用于 fib_cxpr 的任意输入,只需 4 个步骤即可编译。 https://godbolt.org/z/9cvz3hbaE

这不是直接回答问题,但我显然没有足够的声誉将其添加为评论...

【讨论】:

    【解决方案3】:

    与“深度限制”无关,但与斐波那契数计算密切相关。

    递归可能是错误的方法,不需要。

    有一种超快速的解决方案,内存占用少。

    因此,我们可以使用编译时预计算适合 64 位值的所有斐波那契数。

    斐波那契数列的一个重要特性是值呈指数级增长。因此,所有现有的整数数据类型构建都会很快溢出。

    使用Binet's formula,您可以计算出第 93 个斐波那契数是最后一个适合 64 位无符号值的数。

    在编译期间计算 93 个值是一项非常简单的任务。

    我们首先将计算斐波那契数的默认方法定义为constexpr 函数:

    // Constexpr function to calculate the nth Fibonacci number
    constexpr unsigned long long getFibonacciNumber(size_t index) noexcept {
        // Initialize first two even numbers 
        unsigned long long f1{ 0 }, f2{ 1 };
    
        // calculating Fibonacci value 
        while (index--) {
            // get next value of Fibonacci sequence 
            unsigned long long f3 = f2 + f1;
            // Move to next number
            f1 = f2;
            f2 = f3;
        }
        return f2;
    }
    

    这样,斐波那契数可以在编译时轻松计算。然后,我们用所有斐波那契数填充std::array。我们还使用了constexpr,并使其成为带有可变参数包的模板。

    我们使用std::integer_sequence 为索引 0、1、2、3、4、5、...创建一个斐波那契数。

    这很简单,并不复杂:

    template <size_t... ManyIndices>
    constexpr auto generateArrayHelper(std::integer_sequence<size_t, ManyIndices...>) noexcept {
        return std::array<unsigned long long, sizeof...(ManyIndices)>{ { getFibonacciNumber(ManyIndices)... } };
    };
    

    这个函数将输入一个整数序列 0,1,2,3,4,... 并返回一个带有相应斐波那契数的 std::array&lt;unsigned long long, ...&gt;

    我们知道我们最多可以存储 93 个值。因此我们创建了一个 next 函数,它将使用整数序列 1,2,3,4,...,92,93 调用上述函数,如下所示:

    constexpr auto generateArray() noexcept {
        return generateArrayHelper(std::make_integer_sequence<size_t, MaxIndexFor64BitValue>());
    }
    

    现在,终于,

    constexpr auto FIB = generateArray();
    

    将给我们一个编译时std::array&lt;unsigned long long, 93&gt;,名称为 FIB,包含所有斐波那契数。如果我们需要第 i 个斐波那契数,那么我们可以简单地写成FIB[i]。运行时不会进行计算。

    我认为没有更快的方法来计算第 n 个斐波那契数。

    请看下面的完整程序:

    #include <iostream>
    #include <array>
    #include <utility>
    // ----------------------------------------------------------------------
    // All the following will be done during compile time
    
    // Constexpr function to calculate the nth Fibonacci number
    constexpr unsigned long long getFibonacciNumber(size_t index) {
        // Initialize first two even numbers 
        unsigned long long f1{ 0 }, f2{ 1 };
    
        // calculating Fibonacci value 
        while (index--) {
            // get next value of Fibonacci sequence 
            unsigned long long f3 = f2 + f1;
            // Move to next number
            f1 = f2;
            f2 = f3;
        }
        return f2;
    }
    // We will automatically build an array of Fibonacci numberscompile time
    // Generate a std::array with n elements 
    template <size_t... ManyIndices>
    constexpr auto generateArrayHelper(std::integer_sequence<size_t, ManyIndices...>) noexcept {
        return std::array<unsigned long long, sizeof...(ManyIndices)>{ { getFibonacciNumber(ManyIndices)... } };
    };
    
    // Max index for Fibonaccis that for in an 64bit unsigned value (Binets formula)
    constexpr size_t MaxIndexFor64BitValue = 93;
    
    // Generate the required number of elements
    constexpr auto generateArray()noexcept {
        return generateArrayHelper(std::make_integer_sequence<size_t, MaxIndexFor64BitValue>());
    }
    
    // This is an constexpr array of all Fibonacci numbers
    constexpr auto FIB = generateArray();
    // ----------------------------------------------------------------------
    
    // Test
    int main() {
    
        // Print all possible Fibonacci numbers
        for (size_t i{}; i < MaxIndexFor64BitValue; ++i)
    
            std::cout << i << "\t--> " << FIB[i] << '\n';
    
        return 0;
    }
    

    使用 Microsoft Visual Studio Community 2019 版本 16.8.2 开发和测试。

    使用 clang11.0 和 gcc10.2 额外编译和测试

    语言:C++17

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2015-07-04
      • 1970-01-01
      • 2016-07-29
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-06-07
      • 1970-01-01
      相关资源
      最近更新 更多