Я полагаю, что обнаружил ошибку в 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/
Ответы:
Я не вижу здесь никакого UB; ваши типы не подписаны, поэтому UB с переполнением со знаком невозможен, и в этом нет ничего странного. (И даже если он подписан, он должен будет производить правильные выходные данные для входов, которые не вызывают переполнение UB, например
rdi=1
). Это сломано и с GCC-интерфейсом C ++.Кроме того, GCC8.2 правильно компилирует его для AArch64 и RISC-V (в
madd
инструкцию после использования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 (избегая заполнения).источник
master
.Это было исправлено на
trunk
/master
.Вот соответствующий коммит .
А это патч, чтобы исправить проблему.
Основываясь на комментарии в патче,
reload_combine_recognize_pattern
функция пыталась настроить USE insns .источник
Поведение кода, представленного в вопросе, четко определено в отношении стандартов языка C99 и более поздних. В частности, C позволяет функциям возвращать структурные значения без ограничений.
источник
__attribute__((noinline))
, скомпилировав его в единицу перевода и связав без LTO, или скомпилировав,-fPIC
что подразумевает, что все глобальные символы являются (по умолчанию) вставляемыми, поэтому не могут быть встроены в вызывающие объекты. Но на самом деле проблему можно обнаружить, просто взглянув на сгенерированный asm, независимо от вызывающих.__attribute__((noinline))
. (Это было бы шокирующим, не просто удивительно, как ошибка правильности GCC). Вероятно, это было упомянуто только для того, чтобы сделать тестовую программу, которая печатает результат.