Удаление constexpr из переменной, получающей возвращаемое значение функции constexpr, удаляет оценку во время компиляции

Рассмотрим следующее constexpr функция, static_strcmp, который использует C++17 constexprchar_traits::compare функция:

#include <string>

constexpr bool static_strcmp(char const *a, char const *b) 
{
    return std::char_traits<char>::compare(a, b,
        std::char_traits<char>::length(a)) == 0;
}

int main() 
{
    constexpr const char *a = "abcdefghijklmnopqrstuvwxyz";
    constexpr const char *b = "abc";

    constexpr bool result = static_strcmp(a, b);

    return result;
}

Godbolt показывает, что это оценивается во время компиляции и оптимизируется до:

main:
    xor     eax, eax
    ret

Удалить constexpr от bool result :

Если мы удалим constexpr от constexpr bool result Теперь вызов больше не оптимизирован.

#include <string>

constexpr bool static_strcmp(char const *a, char const *b) 
{
    return std::char_traits<char>::compare(a, b,
        std::char_traits<char>::length(a)) == 0;
}

int main() 
{
    constexpr const char *a = "abcdefghijklmnopqrstuvwxyz";
    constexpr const char *b = "abc";

    bool result = static_strcmp(a, b);            // <-- note no constexpr

    return result;
}

Godbolt показывает, что теперь мы называем в memcmp:

.LC0:
    .string "abc"
.LC1:
    .string "abcdefghijklmnopqrstuvwxyz"
main:
    sub     rsp, 8
    mov     edx, 26
    mov     esi, OFFSET FLAT:.LC0
    mov     edi, OFFSET FLAT:.LC1
    call    memcmp
    test    eax, eax
    sete    al
    add     rsp, 8
    movzx   eax, al
    ret

Добавить короткое замыкание length проверять:

если мы сначала сравним char_traits::length для двух аргументов в static_strcmp перед звонком char_traits::compare без constexpr на bool result, вызов снова оптимизируется.

#include <string>

constexpr bool static_strcmp(char const *a, char const *b) 
{
    return 
        std::char_traits<char>::length(a) == std::char_traits<char>::length(b) 
        && std::char_traits<char>::compare(a, b, 
             std::char_traits<char>::length(a)) == 0;
}

int main() 
{
    constexpr const char *a = "abcdefghijklmnopqrstuvwxyz";
    constexpr const char *b = "abc";

    bool result = static_strcmp(a, b);            // <-- note still no constexpr!

    return result;
}

Godbolt показывает, что мы вернулись к оптимизации вызова:

main:
    xor     eax, eax
    ret
  • Почему удаление constexpr от первоначального звонка до static_strcmp вызвать постоянную оценку провал?
  • Ясно даже без constexpr Призыв к char_traits::length оценивается во время компиляции, так почему бы не использовать то же поведение без constexpr в первой версии static_strcmp?

3 ответа

Решение

Обратите внимание, что ничего в стандарте явно не требуется constexpr функция, вызываемая во время компиляции, см. 9.1.5.7 в последней версии:

Вызов функции constexpr дает тот же результат, что и вызов эквивалентной функции non-constexpr во всех отношениях, за исключением того, что (7.1) вызов функции constexpr может появляться в константном выражении, а (7.2) исключение копирования не выполняется в константное выражение ([class.copy.elision]).

(подчеркивает мой)

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

У нас есть три рабочих случая:

1) вычисленное значение требуется для инициализации constexpr значение или если строго требуется значение, известное во время компиляции (параметр шаблона не-типа, размер массива в стиле C, тест в static_assert()...)

2) constexpr функция использует значение, не известное во время компиляции (например: значения, полученные из стандартного ввода.

3) constexpr функция получает значения во время компиляции, но результат находится в месте, которое не требуется во время компиляции.

Если мы игнорируем правило "как будто", то имеем:

  • в случае (1) компилятор должен вычислить значение времени компиляции, потому что вычисленное значение требуется во время компиляции

  • в случае (2) компилятор должен вычислить значение времени выполнения, потому что невозможно вычислить его время компиляции

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

С исходным кодом

constexpr bool result = static_strcmp(a, b);

вы в случае (1): компилятор должен вычислить время компиляции, потому что result переменная объявлена constexpr,

Удаление constexpr,

bool result = static_strcmp(a, b); // no more constexpr

ваш код переводится в серую область (case (3)), где вычисления во время компиляции возможны, но не являются строго обязательными, поскольку входные значения известны как время компиляции (a а также b) но результат идет там, где значение не требуется во время компиляции (обычная переменная). Таким образом, компилятор может выбрать и, в вашем случае, выбрать вычисление во время выполнения с версией функции, вычисление во время компиляции с другой версией.

Ваша программа имеет неопределенное поведение, потому что вы всегда сравниваете strlen(a) персонажи. Строка b не так много персонажей.

Если вы измените свои строки на равную длину (чтобы ваша программа стала четкой), ваша программа будет оптимизирована, как вы ожидаете.

Так что это не пропущенная оптимизация. Компилятор оптимизирует вашу программу, но поскольку он содержит неопределенное поведение, он не оптимизирует его.


Обратите внимание, что независимо от того, является ли это поведение неопределенным или нет, не совсем понятно. Учитывая, что компилятор использует memcmp, он думает, что обе входные строки должны быть по крайней мере strlen(a) долго. Таким образом, согласно поведению компилятора, это неопределенное поведение.

Вот что говорит текущий проект стандарта о сравнении:

Возвращает: 0, если для каждого i в [0, n) X::eq(p[i],q[i]) true; иначе, отрицательное значение, если для некоторого j в [0, n) X::lt(p[j],q[j]) true и для каждого i в [0, j) X::eq(p[i],q[i]) true; еще положительное значение.

Теперь не указано, compare разрешено читать p[j+1..n) или же q[j+1..n) (где j это индекс первой разницы).

Другие вопросы по тегам