Обнаружение времени компиляции или выполнения в функции constexpr

Я был взволнован, когда constexpr был представлен в C ++ 11, но, к сожалению, я сделал оптимистические предположения о его полезности. Я предположил, что мы можем использовать constexpr где угодно, чтобы поймать буквальные константы времени компиляции или любой результат constexpr буквальной константы времени компиляции, включая что-то вроде этого:

constexpr float MyMin(constexpr float a, constexpr float b) { return a<b?a:b; }

Поскольку определение типа возвращаемого значения функции только как constexpr не ограничивает его использование во время компиляции, а также должно быть вызываемым во время выполнения, я решил, что это будет способ гарантировать, что MyMin может когда-либо использоваться только с константами, оцениваемыми во время компиляции. , и это гарантировало бы, что компилятор никогда не разрешит его выполнение во время выполнения, давая мне возможность написать альтернативную, более удобную для времени выполнения версию MyMin, в идеале с тем же именем, которое использует внутренний _mm_min_ss, гарантируя, что компилятор не будет генерировать ветвление во время выполнения код. К сожалению, параметры функции не могут быть constexpr, поэтому может показаться, что это невозможно сделать, если только что-то подобное не было возможно:

constexpr float MyMin(float a, float b)
{
#if __IS_COMPILE_TIME__
    return a<b?a:b;
#else
    return _mm_cvtss_f32(_mm_min_ss(_mm_set_ss(a),_mm_set_ss(b)));
#endif
}

Я серьезно сомневаюсь, что в MSVC ++ есть что-то подобное, но я надеялся, что, возможно, GCC или clang найдут хоть что-то для этого, как бы неэлегантно это ни выглядело.

Конечно, представленный мною пример был очень упрощенным, но если вы можете использовать свое воображение, есть много случаев, когда вы можете свободно делать что-то вроде широкого использования операторов ветвления в функции, которая, как вы знаете, может выполняться только во время компиляции, потому что, если он выполняется во время выполнения, производительность будет скомпрометирована.


person Kumputer    schedule 03.11.2016    source источник
comment
Я широко использую шаблоны для удаления ветвей внутри циклов. Представьте, что у вас есть ветвь внутри цикла, вы можете удалить это, добавив целочисленный (= enum) аргумент шаблона к рассматриваемой функции. Это возможно и было возможно с помощью MSVC в течение многих лет.   -  person Jens Munk    schedule 03.11.2016
comment
@JensMunk И как это связано с вопросом?   -  person plasmacel    schedule 03.11.2016
comment
@plasmacel Это связано с вопросом, предоставляя возможное решение проблемы, описанной OP.   -  person    schedule 03.11.2016
comment
Это очень похоже. Вы можете использовать макрос, чтобы решить, следует ли интерпретировать аргумент как шаблон или как обычный аргумент. Можно использовать для тестирования, включения / выключения оптимизации. Та же проблема   -  person Jens Munk    schedule 03.11.2016
comment
@JensMunk - не могли бы вы расширить свой комментарий в ответе (рабочим примером)?   -  person max66    schedule 03.11.2016
comment
Насколько я понимаю вопрос, OP хочет определить, оценивается ли функция constexpr во время компиляции или во время выполнения. С другой стороны, a < b ? a : b вообще не выполняет ветвления, компилятор сгенерирует команду условного перемещения.   -  person plasmacel    schedule 03.11.2016
comment
@plasmacel, к сожалению, MSVC ++ будет ветвиться с аргументами с плавающей запятой. Clang умнее и выдаст инструкцию minss. И cmov работает только с целыми числами.   -  person Kumputer    schedule 03.11.2016
comment
@plasmacel Я ожидал, что OP хочет убедиться, что что-то оценивается во время компиляции, и этого легко добиться с помощью шаблонов, которые поддерживаются с помощью MSVC ++. Виноват :-)   -  person Jens Munk    schedule 03.11.2016
comment
@Kumputer Для аргументов с плавающей запятой компилятор (ориентированный на AVX и выше) реализует условное перемещение с использованием vcmpeqss и vblendvps без какого-либо ветвления. Для целей до AVX он будет реализовывать то же самое, используя cmpeqss andps andnps orps   -  person plasmacel    schedule 03.11.2016
comment
Этот старый вопрос похож. Пока я пишу это, у него нет хорошего ответа, но, возможно, один из этих вопросов следует пометить как повторяющийся   -  person Aaron McDaid    schedule 04.11.2016


Ответы (2)


Можно определить, является ли данное выражение вызова функции постоянным выражением, и, таким образом, выбрать между двумя различными реализациями. Требуется C ++ 14 для общей лямбды, используемой ниже.

(Этот ответ вырос из ответа @Yakk на вопрос, который я задал в прошлом году).

Я не уверен, насколько далеко я продвигаю Стандарт. Это проверено на clang 3.9, но приводит к тому, что g ++ 6.2 выдает «внутреннюю ошибку компилятора». Я пришлю отчет об ошибке на следующей неделе (если никто не сделает это первым!)

