Почему включение очистки неопределенного поведения мешает оптимизации?

Рассмотрим следующий код:

#include <string_view>

constexpr std::string_view f() { return "hello"; }

static constexpr std::string_view g() {
    auto x = f();
    return x.substr(1, 3);
}

int foo() { return g().length(); }

Если я скомпилирую его с GCC 10.2 и флагами --std=c++17 -O1, я получу:

foo():
        mov     eax, 3
        ret

также, насколько мне известно, этот код не страдает от каких-либо проблем с неопределенным поведением.

Однако, если я добавлю флаг -fsanitize=undefined, результат компиляции будет таким:

.LC0:
        .string "hello"
foo():
        sub     rsp, 104
        mov     QWORD PTR [rsp+80], 5
        mov     QWORD PTR [rsp+16], 5
        mov     QWORD PTR [rsp+24], OFFSET FLAT:.LC0
        mov     QWORD PTR [rsp+8], 3
        mov     QWORD PTR [rsp+72], 4
        mov     eax, OFFSET FLAT:.LC0
        cmp     rax, -1
        jnb     .L4
.L2:
        mov     eax, 3
        add     rsp, 104
        ret
.L4:
        mov     edx, OFFSET FLAT:.LC0+1
        mov     rsi, rax
        mov     edi, OFFSET FLAT:.Lubsan_data154
        call    __ubsan_handle_pointer_overflow
        jmp     .L2
.LC1:
        .string "/opt/compiler-explorer/gcc-10.2.0/include/c++/10.2.0/string_view"
.Lubsan_data154:
        .quad   .LC1
        .long   287
        .long   49

См. это в Compiler Explorer.

Мой вопрос: почему дезинфекция должна мешать оптимизации? Тем более, что в коде, похоже, нет никаких опасностей UB...

Примечания:

  • Я подозреваю ошибку GCC, но, возможно, у меня неправильное представление о том, что делает UBsan.
  • Такое же поведение, если я установил -O3.
  • Без флагов оптимизации более длинный код создается как с очисткой, так и без нее.
  • Если вы объявите x переменной constexpr, очистка не помешает оптимизации.
  • Такое же поведение с C++17 и C++20.
  • С Clang вы также получите это несоответствие, но только с более высоким параметром оптимизации (например, -O3).

person einpoklum    schedule 23.10.2020    source источник
comment
Асан — это не то же самое, что УБсан. В любом случае, я думаю, это просто добавление инструментов, которые делают это, а компиляция либо не оптимизируется, либо не может оптимизироваться вокруг этого.   -  person underscore_d    schedule 23.10.2020
comment
Я предполагаю, что включение очистки отключило анализ потока данных. Таким образом, вместо того, чтобы определять, что не может быть никакого переполнения буфера во время компиляции, он генерирует код для проверки этого во время выполнения.   -  person Barmar    schedule 23.10.2020
comment
@Barmar: Но почему включение очистки должно отключать анализ потока данных? ... это мой вопрос, на самом деле.   -  person einpoklum    schedule 23.10.2020
comment
@underscore_d: я не говорил, что Асан и УБсан одинаковы.   -  person einpoklum    schedule 23.10.2020
comment
@einpoklum В вашем первоначальном заголовке говорилось о санации адреса, но вопрос касается санации неопределенного поведения.   -  person Barmar    schedule 23.10.2020
comment
Оптимизациям это не мешало: mov eax, 3 все еще там, и нет вызовов f или g. Он просто добавил инструментарий (который, по общему признанию, здесь не нужен).   -  person interjay    schedule 23.10.2020


Ответы (3)


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

Непоследовательное поведение, которое вы видите, связано с тем, что вызов g().length(); не выполняется в контексте constexpr, поэтому не требуется (ну, точнее, не ожидалось) вычисляться во время компиляции. GCC, вероятно, имеет некоторые эвристики для вычисления constexpr функций с constexpr аргументами в обычных контекстах, которые не срабатывают, когда в дело вступают дезинфицирующие средства, либо нарушая constexpr-ность функции (из-за добавленного инструментария), либо одну из задействованных эвристик.

Добавление constexpr к x приводит к тому, что f() вызывает постоянное выражение (даже если g() таковым не является), поэтому оно компилируется во время компиляции, поэтому его не нужно инструментировать, чего достаточно для срабатывания других оптимизаций.

Можно рассматривать это как проблему QoI, но в целом это имеет смысл, поскольку

  1. constexpr оценка функции может занимать сколь угодно много времени, поэтому не всегда предпочтительнее оценивать все во время компиляции, если только вас не попросят об этом.
  2. вы всегда можете форсировать такое вычисление (хотя стандарт в этом случае несколько разрешителен), используя такие функции в константных выражениях. Это также позаботится о любом UB для вас.
person Dan M.    schedule 23.10.2020

Тем более, что в коде, похоже, нет никаких опасностей UB.

f() возвращает std::string_view, который содержит длину и указатель. Вызов x.substr(1, 3) требует добавления единицы к этому указателю. Это технически может переполниться. Это потенциал УБ. Измените 1 на 0 и посмотрите, как исчезнет код UB.

Мы знаем, что [ptr, ptr+5] верны, поэтому вывод состоит в том, что gcc не может распространять это знание диапазона значений, несмотря на агрессивное встраивание и другие упрощения.

Я не могу найти непосредственно связанную ошибку gcc, но этот комментарий кажется интересным:

[VRP] делает невероятно плохую работу по отслеживанию диапазонов указателей, где он просто предпочитает отслеживать не-NULL.

person Jeff Garrett    schedule 23.10.2020

Дезинфицирующие средства неопределенного поведения не являются механизмом, работающим только во время компиляции (выделено не в исходном ; и цитата о clang, но она относится и к GCC):

UndefinedBehaviorSanitizer (UBSan) — это быстрый детектор неопределенного поведения. UBSan модифицирует программу во время компиляции, чтобы отлавливать различные виды неопределенного поведения во время выполнения программы.

Таким образом, вместо исходной программы на самом деле компилируется программа с некоторыми дополнительными инструментами, которые вы можете увидеть в более длинном скомпилированном коде, например:

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

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

person einpoklum    schedule 23.10.2020