Что может привести к тому, что числа с плавающей запятой внезапно отключится на 1 бит без арифметических изменений
При внесении нескольких больших изменений рефакторинга , которые не изменяли какой-либо арифметики, мне удалось каким-то образом изменить вывод моей программы (системы моделирования на основе агентов). Различные цифры на выходе теперь отключены минимальными суммами. Экзамен показывает, что эти номера отключены на 1 бит в младшем значении.
Например, 24.198110084326416 станет 24.19811008432642. Отображение с плавающей запятой каждого номера:
24.198110084326416 = 0 10000000011 1000001100101011011101010111101011010011000010010100
24.19811008432642 = 0 10000000011 1000001100101011011101010111101011010011000010010101
В котором мы замечаем, что младший значащий бит отличается.
Мой вопрос в том, как я мог бы ввести это изменение, если бы не менял какой-либо арифметики? Это изменение включало упрощение объекта путем удаления наследования (его суперкласс был вздут с помощью методов, которые не были применимы к этому классу).
Я отмечаю, что вывод (отображающий значения определенных переменных при каждом тике симуляции) иногда выключается, а затем для другого тика, числа ожидаются, только для того, чтобы снова отключиться для следующего тика (например, на одном агенте его значения показывают эту проблему на отметках 57 - 83, но, как и ожидалось, для тиков 84 и 85, только для того, чтобы снова отключиться для отметки 86).
Я знаю, что мы не должны сравнивать числа с плавающей запятой напрямую. Эти ошибки были замечены, когда тест интеграции, который просто сравнивал выходной файл с ожидаемым выходом, не удался. Я мог бы (и, возможно, должен) исправить тест, чтобы проанализировать файлы и сравнить парсированные парные с некоторым epsilon, но мне все еще интересно узнать, почему этот вопрос может быть представлен.
EDIT:
Минимальная разница в изменении, которая ввела проблему:
diff --git a/src/main/java/modelClasses/GridSquare.java b/src/main/java/modelClasses/GridSquare.java
index 4c10760..80276bd 100644
--- a/src/main/java/modelClasses/GridSquare.java
+++ b/src/main/java/modelClasses/GridSquare.java
@@ -63,7 +63,7 @@ public class GridSquare extends VariableLevel
public void addHousehold(Household hh)
{
assert household == null;
- subAgents.add(hh);
+ neighborhood.getHouseholdList().add(hh);
household = hh;
}
@@ -73,7 +73,7 @@ public class GridSquare extends VariableLevel
public void removeHousehold()
{
assert household != null;
- subAgents.remove(household);
+ neighborhood.getHouseholdList().remove(household);
household = null;
}
diff --git a/src/main/java/modelClasses/Neighborhood.java b/src/main/java/modelClasses/Neighborhood.java
index 834a321..8470035 100644
--- a/src/main/java/modelClasses/Neighborhood.java
+++ b/src/main/java/modelClasses/Neighborhood.java
@@ -166,9 +166,14 @@ public class Neighborhood extends VariableLevel
World world;
/**
+ * List of all grid squares within the neighborhood.
+ */
+ ArrayList<VariableLevel> gridSquareList = new ArrayList<>();
+
+ /**
* A list of empty grid squares within the neighborhood
*/
- ArrayList<GridSquare> emptyGridSquareList;
+ ArrayList<GridSquare> emptyGridSquareList = new ArrayList<>();
/**
* The neighborhood grid square bounds
@@ -836,7 +841,7 @@ public class Neighborhood extends VariableLevel
*/
public GridSquare getGridSquare(int i)
{
- return (GridSquare) (subAgents.get(i));
+ return (GridSquare) gridSquareList.get(i);
}
/**
@@ -865,7 +870,7 @@ public class Neighborhood extends VariableLevel
@Override
public ArrayList<VariableLevel> getGridSquareList()
{
- return subAgents;
+ return gridSquareList;
}
/**
@@ -874,12 +879,7 @@ public class Neighborhood extends VariableLevel
@Override
public ArrayList<VariableLevel> getHouseholdList()
{
- ArrayList<VariableLevel> list = new ArrayList<VariableLevel>();
- for (int i = 0; i < subAgents.size(); i++)
- {
- list.addAll(subAgents.get(i).getHouseholdList());
- }
- return list;
+ return subAgents;
}
К сожалению, я не могу создать небольшой компилируемый пример из-за того, что мне не удается воспроизвести это поведение вне программы и не разрезать эту очень большую и запутанную программу до размера.
Что касается операций с плавающей запятой, нет ничего особенно захватывающего. Тонна добавления, умножения, натуральных логарифмов и степеней (почти всегда с базой e). Последние два сделаны со стандартной библиотекой. Случайные числа используются во всей программе и генерируются с Random
классом, включенным в используемую структуру (Repast).
Большинство чисел находятся в диапазоне от 1e-3 до 1e5. Там почти нет очень больших или очень маленьких чисел. Бесконечность и NaN используются во многих местах.
Являясь системой моделирования на основе агентов, многие формулы повторяются для имитации появления. Порядок оценки очень важен (так как многие переменные зависят от того, какие другие оцениваются в первую очередь, например, для расчета ИМТ, нам необходимо сначала рассчитать диету и сердечно-сосудистую систему). Предыдущие значения переменных также очень важны во многих вычислениях (так что эта проблема может быть внедрена где-то в начале программы и переноситься по всему остальному).
Ответы
Ответ 1
Вот несколько способов, по которым оценка выражения с плавающей запятой может различаться:
(1) Процессоры с плавающей запятой имеют "режим округления тока", что может привести к разным результатам в младшем значении бит. Вы можете сделать вызов, который вы можете получить или установить текущее значение: округлить до нуля, в направлении -∞ или в сторону + ∞.
(2) Похоже, что strictfp связан с FLT_EVAL_METHOD в C, который указывает точность, которую следует использовать в промежуточных вычислениях. Иногда новая версия компилятора будет использовать другой метод, чем старый (я был укушен этим). {0,1,2} соответствуют точности {single, double, extended} соответственно, если они не переопределены операндами более высокой точности.
(3) Точно так же, как у другого компилятора может быть другой метод оценки поплавка по умолчанию, разные машины могут использовать другой метод оценки float.
(4) Арифметика с одиночной точностью IEEE с плавающей запятой является четко определенной, повторяемой и независимой от машины. Так что это двойная точность. Я написал (с большой осторожностью) кросс-платформенные тесты с плавающей точкой, которые используют хэш SHA-1 для проверки вычислений для точности бит! Однако, с FLT_EVAL_METHOD = 2, расширенная точность используется для промежуточных вычислений, которая по-разному реализуется с использованием 64-битной, 80-битной или 128-битной арифметики с плавающей запятой, поэтому трудно получить кросс-платформенную и кросс-компиляторную повторяемость если расширенная точность используется в промежуточных вычислениях.
(5) Арифметика с плавающей точкой не является ассоциативной, т.е.
(A + B) + C ≠ A + (B + C)
Составители не могут переупорядочивать вычисления чисел с плавающей запятой из-за этого.
(6) Ведется порядок операций. Алгоритм вычисления суммы большого набора чисел в максимально возможной точности состоит в том, чтобы суммировать их в порядке возрастания. С другой стороны, если два числа различаются по величине
B < (A * epsilon)
тогда их суммирование является no-op:
A + B = A
Ответ 2
По мере устранения strictfp я предложу идею.
В некоторых версиях Repast были/есть ошибки с некорректными генерируемыми случайными числами *.
Даже если случайное семя установлено на одно значение, так как ваш ArrayList создается и используется в другом месте вашего кода, возможно, что вы действуете на агентов в нем в другом порядке. Это особенно верно, если у вас есть запланированный метод со случайным приоритетом. Это также имеет место, если вы используете getAgentList() или аналогично, чтобы заполнить список субагентов. Фактически вы можете создать случайное число (/order), которое находится за пределами RNG, для которого вы устанавливаете семя.
Если есть небольшая разница в порядке выполнения, это может объяснить соответствие на одном шаге только для того, чтобы увидеть эту небольшую разницу на других этапах.
У меня было это, и у меня были подобные головные боли в вашем отчете при отладке. С удовольствием рассмотрим более подробно, если вы можете предоставить их.
* Это поможет много узнать, какую версию вы используете (я знаю, что я не должен просить разъяснений в ответе, но у меня нет комментариев). Из API, который вы связываете, я думаю, что вы используете старый Repast 3 - я использую Simphony, но ответ может по-прежнему применяться.
Ответ 3
Без точного исходного кода для воспроизведения проблемы, очевидно, невозможно точно определить проблему. Но ваш diff показывает, что вы изменили способ обработки списков. Вы также отмечаете, что в вашей заявке происходит много простой математики, например, добавление. Поэтому я предполагаю, что в результате изменений в списке вы измените порядок обработки материала, которого может быть достаточно, чтобы изменить ошибки округления.
И да, ничто никогда не должно полагаться на наименее значимые биты переменных с плавающей запятой, поэтому для тестов нужны эпсилоны.