java - que - punto flotante ejemplos
¿Qué podría causar que los números de coma flotante se desconectaran repentinamente en 1 bit sin cambios aritméticos? (3)
Sin un código fuente exacto para reproducir el problema, es obviamente imposible identificar el problema. Pero tu diff muestra que cambiaste la forma en que se procesan las listas. También mencionas que muchas matemáticas simples, como la adición, ocurren dentro de tu aplicación. Por lo tanto, supongo que con los cambios en la lista puede cambiar el orden en que se procesan las cosas, lo que puede ser suficiente para cambiar los errores de redondeo.
Y sí, nada debería depender nunca de los bits menos significativos de variables de punto flotante, por lo que las pruebas deberían necesitar épsilon.
Al hacer un cambio de refactorización algo grande que no modificó ningún tipo de aritmética , logré de alguna manera cambiar la salida de mi programa (un sistema de simulación basado en agentes). Varios números en la salida ahora están apagados por cantidades minúsculas. El examen muestra que estos números están desactivados en 1 bit en su bit menos significativo.
Por ejemplo, 24.198110084326416 se convertiría en 24.19811008432642. La representación en coma flotante de cada número es:
24.198110084326416 = 0 10000000011 1000001100101011011101010111101011010011000010010100
24.19811008432642 = 0 10000000011 1000001100101011011101010111101011010011000010010101
En el cual notamos que el bit menos significativo es diferente.
Mi pregunta es cómo podría haber introducido este cambio cuando no había modificado ningún tipo de aritmética. El cambio implicó simplificar un objeto eliminando la herencia (su superclase estaba llena de métodos que no eran aplicables a esta clase).
Observo que el resultado (que muestra los valores de ciertas variables en cada tilde de la simulación) a veces estará desactivado, luego, para otro tic, los números son los esperados, solo que estarán desactivados nuevamente para el siguiente tic (por ejemplo, en un agente) , sus valores exhiben este problema en los ticks 57 - 83, pero son como se esperaba para los tics 84 y 85, pero se desactivan nuevamente para tick 86).
Soy consciente de que no debemos comparar los números de coma flotante directamente. Estos errores se notaron cuando una prueba de integración que simplemente comparaba el archivo de salida con un resultado esperado falló. Podría (y tal vez debería) corregir la prueba para analizar los archivos y comparar los dobles analizados con algunos epsilon, pero aún tengo curiosidad de por qué se pudo haber presentado este problema.
EDITAR:
Diferencia mínima de cambio que introdujo el problema:
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''s 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;
}
Lamentablemente, no puedo crear un ejemplo pequeño y compilable, debido a que no puedo replicar este comportamiento fuera del programa ni reducir este gran programa enmarañado.
En cuanto a qué tipo de operaciones de punto flotante se están realizando, no hay nada particularmente emocionante. Una tonelada de suma, multiplicación, logaritmos naturales y poderes (casi siempre con base e). Los dos últimos se hacen con la biblioteca estándar. Los números aleatorios se utilizan en todo el programa y se generan con la clase Random
incluida con el marco que se utiliza (Repast).
La mayoría de los números están en el rango de 1e-3 a 1e5. Casi no hay números muy grandes o muy pequeños. Infinity y NaN se usan en muchos lugares.
Siendo un sistema de simulación basado en agentes, muchas fórmulas se aplican repetitivamente para simular la emergencia. El orden de evaluación es muy importante (dado que muchas variables dependen de otras que se evalúen primero, por ejemplo, para calcular el IMC, primero se debe calcular la dieta y el estado cardiovascular). Los valores previos de las variables también son muy importantes en muchos cálculos (por lo que este problema podría introducirse en algún momento temprano en el programa y llevarse a cabo durante el resto del mismo).
Como strictfp ha sido eliminado, voy a ofrecer una idea.
Algunas versiones de Repast tenían / tienen errores con ciertos números aleatorios generados incorrectamente *.
Incluso con la semilla aleatoria configurada con el mismo valor, ya que su ArrayList se crea y usa en un punto diferente de su código, es posible que esté actuando sobre los agentes en un orden diferente. Esto es particularmente cierto si tiene algún método programado con prioridad aleatoria. También es el caso si usa getAgentList () o similar para llenar su lista subAgents. En efecto, puede generar un número aleatorio (/ orden) que esté fuera del RNG para el que estableció el valor inicial.
Si hay una ligera diferencia en el orden de ejecución, esto podría explicar la correspondencia en un solo paso para ver esta pequeña diferencia en otros pasos.
He tenido que pasar esto y tuve dolores de cabeza similares a su informe cuando estaba depurando. Feliz de entrar en más detalles si puede proporcionarlos.
* Te ayudará mucho saber qué versión estás usando (sé que no debería pedir una aclaración en una respuesta, pero no he recibido la respuesta para comentar). Desde la API que enlazas, creo que estás utilizando el antiguo Repast 3 - Utilizo Simphony, pero la respuesta aún puede aplicarse.
Aquí hay algunas maneras en que la evaluación de una expresión de coma flotante puede diferir:
(1) Los procesadores de punto flotante tienen un "modo de redondeo actual", que podría causar que los resultados difieran en el bit menos significativo. Puede hacer una llamada que puede obtener o establecer el valor actual: redondear hacia cero, hacia -∞, o hacia + ∞.
(2) Parece que el strictfp está relacionado con FLT_EVAL_METHOD en C, que especifica la precisión que se utilizará en los cálculos intermedios. Algunas veces, una nueva versión del compilador usará un método diferente al anterior (me mordió). {0,1,2} corresponde a la precisión {simple, doble, extendida}, respectivamente, a menos que sea reemplazado por operandos de mayor precisión.
(3) De la misma forma que un compilador diferente puede tener un método de evaluación de flotación predeterminado diferente, diferentes máquinas pueden usar un método diferente de evaluación de flotación.
(4) La aritmética de punto flotante IEEE de precisión simple está bien definida, repetible e independiente de la máquina. Entonces es de doble precisión. He escrito (con mucho cuidado) pruebas de punto flotante multiplataforma que utilizan un hash SHA-1 para verificar los cálculos de exactitud de bits. Sin embargo, con FLT_EVAL_METHOD = 2, se usa la precisión extendida para los cálculos intermedios, que se implementa de diversas maneras usando aritmética de coma flotante de 64 bits, 80 bits o 128 bits, por lo que es difícil obtener repetibilidad multiplataforma y compilador cruzado si se usa precisión extendida en los cálculos intermedios.
(5) La aritmética de coma flotante no es asociativa, es decir,
(A + B) + C ≠ A + (B + C)
Los compiladores no pueden reordenar los cálculos de los números de coma flotante debido a esto.
(6) La orden de las operaciones es importante. Un algoritmo para calcular la suma de un gran conjunto de números con la mayor precisión posible, es sumarlos en orden creciente de magnitud. Por otro lado, si dos números difieren lo suficiente en magnitud
B < (A * epsilon)
luego, sumarlos es un no-op:
A + B = A