Рассмотрим следующую настройку:
typedef struct
{
float d;
} InnerStruct;
typedef struct
{
InnerStruct **c;
} OuterStruct;
float TestFunc(OuterStruct *b)
{
float a = 0.0f;
for (int i = 0; i < 8; i++)
a += b->c[i]->d;
return a;
}
Цикл for в TestFunc точно повторяет цикл в другой функции, которую я тестирую. Оба цикла разворачиваются с помощью gcc (4.9.2), но после этого получается немного другая сборка.
Сборка для моего тестового цикла:ㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤСборка для исходного цикла:
lwz r9,-0x725C(r13) lwz r9,0x4(r3)
lwz r8,0x4(r9) lwz r8,0x8(r9)
lwz r10,0x0(r9) lwz r10,0x4(r9)
lwz r11,0x8(r9) lwz r11,0x0C(r9)
lwz r4,0x4(r8) lwz r3,0x4(r8)
lwz r10,0x4(r10) lwz r10,0x4(r10)
lwz r8,0x4(r11) lwz r0,0x4(r11)
lwz r11,0x0C(r9) lwz r11,0x10(r9)
efsadd r4,r4,r10 efsadd r3,r3,r10
lwz r10,0x10(r9) lwz r8,0x14(r9)
lwz r7,0x4(r11) lwz r10,0x4(r11)
lwz r11,0x14(r9) lwz r11,0x18(r9)
efsadd r4,r4,r8 efsadd r3,r3,r0
lwz r8,0x4(r10) lwz r0,0x4(r8)
lwz r10,0x4(r11) lwz r8,0x0(r9)
lwz r11,0x18(r9) lwz r11,0x4(r11)
efsadd r4,r4,r7 efsadd r3,r3,r10
lwz r9,0x1C(r9) lwz r10,0x1C(r9)
lwz r11,0x4(r11) lwz r9,0x4(r8)
lwz r9,0x4(r9) efsadd r3,r3,r0
efsadd r4,r4,r8 lwz r0,0x4(r10)
efsadd r4,r4,r10 efsadd r3,r3,r11
efsadd r4,r4,r11 efsadd r3,r3,r9
efsadd r4,r4,r9 efsadd r3,r3,r0
Проблема в том, что возвращаемые этими инструкциями значения с плавающей запятой не совсем совпадают. И я не могу изменить исходный цикл. Мне нужно как-то изменить тестовый цикл, чтобы он возвращал те же значения. Я считаю, что сборка теста эквивалентна простому добавлению каждого элемента один за другим. Я не очень хорошо знаком с ассемблером, поэтому я не был уверен, как приведенные выше различия переводятся в c. Я знаю, что это проблема, потому что, если я добавлю печать в циклы, они не развернутся, и результаты будут соответствовать ожидаемым.
Плавающая точка по своей природе неточна, и разные способы компиляции кода могут привести к несколько разным неточностям.
Отредактируйте вопрос, чтобы предоставить минимальный воспроизводимый пример, включая исходный код и команды компиляции, которые создают ассемблерный код.
c[i]
— это указатель, поэтому b->c[i].d
не должен компилироваться. Пожалуйста, предоставьте минимальный воспроизводимый пример.
Порядок инструкций efsadd повлияет на результат. Но оптимизатор не обязательно будет выполнять добавления в том же порядке для исходного цикла, что и для тестового цикла. Возможно, вы сможете получить одинаковые результаты, убедившись, что все числа имеют одинаковую величину и имеют точное представление с плавающей запятой. Это предполагает, что в ходе тестирования вы можете контролировать числа, добавляемые исходным циклом.
Плавающая точка по своей природе неточна по отношению к десятичной или действительной арифметике, в которой люди привыкли думать, но в пределах своей области она совершенно точна. Также предполагается, что он идеально повторяем — т. е. проверка на точное равенство может быть законной. Поэтому, когда подобные упражнения показывают несоответствия, они обычно возникают из-за ошибки, допущенной программистом, или из-за ошибки в компиляторе. Но у нас пока недостаточно информации об этом несоответствии, чтобы сказать, какое именно. Нам нужно увидеть «другую функцию, которая копируется» и, возможно, входные данные.
Похоже на проблему X-Y. Лучше исправить то, что ломается, когда результат отличается, потому что это ошибка. Вы действительно «потеряли точность», а не просто получили другой неточный результат? Помня, что float имеет конечную точность всего в шесть значащих цифр после запятой.
Сложение с плавающей запятой не является ассоциативным, как математическое сложение. Таким образом, результат может отличаться, если порядок операций отличается. Фрагмент слева вычисляет (слева направо): c[0].d + c[1].d + c[2].d + c[3].d + c[4].d + c[5].d + c[6].d + c[7].d. Фрагмент справа вычисляет: c[1].d + c[2].d + c[3].d + c[4].d + c[5].d + c[6].d + c[0].d + c[7].d. Попробуйте указать компилятору поддерживать строжайшее соответствие стандарту IEEE-754 (например, -fp-model:strict для компилятора Intel). Может помочь, но не гарантированно поможет.
@njuffa Хороший улов. Поскольку сложение с плавающей запятой не является ассоциативным, компилятору обычно неправильно переупорядочивать выражения с плавающей запятой на основе ассоциативности, что я и имел в виду, когда сказал «из-за... ошибки в компиляторе». (Или, если «другая копируемая функция» действительно запросила этот другой порядок, то здесь мы имеем «ошибку, допущенную программистом».)
@SteveSummit Различные переключатели компиляции обычно позволяют повторно связывать выражения с плавающей запятой, а некоторые компиляторы могут даже включить одно из них по умолчанию. В частности, следует избегать -ffast-math. К сожалению, спрашивающий не задокументировал используемые ключи компиляции.
@SteveSummit Да, я, наверное, мог бы использовать в вопросе слово получше, чем «точность». Я знал, что само число не будет точно соответствовать ожидаемому результату. Но обе функции должны иметь одинаковую неточность в своих результатах, то есть эти числа должны точно совпадать. Я считаю, что @njuffa на правильном пути с -fp-model:strict или -ffast-math. Если это так, то вопрос будет заключаться в том, как я буду навязывать порядок добавления элементов. Я не могу изменить переключатели компиляции для этого одного теста, поэтому мне придется найти решение, основанное исключительно на коде.
Если компилятору был предоставлен переключатель, указывающий, что он может переупорядочивать операции с плавающей запятой, как правило, нет реального способа написать исходный код, чтобы обеспечить определенный порядок операций с плавающей запятой. Мы все еще ждем, когда вы предоставите минимальный воспроизводимый пример, включая исходный код, который воспроизводит проблему, и команды компиляции, которые создают показанный код сборки.
@EricPostpischil Это скорее концептуальная проблема, чем проблема, связанная с каким-либо конкретным кодом. Я не могу поделиться ни точным исходным кодом, в котором есть эти проблемы, ни сценариями компиляции.
Я предполагаю, что это для модульного тестирования одной функции с другой.
Как правило, вычисления с плавающей запятой никогда не бывают точными в C или C++, и обычно не считается законным ожидать, что они будут точными.
Стандарт языка Java требует точных результатов с плавающей запятой. Это является постоянным источником ненависти к Java, с различными обвинениями в том, что воспроизводимость результатов обычно делает их менее точными, а иногда и код намного медленнее.
Если вы проводите тестирование на C или C++, я бы предложил такой подход:
Рассчитайте результат как можно лучше, как с высокой точностью, так и с высокой точностью. В этом случае входные данные находятся в 32-битном формате с плавающей запятой, поэтому преобразуйте их все в 64-битный формат с плавающей запятой перед вычислением ожидаемого результата.
Если входные данные были двойными (и у вас нет более длинного двойного типа), то отсортируйте значения по порядку и добавьте их от меньшего к большему. Это приведет к наименьшей потере точности.
Получив ожидаемый результат, проверьте, соответствует ли результат функции ему в некоторых пределах.
Существует два подхода к настройке требуемой точности, чтобы считать тест пройденным:
Один из подходов состоит в том, чтобы проверить, каков реальный физический смысл числа и какая точность вам действительно требуется.
Другой подход состоит в том, чтобы просто потребовать, чтобы результат был точен с точностью до нескольких младших битов от идеального результата, т. е. чтобы ошибка была меньше, чем в несколько раз идеальный результат, умноженный на FLT_EPSILON.
Правда, расчет вызовет неточность по сравнению с ожидаемым результатом. Но, как сказал @SteveSummit, не должна ли неточность в обеих функциях быть одинаковой? Если они оба работают с одними и теми же ключами компиляции, я ожидаю, что они всегда будут давать одинаковые результаты. Оба этих результата неточны по сравнению с ожидаемым результатом, да, но по сравнению друг с другом они должны оставаться точно равными. Я уже тестирую поплавки в определенных пределах (± 0,001), и 22% тестов терпят неудачу из-за несоответствия между двумя циклами.
В основном то, что вы предлагаете, верно для Java, но не верно для C или C++. Причина в том, что в C/C++ компилятору разрешено распространять промежуточные значения с большей точностью, чем обеспечивает тип, и он не обязан делать это последовательно. Java требует, чтобы дополнительная точность отбрасывалась после каждой операции, что, по мнению многих людей, глупо, но, по крайней мере, даст тот вид детерминизма, на который вы надеетесь.
Если это правда, почему отключение быстрой математики решает проблему?
Такова природа чего-то, что не гарантируется точностью. Это может быть то же самое в любое время, когда он хочет, это просто не должно быть. Тем не менее, я все равно поддержу ваш ответ, так как это решение, которое вы хотели в этом случае.
Кажется, я не понимаю. Я провел тест 10 000 случайных значений с плавающей запятой для d. С быстрой математикой мы получаем этот разный порядок сложения, и примерно 30% этих тестов выходят за пределы допустимого диапазона ±0,001. Но с отключенной быстрой математикой каждая разница рассчитывается как ровно 0, и все 10 000 тестов проходят. Я что-то упускаю?
Да. Вы упускаете тот факт, что компилятору разрешено делать то, что вы наблюдали, и ему также разрешено делать по-другому, если то, что он делает по-другому, является более точным. Если вы скомпилируете этот код (с быстрой математикой или без нее) для процессора, который аппаратно поддерживает double, но не float, или где double не медленнее, чем float, он может решить делать все в double — тогда ответ будет не совсем соответствовать значению, которое вы вычисляете в float.
К счастью, это выполняется только на одном процессоре, где я получаю ожидаемый результат. Так что похоже, что этого решения достаточно в этом случае. Я ценю помощь и информацию!
Отключение быстрой математики, кажется, решает эту проблему. Спасибо @njuffa за предложение. Я надеялся разработать тестовую функцию с учетом этой оптимизации, но это не представляется возможным. По крайней мере, теперь я знаю, в чем проблема. Благодарю всех за помощь в решении проблемы!
Каковы некоторые примеры различных результатов, которые вы получаете? Если различия близки к точности, которую может представлять число с плавающей запятой, я не думаю, что вам следует полагаться на получение точно такого же результата при любых условиях.