Этот первый шаг - переместить реализацию constexpr в struct как метод constexpr static. Проще говоря, вы можете оставить текущий constexpr как есть и вызвать его из метода constexpr static нового struct.

struct StaticStruct {
    static constexpr float MyMin_constexpr (float a, float b) {
        return a<b?a:b;
    }
};

Также определите это (даже если это выглядит бесполезным!):

template<int>
using Void = void;

Основная идея состоит в том, что Void<i> требует, чтобы i было постоянным выражением. Точнее, эта следующая лямбда будет иметь подходящие перегрузки только при определенных обстоятельствах:

auto l = [](auto ty)-> Void<(decltype(ty)::   MyMin_constexpr(1,3)   ,0)>{};
                                              \------------------/
                                               testing if this
                                               expression is a
                                               constant expression.

Мы можем вызвать l, только если аргумент ty имеет тип StaticStruct и, если наше выражение интереса (MyMin_constexpr(1,3)) является постоянным выражением. Если мы заменим 1 или 3 непостоянными аргументами, то общая лямбда l потеряет метод через SFINAE.

Следовательно, следующие два теста эквивалентны:

  • StaticStruct::MyMin_constexpr(1,3) - это постоянное выражение?
  • Можно ли вызвать l через l(StaticStruct{})?

Заманчиво просто удалить auto ty и decltype(ty) из приведенной выше лямбды. Но это приведет к серьезной ошибке (в непостоянном случае) вместо хорошей ошибки подстановки. Поэтому мы используем auto ty, чтобы получить ошибку замены (которую мы можем с пользой обнаружить) вместо ошибки.

Следующий код просто возвращает std:true_type тогда и только тогда, когда f (наша общая лямбда) может быть вызвана с a (StaticStruct):

template<typename F,typename A>
constexpr
auto
is_a_constant_expression(F&& f, A&& a)
    -> decltype( ( std::forward<F>(f)(std::forward<A>(a)) , std::true_type{} ) )
{ return {}; }
constexpr
std::false_type is_a_constant_expression(...)
{ return {}; }

Далее демонстрация его использования:

int main() {
    {
        auto should_be_true = is_a_constant_expression(
            [](auto ty)-> Void<(decltype(ty)::   MyMin_constexpr(1,3)   ,0)>{}
            , StaticStruct{});
        static_assert( should_be_true ,"");
    }
    {   
        float f = 3; // non-constexpr
        auto should_be_false = is_a_constant_expression(
            [](auto ty)-> Void<(decltype(ty)::   MyMin_constexpr(1,f)   ,0)>{}
            , StaticStruct{});
        static_assert(!should_be_false ,"");
    }
}

Чтобы решить вашу исходную проблему напрямую, мы могли бы сначала определить макрос для сохранения повторений:

(Я не тестировал этот макрос, извиняюсь за опечатки.)

#define IS_A_CONSTANT_EXPRESSION( EXPR )                \
     is_a_constant_expression(                          \
         [](auto ty)-> Void<(decltype(ty)::             \
              EXPR                         ,0)>{}       \
         , StaticStruct{})

На этом этапе, возможно, вы могли бы просто сделать:

#define MY_MIN(...)                                            \
    IS_A_CONSTANT_EXPRESSION( MyMin_constexpr(__VA_ARGS__) ) ? \
        StaticStruct :: MyMin_constexpr( __VA_ARGS__ )     :   \
                        MyMin_runtime  ( __VA_ARGS__ )

или, если вы не доверяете своему компилятору оптимизацию с std::true_type и с std::false_type по ?:, то, возможно,:

constexpr
float MyMin(std::true_type, float a, float b) { // called if it is a constant expression
    return StaticStruct:: MyMin_constexpr(a,b);
}
float MyMin(std::false_type, float , float ) { // called if NOT a constant expression
    return                MyMin_runtime(a,b);
}

с этим макросом вместо этого:

#define MY_MIN(...)                                             \
  MyMin( IS_A_CONSTANT_EXPRESSION(MyMin_constexpr(__VA_ARGS__)) \
       , __VA_ARGS__)
person Aaron McDaid    schedule 03.11.2016
comment
Мне действительно пора идти. Надеюсь, этот ответ поможет. Не стесняйтесь вносить правки в этот вопрос, я, вероятно, не буду в сети до понедельника. - person Aaron McDaid; 04.11.2016
comment
Я сообщил об этом как об ошибке в gcc 6.2.0. У меня не было возможности проверить это в каком-либо более позднем gcc, извиняюсь, если это уже было исправлено - person Aaron McDaid; 13.11.2016
comment
Предостережение: std::cout << MY_MIN(1, 3) будет использовать версию константного выражения, тогда как он вызывается в непостоянном выражении (и поэтому может быть вычислен только во время выполнения (хотя оптимизатор может работать)). - person Jarod42; 22.08.2020

