удаление constexpr из переменной, получающей возвращаемое значение функции constexpr, удаляет оценку во время компиляции - PullRequest
3 голосов
/ 20 марта 2019

Рассмотрим следующую функцию constexpr, static_strcmp, которая использует функцию C ++ 17 constexpr char_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 ]

4 голосов
/ 20 марта 2019

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

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), но результат идет туда, где значение не требуется во время компиляции (обычная переменная). Таким образом, компилятор может выбрать и, в вашем случае, выбрать вычисление во время выполнения с версией функции, вычисление во время компиляции с другой версией.

3 голосов
/ 20 марта 2019

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

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

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

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

2 голосов
/ 20 марта 2019

Ваша программа имеет неопределенное поведение, потому что вы всегда сравниваете 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 - индекс первой разности).

...