2016-10-20 1 views
2

У меня когда-то была очень тонкая ошибка в одном из проектов, которые мне пришлось поддерживать. По существу, это делалось примерно так:Практические последствия нарушения строгой сглаживания между int и float

union Value { 
    int64_t int64; 
    int32_t int32[2]; 
    int16_t shorts[4]; 
    int8_t chars[8]; 
    float floats[2]; 
    double float64; 
}; 

Value v; 
// in one place (not sure about exact code, it could be just memcpy): 
v.shorts[0] = <some short value>; 
v.shorts[1] = <some other short value>; 
// in another place: 
float f = v.floats[0]; 

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

  1. Компилятор может ввернуть что-то с оптимизацией, не понимая, что он имеет дело с той же памятью здесь. Довольно маловероятно в этом случае, поскольку записи и чтения происходят в совершенно разных местах.

  2. Ничего плохого на самом деле не происходит, и значение float просто читается поэтапно.

На практике это было почти всегда случай 2, за один раз, за ​​исключением. После запуска программы, скомпилированной с MSVC 2010 в режиме выпуска около 100-150 входных файлов, в одном из файлов она породила неверное значение, которое отличалось ровно одним битом от того, что должно было быть в соответствии с здравым смыслом. Это тоже был довольно значительный бит, так что вместо, скажем, 1.5, я получил что-то вроде 117.9. Я смог проследить его до этого точного чтения, и после исправления кода, чтобы придерживаться строгих правил псевдонимов, все работало нормально.

Вопрос теперь, чисто с точки зрения низкого уровня, что могло бы вызвать это? Некоторые особенности обработки ЦП с плавающей запятой? Особенности кеширования оборудования? Компилятор причуды? Почему только одно значение было неправильным?

Аппаратное обеспечение было некоторым старым двухъядерным 64-разрядным процессором Intel с 32-разрядной Windows 7, если это поможет. Программа представляет собой однопоточное консольное приложение, ничего необычного. Проблема была на 100% воспроизводимой, одни и те же входные файлы всегда производили один и тот же вывод, и это всегда одно и то же значение, которое было неправильным.

+0

Вам действительно нужно посмотреть на сборку; рассуждение о UB на уровне языка - безумие, рассуждение об этом на уровне сборки просто глупо;) – TartanLlama

+0

@TartanLlama, это было давно, у меня больше нет точного кода и ввода, поэтому я не думаю, что смогу воспроизведите его. Я больше искал теоретические знания от экспертов по программному и аппаратного обеспечения низкого уровня. Не то, что * вызвало * это, но то, что * могло * вызвало это. –

+0

Я предполагаю, что компилятор, который выполнял много анализа псевдонимов, мог заметить, что часть 'floats' этого объединения никогда не была инициализирована, поэтому объект не жив и поэтому просто дает вам что-то неинициализированное. LLVM имеет 'undef' для обозначения этого в IR, например. – TartanLlama

ответ

0

С точки зрения Стандарта, код v.shorts[0] = something; берет значение указателя типа «short *», добавляет ноль и использует полученный указатель для хранения значения. Я думаю, что авторы C89 предполагали, что качественные реализации, в которых использование псевдонимов было бы полезным, в этом случае распознавали бы псевдонимы, но ничто в письме Стандарта не потребовало бы этого. Обратите внимание, что когда правила были включены в C89, компиляторы должны были применять их только на очень локальном уровне; Кроме того, правила обычно не создают серьезных проблем для программистов, если они не применяются на более далеко идущем уровне. К сожалению, некоторые компиляторы настойчиво стремятся максимально расширить диапазон правил.

Если вы должны были поместить каждый массив в отдельную структуру в рамках союза, а затем сделать что-то вроде:

v.floats.arr[0] = value; 
v.floats.arr[1] = value; 
v.floats = v.floats; // Compiler knows that float* may alter float members, 
        // and that writing member of union may alter other 
        // members 

... теперь использовать другой материал

компилятор, следует надеяться, признать, что уступки для v.floats необходимо, чтобы не генерировал никакого кода, но соответствующий компилятор должен по-прежнему считать его надлежащим уведомлением о том, что другие члены профсоюза, возможно, были изменены. Обратите внимание, что шаблон не кажется надежным в gcc по состоянию на 6.2; в некоторых случаях, когда присваивание не требуется для создания какого-либо кода, компилятор будет игнорировать присваивание, включая его последствия для псевдонимов, в целом.Однако я не вижу никаких оснований для того, чтобы склониться назад, работая над нарушенным поведением gcc, - просто используйте -fno-strict-aliasing без вины, если только до тех пор, пока не будет исправлена ​​логика aliasing gcc.

+0

Интересная информация, но не совсем отвечает на мой вопрос. Я понимаю, что многое может случиться. Я не понимаю, что может произойти, что приведет к 1-битной ошибке в одном конкретном значении. И он отлично справился с GCC, только MSVS 2010 в режиме выпуска вызвал этот любопытный эффект. –

+1

@SergeyTachenov: Оптимизации странные. Было бы полезно узнать, какие значения были написаны для всего, что перекрывает «float» во время чтения ошибочных данных, какое значение ожидалось и какое значение было получено. Иногда оптимизация может перемещать записи так же, как и чтения, поэтому возможно, что какое-то значение, которое записывается после чтения в логическом порядке выполнения, может происходить раньше в машинный код, если компилятор не считает, что порядок будет отрицательно влиять на вещи. Я бы не ожидал, что MSVC2010 сделает это, но что-то ведет себя неожиданно. – supercat

+0

@SergeyTachenov: В любом случае, будь то gcc плохо работает в настоящее время, я действительно думаю, что важно знать, что авторы gcc открыто заявили, что тот факт, что часть кода работает определенным образом на сегодняшнем gcc, не представляет собой вроде того, что завтрашняя версия не нарушит его, если код зависит от любого поведения, выходящего за рамки тех, которые предусмотрены [авторской интерпретацией] Стандарта. – supercat

Смежные вопросы