ограничение глубины constexpr с помощью clang (fconstexpr-depth, похоже, не работает)

Есть ли способ настроить глубину создания экземпляра constexpr? Я использую -fconstexpr-depth = 4096 (используя clang / XCode).

Но по-прежнему не удается скомпилировать этот код с ошибкой: переменная Constexpr fib_1 должна быть инициализирована константным выражением. Код не работает независимо от того, установлена ​​ли опция -fconstexpr-depth = 4096 или нет.

Это ошибка с лязгом или ожидается, что это так? Примечание: это работает до тех пор, пока 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; 
}

person Sarang    schedule 05.07.2014    source источник
comment
@ Брайан: спасибо. Как-то не получилось правильно отформатировать.   -  person Sarang    schedule 06.07.2014
comment
Хм, GCC отлично справляется ...   -  person Kerrek SB    schedule 06.07.2014
comment
С другой стороны, Clang вполне устраивает традиционная метапрограмма в виде шаблона.   -  person Kerrek SB    schedule 06.07.2014
comment
@KerrekSB: да, это я заметил. Он действительно хорошо справляется с TMP.   -  person Sarang    schedule 06.07.2014


Ответы (3)


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 различает две ошибки: глубину рекурсии и «ограничение шага».


Лучшие результаты Google по запросу «clang maximum step limit» приводят к страницам о патче clang, реализующем эту функцию, включая реализацию параметра командной строки: -fconstexpr-steps. Дальнейший поиск этого параметра в Google указывает на отсутствие документации на уровне пользователя.

Так что нет документации о том, что clang считается «шагом» или сколько «шагов» clang требуется для fib(27). Мы могли бы просто установить это очень высоко, но я думаю, что это плохая идея. Вместо этого некоторые эксперименты показывают:

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

Это означает, что шаги (fib(n)) == шаги (fib(n-1)) + шаги (fib(n-2)) + 2. Немного вычислений показывает, что, согласно этому, fib(27) должно потребовать 1 271 242 шагов clang. Таким образом, компиляция с -fconstexpr-steps=1271242 должна позволить программе компилироваться, что действительно так и есть. Компиляция с -fconstexpr-steps=1271241 приводит к такой же ошибке, как и раньше, поэтому мы знаем, что у нас есть точный предел.

Альтернативный, менее точный метод включает в себя наблюдение из патча, что предел шага по умолчанию составляет 1 048 576 (2 20), что, очевидно, достаточно для fib(26). Интуитивно, удвоения этого должно быть достаточно, и из более раннего анализа мы знаем, что два миллиона - это достаточно. Жесткий предел был бы φ · шагов (fib(26)) ⌉ (что составляет ровно 1 271 242).


Также следует отметить, что эти результаты ясно показывают, что clang не выполняет никаких запоминаний оценки constexpr. GCC делает, но похоже, что это не реализовано в лязгах вообще. Хотя мемоизация увеличивает требования к памяти, иногда, как в данном случае, она может значительно сократить время, необходимое для оценки. Два вывода, которые я сделал из этого, заключаются в том, что написание кода constexpr, который требует мемоизации для хорошего времени компиляции, не является хорошей идеей для переносимого кода, и что clang можно улучшить с помощью поддержки мемоизации constexpr и параметра командной строки для его включения / отключения.

person bames53    schedule 06.07.2014
comment
Итак, шаги сами по себе являются последовательностью в стиле Фибоначчи? - person Ben Voigt; 06.07.2014
comment
@BenVoigt Да, я думаю, это немного забавно, но имеет смысл. (и вы можете видеть в скомпилированном примере, который я связал с тем, что я только что изменил копию исходной функции, чтобы вычислить количество шагов, вместо того, чтобы работать с закрытой формой или делать что-то аналогичное умному.) - person bames53; 06.07.2014
comment
@ bames53: спасибо за подробное объяснение. Думаю, я пропустил раскрытие семантической проблемы, о которой сообщалось. XCode дал очень хорошее объяснение того, как что-то пошло не так ... - person Sarang; 06.07.2014
comment
также кажется, что ограничение шага было добавлено как часть расширений C ++ 1y: llvm.org/klaus / clang / commit / - person Sarang; 06.07.2014
comment
@Sarang Да, C ++ 14 устранил многие ограничения для функций constexpr, поэтому я думаю, что clang добавил ограничение на шаг как более общее ограничение на использование ресурсов. Однако -fconstexpr-steps также доступен в режиме C ++ 11. - person bames53; 06.07.2014
comment
Некоторое время назад я смотрел реализацию в clang. Компилятор интерпретирует AST при вызове функции constexpr. Шаги увеличиваются каждый раз, когда оценивается узел. Это означает, что минимальное количество шагов для достижения успеха во многом зависит от тела функции и возможной оптимизации, применяемой к AST. Также было удивительно наблюдать, что не было кеша (по сравнению с рекурсивной версией шаблона, работающей в линейное время из-за кеширования типов). В любом случае, хорошая реализация Фибоначчи не требует рекурсии :) - person galop1n; 06.07.2014
comment
@ galop1n Кажется, это правда. Когда я переписываю fib_cxpr для использования блока if-else if-else вместо тернарного оператора, минимальное количество шагов для успешного выполнения увеличивается до 3056712. - person Saeed Baig; 27.05.2020

Вы также можете реорганизовать свой алгоритм Фибоначчи, чтобы включить явную мемоизацию, которая будет работать в 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

Это не отвечает напрямую на вопрос, но у меня, по-видимому, недостаточно репутации, чтобы добавить это в качестве комментария ...

person Chris Philip    schedule 30.03.2021

Не связано с ограничением глубины, но сильно связано с вычислением числа Фибоначчи.

Рекурсия, возможно, неправильный подход и не нужна.

Существует сверхбыстрое решение с минимальным объемом памяти.

Итак, мы могли бы использовать предварительное вычисление во время компиляции всех чисел Фибоначчи, которые соответствуют 64-битному значению.

Одним из важных свойств ряда Фибоначчи является то, что значения растут строго экспоненциально. Таким образом, все существующие встроенные целочисленные типы данных довольно быстро переполнятся.

С помощью формулы Бине вы можете вычислить, что 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<unsigned long long, ...> с соответствующими числами Фибоначчи.

Мы знаем, что можем сохранить максимум 93 значения. И поэтому мы создаем следующую функцию, которая будет вызывать вышеуказанное с целочисленной последовательностью 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<unsigned long long, 93> с именем 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

person Armin Montigny    schedule 17.04.2021