Различия в сборке между развернутыми циклами for вызывают разные результаты с плавающей запятой

Рассмотрим следующую настройку:

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. Я знаю, что это проблема, потому что, если я добавлю печать в циклы, они не развернутся, и результаты будут соответствовать ожидаемым.

Каковы некоторые примеры различных результатов, которые вы получаете? Если различия близки к точности, которую может представлять число с плавающей запятой, я не думаю, что вам следует полагаться на получение точно такого же результата при любых условиях.

500 - Internal Server Error 18.11.2022 22:46

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

Barmar 18.11.2022 22:48

Отредактируйте вопрос, чтобы предоставить минимальный воспроизводимый пример, включая исходный код и команды компиляции, которые создают ассемблерный код.

Eric Postpischil 18.11.2022 22:58
c[i] — это указатель, поэтому b->c[i].d не должен компилироваться. Пожалуйста, предоставьте минимальный воспроизводимый пример.
hyde 18.11.2022 23:03

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

user3386109 18.11.2022 23:32

Плавающая точка по своей природе неточна по отношению к десятичной или действительной арифметике, в которой люди привыкли думать, но в пределах своей области она совершенно точна. Также предполагается, что он идеально повторяем — т. е. проверка на точное равенство может быть законной. Поэтому, когда подобные упражнения показывают несоответствия, они обычно возникают из-за ошибки, допущенной программистом, или из-за ошибки в компиляторе. Но у нас пока недостаточно информации об этом несоответствии, чтобы сказать, какое именно. Нам нужно увидеть «другую функцию, которая копируется» и, возможно, входные данные.

Steve Summit 18.11.2022 23:34

Похоже на проблему X-Y. Лучше исправить то, что ломается, когда результат отличается, потому что это ошибка. Вы действительно «потеряли точность», а не просто получили другой неточный результат? Помня, что float имеет конечную точность всего в шесть значащих цифр после запятой.

Clifford 18.11.2022 23:35

Сложение с плавающей запятой не является ассоциативным, как математическое сложение. Таким образом, результат может отличаться, если порядок операций отличается. Фрагмент слева вычисляет (слева направо): 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 18.11.2022 23:36

@njuffa Хороший улов. Поскольку сложение с плавающей запятой не является ассоциативным, компилятору обычно неправильно переупорядочивать выражения с плавающей запятой на основе ассоциативности, что я и имел в виду, когда сказал «из-за... ошибки в компиляторе». (Или, если «другая копируемая функция» действительно запросила этот другой порядок, то здесь мы имеем «ошибку, допущенную программистом».)

Steve Summit 18.11.2022 23:39

@SteveSummit Различные переключатели компиляции обычно позволяют повторно связывать выражения с плавающей запятой, а некоторые компиляторы могут даже включить одно из них по умолчанию. В частности, следует избегать -ffast-math. К сожалению, спрашивающий не задокументировал используемые ключи компиляции.

njuffa 18.11.2022 23:42

@SteveSummit Да, я, наверное, мог бы использовать в вопросе слово получше, чем «точность». Я знал, что само число не будет точно соответствовать ожидаемому результату. Но обе функции должны иметь одинаковую неточность в своих результатах, то есть эти числа должны точно совпадать. Я считаю, что @njuffa на правильном пути с -fp-model:strict или -ffast-math. Если это так, то вопрос будет заключаться в том, как я буду навязывать порядок добавления элементов. Я не могу изменить переключатели компиляции для этого одного теста, поэтому мне придется найти решение, основанное исключительно на коде.

Kyle Ponikiewski 21.11.2022 13:30

Если компилятору был предоставлен переключатель, указывающий, что он может переупорядочивать операции с плавающей запятой, как правило, нет реального способа написать исходный код, чтобы обеспечить определенный порядок операций с плавающей запятой. Мы все еще ждем, когда вы предоставите минимальный воспроизводимый пример, включая исходный код, который воспроизводит проблему, и команды компиляции, которые создают показанный код сборки.

Eric Postpischil 21.11.2022 13:56

@EricPostpischil Это скорее концептуальная проблема, чем проблема, связанная с каким-либо конкретным кодом. Я не могу поделиться ни точным исходным кодом, в котором есть эти проблемы, ни сценариями компиляции.

