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_canonicalconfirma 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.outy clang 3.3
clang++ -stdlib=libc++ -std=c++11 test.c && ./a.outSi 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.outda la misma salida, lddrendimientos
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.fen 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.fy 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>::epsiloncheques 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
RealTypedegenerate_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.0ya no se generará; en cambio, los valores límite0x1.fffffep-Npara0 < N <= 8se generarán con más frecuencia (aproximadamente2^(8 - N - 32)porN, dependiendo de la distribución real de MT19937).Yo recomendaría no usar
floatconstd::generate_canonicaldirectamente; en lugar de generar el número endoubley 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óndoubley 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_canonicaldeberí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.0no 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
Tesfloat(24 bits de mantisa), esperamos veruniform_real_distribution<float>(0,1)(g) == 1.0faproximadamente 1 de cada 2 ^ 25 veces. Mi experimentación de fuerza bruta con libc ++ confirma esta expectativa.Salida de ejemplo:
Cuando
Tesdouble(53 bits de mantisa), esperamos veruniform_real_distribution<double>(0,1)(g) == 1.0aproximadamente 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.0fpero 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 dentroepsde 1, está en un estado de pecado.canonical - 1.0f. Por cada flotante representable[0, 1.0),x-1.0fno es cero. Con exactamente 1.0f, puede obtener una división por cero en lugar de solo un divisor muy pequeño.