Siempre pensé que los números aleatorios estarían entre cero y uno, sin ellos1
, es decir, son números del intervalo medio abierto [0,1]. La documentación en cppreference.com de std::generate_canonical
confirma esto.
Sin embargo, cuando ejecuto el siguiente programa:
#include <iostream>
#include <limits>
#include <random>
int main()
{
std::mt19937 rng;
std::seed_seq sequence{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
rng.seed(sequence);
rng.discard(12 * 629143 + 6);
float random = std::generate_canonical<float,
std::numeric_limits<float>::digits>(rng);
if (random == 1.0f)
{
std::cout << "Bug!\n";
}
return 0;
}
Me da el siguiente resultado:
Bug!
es decir, me genera un perfecto 1
, lo que causa problemas en mi integración MC. ¿Es ese comportamiento válido o hay un error de mi parte? Esto da el mismo resultado con G ++ 4.7.3
g++ -std=c++11 test.c && ./a.out
y clang 3.3
clang++ -stdlib=libc++ -std=c++11 test.c && ./a.out
Si este es el comportamiento correcto, ¿cómo puedo evitarlo 1
?
Edición 1 : G ++ de git parece sufrir el mismo problema. Estoy en
commit baf369d7a57fb4d0d5897b02549c3517bb8800fd
Date: Mon Sep 1 08:26:51 2014 +0000
y compilando ~/temp/prefix/bin/c++ -std=c++11 -Wl,-rpath,/home/cschwan/temp/prefix/lib64 test.c && ./a.out
da la misma salida, ldd
rendimientos
linux-vdso.so.1 (0x00007fff39d0d000)
libstdc++.so.6 => /home/cschwan/temp/prefix/lib64/libstdc++.so.6 (0x00007f123d785000)
libm.so.6 => /lib64/libm.so.6 (0x000000317ea00000)
libgcc_s.so.1 => /home/cschwan/temp/prefix/lib64/libgcc_s.so.1 (0x00007f123d54e000)
libc.so.6 => /lib64/libc.so.6 (0x000000317e600000)
/lib64/ld-linux-x86-64.so.2 (0x000000317e200000)
Edición 2 : informé el comportamiento aquí: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=63176
Edición 3 : El equipo clang parece estar al tanto del problema: http://llvm.org/bugs/show_bug.cgi?id=18767
1.f == 1.f
en todos los casos (¿en qué casos están ahí? Ni siquiera he visto ninguna variable1.f == 1.f
; solo hay un caso aquí:1.f == 1.f
y eso es invariablementetrue
). Por favor, no difunda más este mito. Las comparaciones de punto flotante son siempre exactas.abs(random - 1.f) < numeric_limits<float>::epsilon
cheques si el resultado es cercano a 1.0 , lo cual es totalmente incorrecto en este contexto: hay números cercanos a 1.0 que son resultados válidos aquí, a saber, todos aquellos que son menores a 1.0.Respuestas:
El problema está en el mapeo del codominio de
std::mt19937
(std::uint_fast32_t
) afloat
; el algoritmo descrito por el estándar da resultados incorrectos (inconsistentes con su descripción de la salida del algoritmo) cuando se produce una pérdida de precisión si el modo de redondeo IEEE754 actual no es redondo a infinito negativo (tenga en cuenta que el valor predeterminado es redondo -al más cercano).La salida 7549723a de mt19937 con su semilla es 4294967257 (
0xffffffd9u
), que cuando se redondea a flotación de 32 bits0x1p+32
, que es igual al valor máximo de mt19937, 4294967295 (0xffffffffu
) cuando eso también se redondea a flotación de 32 bits.El estándar podría garantizar un comportamiento correcto si especificara que al convertir de la salida de la URNG a la
RealType
degenerate_canonical
, el redondeo debe realizarse hacia el infinito negativo; Esto daría un resultado correcto en este caso. Como QOI, sería bueno para libstdc ++ hacer este cambio.Con este cambio,
1.0
ya no se generará; en cambio, los valores límite0x1.fffffep-N
para0 < N <= 8
se generarán con más frecuencia (aproximadamente2^(8 - N - 32)
porN
, dependiendo de la distribución real de MT19937).Yo recomendaría no usar
float
constd::generate_canonical
directamente; en lugar de generar el número endouble
y luego redondear hacia el infinito negativo:Este problema también puede ocurrir con
std::uniform_real_distribution<float>
; la solución es la misma, para especializar la distribucióndouble
y redondear el resultado hacia infinito negativo enfloat
.fuente
sin(x)
, lo que realmente quiere es el seno de (π / Math.PI) veces x. Las personas que mantienen Java insisten en que es mejor tener un informe lento de rutina matemática que el seno de Math.PI es la diferencia entre π y Math.PI que hacer que informe un valor que es ligeramente menor, a pesar de que en el 99% de las aplicaciones sería mejor ...std::uniform_real_distribution<float>
sufre el mismo problema como consecuencia de esto. (Para que las personas que buscan uniform_real_distribution tengan este Q / A aparece).generate_canonical
debería generar un número en el rango[0,1)
, y estamos hablando de un error en el que genera 1.0 ocasionalmente, ¿no sería tan efectivo redondear hacia cero?Según el estándar,
1.0
no es válido.fuente
Me encontré con una pregunta similar con
uniform_real_distribution
, y así es como interpreto la redacción parsimoniosa de la Norma sobre el tema:El estándar siempre define las funciones matemáticas en términos de matemáticas , nunca en términos de punto flotante IEEE (porque el estándar todavía finge que punto flotante podría no significar punto flotante IEEE). Entonces, cada vez que ve una redacción matemática en el Estándar, se trata de matemáticas reales , no de IEEE.
La norma dice que tanto
uniform_real_distribution<T>(0,1)(g)
ygenerate_canonical<T,1000>(g)
debe devolver valores en el rango medio abierta [0,1). Pero estos son valores matemáticos . Cuando toma un número real en el rango medio abierto [0,1) y lo representa como punto flotante IEEE, bueno, una fracción significativa del tiempo se redondearáT(1.0)
.Cuando
T
esfloat
(24 bits de mantisa), esperamos veruniform_real_distribution<float>(0,1)(g) == 1.0f
aproximadamente 1 de cada 2 ^ 25 veces. Mi experimentación de fuerza bruta con libc ++ confirma esta expectativa.Salida de ejemplo:
Cuando
T
esdouble
(53 bits de mantisa), esperamos veruniform_real_distribution<double>(0,1)(g) == 1.0
aproximadamente 1 de cada 2 ^ 54 veces. No tengo la paciencia para probar esta expectativa. :)Tengo entendido que este comportamiento está bien. Puede ofender nuestro sentido de "rango medio abierto" de que una distribución que dice devolver números "menores que 1.0" puede de hecho devolver números que son iguales a
1.0
; pero esos son dos significados diferentes de "1.0", ¿ves? El primero es el matemático 1.0; el segundo es el número de coma flotante de precisión simple IEEE1.0
. Y nos han enseñado durante décadas a no comparar números de punto flotante para la igualdad exacta.Cualquier algoritmo en el que alimente los números aleatorios no le importará si a veces es exacto
1.0
. No hay nada que pueda hacer con un número de punto flotante, excepto las operaciones matemáticas, y tan pronto como realice alguna operación matemática, su código tendrá que lidiar con el redondeo. Incluso si pudieras asumir eso legítimamentegenerate_canonical<float,1000>(g) != 1.0f
, aún no podrías asumir esogenerate_canonical<float,1000>(g) + 1.0f != 2.0f
, debido al redondeo. Simplemente no puedes alejarte de eso; Entonces, ¿por qué fingiríamos en esta única instancia que puedes?fuente
1.0f
pero eso es inevitable cuando los lanzas a flotadores IEEE. Si desea resultados matemáticos puros, use un sistema de cálculo simbólico; Si está tratando de usar el punto flotante IEEE para representar números que están dentroeps
de 1, está en un estado de pecado.canonical - 1.0f
. Por cada flotante representable[0, 1.0)
,x-1.0f
no es cero. Con exactamente 1.0f, puede obtener una división por cero en lugar de solo un divisor muy pequeño.