Kyle Ponikiewski 21.11.2022 22:27
Руководство для начинающих по веб-разработке на React.js
Руководство для начинающих по веб-разработке на React.js
Веб-разработка - это захватывающая и постоянно меняющаяся область, которая постоянно развивается благодаря новым технологиям и тенденциям. Одним из...
Разница между Angular и React
Разница между Angular и React
React и AngularJS - это два самых популярных фреймворка для веб-разработки. Оба фреймворка имеют свои уникальные особенности и преимущества, которые...
Инструменты для веб-скрапинга с открытым исходным кодом: Python Developer Toolkit
Инструменты для веб-скрапинга с открытым исходным кодом: Python Developer Toolkit
Веб-скрейпинг, как мы все знаем, это дисциплина, которая развивается с течением времени. Появляются все более сложные средства борьбы с ботами, а...
Калькулятор CGPA 12 для семестра
Калькулятор CGPA 12 для семестра
Чтобы запустить этот код и рассчитать CGPA, необходимо сохранить код как HTML-файл, а затем открыть его в веб-браузере. Для этого выполните следующие...
ONLBest Online HTML CSS JAVASCRIPT Training In INDIA 2023
ONLBest Online HTML CSS JAVASCRIPT Training In INDIA 2023
О тренинге HTML JavaScript :HTML (язык гипертекстовой разметки) и CSS (каскадные таблицы стилей) - две основные технологии для создания веб-страниц....
Как собрать/развернуть часть вашего приложения Angular
Как собрать/развернуть часть вашего приложения Angular
Вам когда-нибудь требовалось собрать/развернуть только часть вашего приложения Angular или, возможно, скрыть некоторые маршруты в определенных средах?
0
13
87
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

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

Как правило, вычисления с плавающей запятой никогда не бывают точными в C или C++, и обычно не считается законным ожидать, что они будут точными.

Стандарт языка Java требует точных результатов с плавающей запятой. Это является постоянным источником ненависти к Java, с различными обвинениями в том, что воспроизводимость результатов обычно делает их менее точными, а иногда и код намного медленнее.

Если вы проводите тестирование на C или C++, я бы предложил такой подход:

Рассчитайте результат как можно лучше, как с высокой точностью, так и с высокой точностью. В этом случае входные данные находятся в 32-битном формате с плавающей запятой, поэтому преобразуйте их все в 64-битный формат с плавающей запятой перед вычислением ожидаемого результата.

Если входные данные были двойными (и у вас нет более длинного двойного типа), то отсортируйте значения по порядку и добавьте их от меньшего к большему. Это приведет к наименьшей потере точности.

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

Существует два подхода к настройке требуемой точности, чтобы считать тест пройденным:

Один из подходов состоит в том, чтобы проверить, каков реальный физический смысл числа и какая точность вам действительно требуется.

Другой подход состоит в том, чтобы просто потребовать, чтобы результат был точен с точностью до нескольких младших битов от идеального результата, т. е. чтобы ошибка была меньше, чем в несколько раз идеальный результат, умноженный на FLT_EPSILON.

Правда, расчет вызовет неточность по сравнению с ожидаемым результатом. Но, как сказал @SteveSummit, не должна ли неточность в обеих функциях быть одинаковой? Если они оба работают с одними и теми же ключами компиляции, я ожидаю, что они всегда будут давать одинаковые результаты. Оба этих результата неточны по сравнению с ожидаемым результатом, да, но по сравнению друг с другом они должны оставаться точно равными. Я уже тестирую поплавки в определенных пределах (± 0,001), и 22% тестов терпят неудачу из-за несоответствия между двумя циклами.

Kyle Ponikiewski 21.11.2022 13:43

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

Tom V 21.11.2022 17:58

Если это правда, почему отключение быстрой математики решает проблему?

Kyle Ponikiewski 21.11.2022 22:26

Такова природа чего-то, что не гарантируется точностью. Это может быть то же самое в любое время, когда он хочет, это просто не должно быть. Тем не менее, я все равно поддержу ваш ответ, так как это решение, которое вы хотели в этом случае.

Tom V 22.11.2022 09:54

Кажется, я не понимаю. Я провел тест 10 000 случайных значений с плавающей запятой для d. С быстрой математикой мы получаем этот разный порядок сложения, и примерно 30% этих тестов выходят за пределы допустимого диапазона ±0,001. Но с отключенной быстрой математикой каждая разница рассчитывается как ровно 0, и все 10 000 тестов проходят. Я что-то упускаю?

Kyle Ponikiewski 22.11.2022 12:54

Да. Вы упускаете тот факт, что компилятору разрешено делать то, что вы наблюдали, и ему также разрешено делать по-другому, если то, что он делает по-другому, является более точным. Если вы скомпилируете этот код (с быстрой математикой или без нее) для процессора, который аппаратно поддерживает double, но не float, или где double не медленнее, чем float, он может решить делать все в double — тогда ответ будет не совсем соответствовать значению, которое вы вычисляете в float.

Tom V 22.11.2022 13:40

К счастью, это выполняется только на одном процессоре, где я получаю ожидаемый результат. Так что похоже, что этого решения достаточно в этом случае. Я ценю помощь и информацию!

Kyle Ponikiewski 23.11.2022 16:08
Ответ принят как подходящий

Отключение быстрой математики, кажется, решает эту проблему. Спасибо @njuffa за предложение. Я надеялся разработать тестовую функцию с учетом этой оптимизации, но это не представляется возможным. По крайней мере, теперь я знаю, в чем проблема. Благодарю всех за помощь в решении проблемы!

Другие вопросы по теме