Я подумал, что это будет способ гарантировать, что MyMin может когда-либо использоваться только с константами, оцениваемыми во время компиляции, и это гарантирует, что компилятор никогда не разрешит его выполнение во время выполнения.

Да; есть выход.

И работает с C ++ 11.

Google-ing Я нашел странный способ отравления (Скотт Шурр ): короче, следующие

extern int no_symbol;

constexpr float MyMin (float a, float b)
 {
   return a != a ? throw (no_symbol)
                 : (a < b ? a : b) ;
 }

int main()
 {
   constexpr  float  m0 { MyMin(2.0f, 3.0f) }; // OK

   float  f1 { 2.0f };

   float  m1 { MyMin(f1, 3.0f) };  // linker error: undefined "no_symbol"
 }

Если я хорошо понимаю, идея заключается в том, что если MyMin() выполняется во время компиляции, throw(no_symbol) никогда не используется (a != a всегда ложно), поэтому нет необходимости использовать no_symbol, который объявлен extern, но никогда не определяется (и throw() не может использоваться время компиляции).

Если вы используете MyMin() время выполнения, throw(no_symbol) компилируется и no_symbol выдает ошибку на этапе компоновки.

В более общем плане существует предложение ( когда-либо от Скотта Шурра), но я не знаю о реализациях.

--- ИЗМЕНИТЬ ---

Как указал T.C. (спасибо!) это решение работает (если работает и когда работает) только потому, что компилятор не выполняет оптимизацию, чтобы понять, что a != a всегда ложно.

В частности, MyMin() работает (без хорошей оптимизации), потому что в этом примере мы работаем с числами с плавающей запятой и a != a может быть истинным, если a равно NaN, поэтому компилятору сложнее определить, что часть throw() не используется. Если MyMin() - функция для целых чисел, тело может быть записано (с проверкой float(a) != float(a), чтобы попытаться помешать оптимизации компилятора) как

constexpr int MyMin (int a, int b)
 {
   return float(a) != float(a) ? throw (no_symbol)
                 : (a < b ? a : b) ;
 }

но это не настоящее решение для функции, в которой нет "естественных" ошибок, которые могут быть выброшены.

Когда это естественный случай ошибки, который должен выдать ошибку (компиляция или выполнение), дело обстоит иначе: компилятор не может оптимизировать, и уловка сработает.

Пример: если MyMin() возвращает минимальное значение между a и b, но a и b должны быть разными, или MyMin() должен выдавать ошибку компилятора (не лучший пример ... я знаю), поэтому

constexpr float MyMin (float a, float b)
 {
   return a != b ? throw (no_symbol)
                 : (a < b ? a : b) ;
 }

работает, потому что компилятор не может оптимизировать a != b и должен компилировать (с ошибкой компоновщика) часть throw().

person max66    schedule 03.11.2016
comment
Это лишь частичное решение, но все же умное. - person plasmacel; 03.11.2016
comment
@plasmacel - согласен; к сожалению, это не мое решение :(; Я только нашел. - person max66; 03.11.2016
comment
Я не совсем понимаю, почему это работает: почему a != a верно во время выполнения? - person plasmacel; 03.11.2016
comment
Я не могу сгенерировать ошибку компоновщика, используя этот код. Похоже, что компиляторы хорошо справляются с этой уловкой. Пробовал на gcc6.2, clang3.9 и ideone - person Aaron McDaid; 03.11.2016
comment
Оптимизатор увидит ваш тривиальный пример, и a != a не оптимизируется только из-за NaN. Это работает, только если -ffast-math и т. Д. Не включены и значение a не видно оптимизатору. - person T.C.; 04.11.2016
comment
@plasmacel - a != a не true во время выполнения (но это правда, если a равно Nan, если я хорошо понимаю комментарий T.C.), но компилятор (если оптимизацию избежать) должен скомпилировать и связать часть throw(). - person max66; 04.11.2016
comment
@AaronMcDaid - я полагаю, это проблема параметров компилятора; с Wandbox я когда-либо получал ошибку. - person max66; 04.11.2016
comment
@ T.C. - что вы думаете о float(a) != float(a), когда a является целым числом? Должно работать (без -ffast-math)? - person max66; 04.11.2016
comment
Трюк, который работает только без оптимизации, в большинстве случаев бесполезен, и весь этот прием зависит от нарушения ODR, поэтому программа, использующая его, формально бессмысленна. @ max66 Достаточно умный оптимизатор может заметить, что это приведение никогда не может дать NaN. - person T.C.; 04.11.2016
comment
@ T.C. - К сожалению, ты прав. Предложения? Я имею в виду: знаете ли вы, как наложить компиляцию части throw() (в случае отсутствия contstexpress)? - person max66; 04.11.2016
comment
@ T.C. - Я отредактировал свой ответ, пытаясь сообщить о ваших возражениях. - person max66; 04.11.2016