Почему компиляторы больше не оптимизируют этот UB со строгим псевдонимом

Один из первых результатов для строгого сглаживания на google - это статья http://dbp-consulting.com/tutorials/StrictAliasing.html
Интересно, что я заметил следующее: http://goo.gl/lPtIa5

uint32_t swaphalves(uint32_t a) {
  uint32_t acopy = a;
  uint16_t* ptr = (uint16_t*)&acopy;
  uint16_t tmp = ptr[0];
  ptr[0] = ptr[1];
  ptr[1] = tmp;
  return acopy;
}

скомпилирован в

swaphalves(unsigned int):
        mov     eax, edi
        ret

по GCC 4.4.7. Любой компилятор, более новый, чем это (4.4 упоминается в статье, поэтому статья не ошибается) не реализует функцию, поскольку она может использовать строгий псевдоним. Что является причиной этого? Было ли это ошибкой в ​​GCC или GCC, решил отказаться от нее, так как многие строки кода были написаны так, что они производят UB или это просто регрессия компилятора, которая длится годами... Также Кланг не оптимизирует его.

Ответы

Ответ 1

Разработчики GCC приложили определенные усилия, чтобы заставить компилятор вести себя "как ожидалось" в этих случаях. (Хотелось бы, чтобы я мог дать вам правильную ссылку для этого - я помню, что это появилось в списке рассылки или в какой-то момент).

Во всяком случае, что-то вы говорите:

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

... подразумевает, возможно, небольшое недоразумение того, для чего предназначены правила строгого сглаживания. Ваш пример кода вызывает поведение undefined - поэтому любая компиляция технически достоверна, включая просто обычный ret или генерацию команды trap, или даже ничего (допустимо предположить, что метод никогда не может быть вызван). Более поздние версии GCC для получения более длинного/медленного кода вряд ли являются недостатком, поскольку код, который делает какую-либо конкретную вещь вообще, не будет нарушать стандарт. Фактически, более новые версии улучшают ситуацию, создавая код, который делает то, что программист, вероятно, намеревался сделать, вместо того, чтобы молча делать что-то другое.

Что бы вы предпочли - что компилятор создает быстрый код, который не делает то, что вам нужно, или немного медленнее, чем тот, который вам нужен?

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

Почему предыдущие версии GCC ведут себя так, как они делали, и "оптимизировали" эту функцию, полагаясь на соблюдение правил строгой алиасии?

Это немного сложно, но интересно для этого обсуждения (особенно в свете предложений о том, что компилятор собирается до некоторой длины просто сломать код). Строгое сглаживание является компонентом (или, скорее, правилом, которое помогает) процессом, называемым alias analysis. Этот процесс решает, являются ли два указателя псевдонимами или нет. Существуют, по существу, 3 возможных условия между любыми двумя указателями:

  • Они НЕ ДОЛЖНЫ АЛИА (строгое правило сглаживания позволяет легко вывести это условие, хотя иногда его можно вывести другими способами).
  • Они ДОЛЖНЫ ALIAS (для этого требуется анализ, например, распространение значения может обнаружить это условие)
  • Они МОГУТ АЛИАС. Это условие по умолчанию, когда ни одно из двух других условий не может быть установлено.

В случае кода в вашем вопросе строгое сглаживание подразумевает условие ДОЛЖНО НЕ АЛИАС между &acopy и ptr (тривиально сделать это определение, потому что два значения имеют несовместимые типы, которые не допускаются псевдоним). Это условие позволяет оптимизировать, что вы тогда видите: все манипуляции с значениями *ptr могут быть отброшены, потому что они не могут теоретически влиять на значение acopy, и они иначе не выходят из функции (что может быть определено с помощью эвакуационного анализа).

Требуется дополнительное усилие для определения условия MUST ALIAS между двумя указателями. Кроме того, при этом компилятору необходимо будет игнорировать (по крайней мере временно) ранее установленное условие НЕ ДОЛЖНО АЛИАС, а это означает, что он должен тратить время, пытаясь выяснить правду о состоянии, которое, если все так, как должно быть, должно быть ложь.

Когда определены условия как НЕ ДОЛЖНЫ АЛИАСЫ, так и ДОЛЖНЫ АЛИА, мы имеем случай, когда код должен вызывать поведение undefined (и мы можем выпустить предупреждение). Затем мы должны решить, какое условие сохранить и от чего отказаться. Поскольку MUST NOT ALIAS, в этом случае, исходит из ограничения, которое может быть (и действительно было) нарушено пользователем, это лучший вариант для отказа.

Таким образом, более старые версии GCC либо не выполняют необходимый анализ, чтобы определить условие MUST ALIAS (возможно, потому, что уже установлено условие MUST NOT ALIAS), или, альтернативно, более старая версия GCC выбирает отказ от MUST Условие ALIAS предпочтительнее условия MUST NOT ALIAS, что приводит к более быстрому коду, который не делает то, что, по-видимому, предполагал программист. В любом случае кажется, что новые версии предлагают улучшение.

Ответ 2

В этом другом связанном вопросе есть комментарий от @DanMoulding. Позвольте мне плагиатом:

Цель стандартных правил строжайшего сглаживания заключается в том, чтобы позволить компилятору оптимизироваться в ситуациях, где он отсутствует, и не может знать, является ли объект псевдонимом. Эти правила позволяют оптимизатору не делать предположения о наименее вероятном псевдониме в этих ситуациях. Однако, когда из контекста ясно, что объект сглажен, компилятор должен рассматривать объект как псевдониму, независимо от того, какие типы используются для доступа к нему. Выполнение в противном случае не соответствует требованиям правил псевдонимов языка.

В вашем коде сглаживание *ptr и acopy очевидно, так как оба являются локальными переменными, поэтому любой здравомыслящий компилятор должен относиться к ним как к псевдониму. С этой точки зрения поведение GCC 4.4, хотя и в соответствии со строгим чтением стандарта, будет считаться ошибкой большинства программистов реального мира.

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

void foo(int *idx, float *data)
{ /* idx and data do not overlap */ }

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

void foo()
{
    uint32_t x;
    uint16_t *p = (uint16_t *)&x; //x and p do overlap!
}

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

Существует много кода, который является технически UB, но игнорируется всеми компиляторами. Например, что бы вы подумали о компиляторе, который рассматривает это как пустой файл:

#ifndef _FOO_H_
#define _FOO_H_
void foo(void);
#endif

Или как насчет компилятора, который игнорирует этот макрос:

#define new DEBUG_NEW

просто потому, что стандарт позволяет это сделать?

Ответ 3

Цель компилятора, как правило, должна соответствовать целям кода как можно ближе. В этом случае код вызывает UB, но намерение должно быть довольно ясным. Я предполагаю, что в последнее время компиляторы сосредоточились на том, чтобы быть верными, чем использовать UB для целей оптимизации.

Строгое сглаживание - это, по сути, предположение, что код не пытается подорвать систему типов, что, как отмечает @rodrigo, дает компилятору больше информации, которую он может использовать для оптимизации. Если компилятор не может принять строгий псевдоним, он исключает ряд нетривиальных оптимизаций, поэтому C даже добавил квалификатор restrict (C99).

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

uint32_t wswap(uint32_t ws) {
  return (ws << 16) | (ws >> 16);
}

компилируется для...

wswap:                                  # @wswap
    .cfi_startproc
# BB#0:
    roll    $16, %edi
    movl    %edi, %eax
    retq