Возможная ошибка GCC при возврате структуры из функции

133

Я полагаю, что обнаружил ошибку в GCC при реализации PCG PRNG О'Нила. ( Исходный код в проводнике компилятора Годболта )

После умножения oldstateна MULTIPLIER(результат сохраняется в rdi), GCC не добавляет этот результат INCREMENT, перемещая INCREMENTвместо него значение rdx, которое затем используется как возвращаемое значение rand32_ret.state.

Минимальный воспроизводимый пример ( Compiler Explorer ):

#include <stdint.h>

struct retstruct {
    uint32_t a;
    uint64_t b;
};

struct retstruct fn(uint64_t input)
{
    struct retstruct ret;

    ret.a = 0;
    ret.b = input * 11111111111 + 111111111111;

    return ret;
}

Созданная сборка (GCC 9.2, x86_64, -O3):

fn:
  movabs rdx, 11111111111     # multiplier constant (doesn't fit in imm32)
  xor eax, eax                # ret.a = 0
  imul rdi, rdx
  movabs rdx, 111111111111    # add constant; one more 1 than multiplier
     # missing   add rdx, rdi   # ret.b=... that we get with clang or older gcc
  ret
# returns RDX:RAX = constant 111111111111 : 0
# independent of input RDI, and not using the imul result it just computed

Интересно, что изменение структуры таким образом, чтобы uint64_t в качестве первого члена приводил к правильному коду , равно как и изменение обоих членов на uint64_t.

x86-64 System V действительно возвращает структуры размером менее 16 байт в RDX: RAX, когда они тривиально копируемы. В этом случае 2-й элемент находится в RDX, потому что верхняя половина RAX является отступом для выравнивания или .bкогда .aиспользуется более узкий тип. ( sizeof(retstruct)16 в любом случае; мы не используем, __attribute__((packed))поэтому он учитывает alignof (uint64_t) = 8.)

Содержит ли этот код какое-либо неопределенное поведение, которое позволило бы GCC выдавать «неправильную» сборку?

Если нет, об этом следует сообщить на https://gcc.gnu.org/bugzilla/

vitorhnn
источник
Комментарии не для расширенного обсуждения; этот разговор был перенесен в чат .
Самуэль Лев

Ответы:

102

Я не вижу здесь никакого UB; ваши типы не подписаны, поэтому UB с переполнением со знаком невозможен, и в этом нет ничего странного. (И даже если он подписан, он должен будет производить правильные выходные данные для входов, которые не вызывают переполнение UB, например rdi=1). Это сломано и с GCC-интерфейсом C ++.

Кроме того, GCC8.2 правильно компилирует его для AArch64 и RISC-Vmaddинструкцию после использования movkдля построения констант или в RISC-V mul и добавляет после загрузки констант). Если бы это был UB, который GCC находил, мы, как правило, ожидали, что он найдет его и сломает ваш код также для других ISA, по крайней мере для тех, которые имеют одинаковую ширину типов и ширину регистров.

Clang также правильно его компилирует.

Похоже, это регрессия от GCC 5 до 6; GCC5.4 компилируется правильно, 6.1 и позже - нет. ( Godbolt ).

Вы можете сообщить об этом на bugzilla GCC, используя MCVE из вашего вопроса.

Похоже, что это ошибка в обработке возврата структуры x86-64 System V, возможно, структур, содержащих отступы. Это объясняет, почему это работает при вставке и при расширении aдо uint64_t (избегая заполнения).

Питер Кордес
источник
11
@vitorhnn Похоже, это было исправлено master.
SS Anne
14

Содержит ли этот код какое-либо неопределенное поведение, которое позволило бы GCC выдавать «неправильную» сборку?

Поведение кода, представленного в вопросе, четко определено в отношении стандартов языка C99 и более поздних. В частности, C позволяет функциям возвращать структурные значения без ограничений.

Джон Боллинджер
источник
2
GCC производит отдельное определение функции; это то, на что мы смотрим, независимо от того, работает ли это, когда вы компилируете его в модуль перевода вместе с другими функциями. Вы также можете легко протестировать его без фактического использования __attribute__((noinline)), скомпилировав его в единицу перевода и связав без LTO, или скомпилировав, -fPICчто подразумевает, что все глобальные символы являются (по умолчанию) вставляемыми, поэтому не могут быть встроены в вызывающие объекты. Но на самом деле проблему можно обнаружить, просто взглянув на сгенерированный asm, независимо от вызывающих.
Питер Кордес
Справедливо, @PeterCordes, хотя я достаточно уверен, что эта деталь была изменена из-под меня в Godbolt.
Джон Боллинджер
Версия 1 вопроса связана с Godbolt только с помощью самой функции в переводческой единице, например, состояния самого вопроса, когда вы ответили. Я не проверял все изменения или комментарии, которые вы могли просматривать. Вода под мостом, но я не думаю, что когда-либо утверждалось, что автономное определение асма было нарушено только при использовании источника __attribute__((noinline)). (Это было бы шокирующим, не просто удивительно, как ошибка правильности GCC). Вероятно, это было упомянуто только для того, чтобы сделать тестовую программу, которая печатает результат.
Питер Кордес