¿Por qué muchos programas (antiguos) usan piso (0.5 + entrada) en lugar de redondo (entrada)?

80

Las diferencias residen en el valor devuelto que proporciona entradas sobre el desempate, creo, como este código :

int main()
{
    std::cout.precision(100);

    double input = std::nextafter(0.05, 0.0) / 0.1;
    double x1 = floor(0.5 + input);
    double x2 = round(input);

    std::cout << x1 << std::endl;
    std::cout << x2 << std::endl;
}

que salidas:

1
0

Pero son solo resultados diferentes al final, uno elige su preferido. Veo muchos programas "antiguos" de C / C ++ que usan en floor(0.5 + input)lugar de round(input).

¿Existe alguna razón histórica? ¿Más barato en la CPU?

Markzzz
fuente
20
std :: round mueve los casos a la mitad de cero. Eso no es matemáticamente uniforme, como el piso (f + .5), donde los casos intermedios siempre van hacia el lado superior. La razón para utilizar el método del piso es que se requiere para un redondeo adecuado en el mundo de la ingeniería.
Michaël Roy
7
Como se indica en round () para float en C ++ pre-C ++ 11, no teníamos round. Como señalé en mi respuesta, escribir tu propia ronda correctamente es un problema difícil.
Shafik Yaghmour
16
@Arne Usando std :: round () la distancia entre los valores redondeados de -0.5 y +0.5 es 2. usando floor, es 1. Solo ocurre cuando los dos valores tienen signos opuestos. Muy irritante al intentar dibujar líneas rectas, o te hace elegir el píxel de textura incorrecto.
Michaël Roy
20
Algunos lenguajes y entornos de programación (incluido .NET) utilizan algo engañoso llamado redondeo bancario, en el que x.5 redondea al número PAR más cercano. Entonces, 0.5 se redondea a 0 mientras que 1.5 se redondea a 2. Puede imaginarse la confusión que esto puede causar al depurar. Creo que la solución a esta 'característica' malvada es no tener una función .Round () en absoluto, y en su lugar tener .RoundBankers (), .RoundHalfUp (), .RoundHalfDown (), etc. (o .BankersRound (), etc. pero intellisense funcionaría mejor con .RoundBankers ()). Al menos de esa manera te verías obligado a saber qué esperar.
user3685427
8
@ user3685427: El redondeo bancario es necesario en aplicaciones financieras y estadísticas que requieren la eliminación del sesgo ascendente sutil y sistémico introducido por el redondeo de cero . Es casi imposible de implementar sin un conocimiento real de la implementación de punto flotante de hardware, por lo tanto, su selección como predeterminada en C #.
Pieter Geerkens

Respuestas:

115

std::roundse introduce en C ++ 11. Antes de eso, solo std::floorestaba disponible para que los programadores lo estuvieran usando.

haccks
fuente
Nada. Pero es más antiguo que C ++ 11. Pensé que era lógico que C ++ lo consiguiera antes del 11. Eso es todo;)
markzzz
12
@markzzz: la biblioteca estándar de C ++ no hereda automáticamente la biblioteca estándar de C. Hay una cuidadosa selección y elección. Les tomó 12 años sincronizarse con C99.
StoryTeller - Unslander Monica
2
@haccks: De hecho. En mi humilde opinión, C estaba por delante de C ++ en términos de funciones matemáticas hasta C ++ 11.
Betsabé
3
Es importante tener en cuenta que el método que se usa se rompe para algunas entradas y también que C ++ 11 se basa en C99, mientras que C ++ 03 se basa en C90, que va al punto @markzzz.
Shafik Yaghmour
1
@markzzz: Tu comentario es acertado (a pesar de las críticas). La cuestión es que había una gran brecha sin los estándares C ++, el primer estándar C ++ fue C ++ 98 y la primera revisión importante fue C ++ 11. Hubo una actualización menor, C ++ 03, pero como señala la página de Wikipedia , fue principalmente una versión de "corrección de errores". Por lo tanto, C ++ 11 fue la primera oportunidad para ponerse al día con la biblioteca estándar de C, después de 13 años de iatus.
Matthieu M.
21

No hay ninguna razón histórica. Este tipo de desviación ha existido desde el año punto. La gente hace esto cuando se siente muy, muy traviesa. Es un abuso de la aritmética de coma flotante, y muchos programadores profesionales experimentados caen en la trampa. Incluso los cuerpos de Java lo hicieron hasta la versión 1.7. Chicos divertidos.

Mi conjetura es que una función de redondeo alemana decente y lista para usar no estuvo disponible formalmente hasta C ++ 11 (a pesar de que C obtuvo la suya en C99), pero eso realmente no es excusa para adoptar la supuesta alternativa.

Aquí está la cuestión: floor(0.5 + input) no siempre recupera el mismo resultado que el correspondientestd::round llamada !

La razón es bastante sutil: el punto de corte para un redondeo alemán, a.5para un entero a, es, por una propiedad coincidente del universo, un racional diádico . Como esto se puede representar exactamente en un punto flotante IEEE754 hasta la potencia 52 de 2, y a partir de entonces el redondeo es una operación no válida de todos modos,std::round siempre funciona correctamente. Para otros esquemas de coma flotante, consulte la documentación.

Pero agregar 0.5a una doublelata introduce imprecisión, lo que provoca un ligero sobrepaso o subestimado de algunos valores. Si lo piensa, sumar dos doublevalores juntos, que son el comienzo de conversiones denarias involuntarias, y aplicar una función que es una función muy fuerte de la entrada (como una función de redondeo), seguramente terminará en lágrimas.

No lo hagas .

Referencia: ¿Por qué Math.round (0.49999999999999994) devuelve 1?

Betsabé
fuente
2
Los comentarios no son para una discusión extensa; esta conversación se ha movido al chat .
Andy
2
nearbyint()suele ser una mejor opción que round(), porque nearbyintusa el modo de redondeo actual en lugar del funky tiebreak lejos de cero de round()(para el cual x86 ni siquiera tiene soporte de hardware, aunque ARM sí). Ambos se agregaron a C ++ en C ++ 11.
Peter Cordes
9

Creo que aquí es donde te equivocas:

Pero son solo resultados diferentes al final, uno elige su preferido. Veo muchos programas "antiguos" de C / C ++ que usan floor (0.5 + input) en lugar de round (input).

Ese no es el caso. Debe seleccionar el esquema de redondeo correcto para el dominio . En una aplicación financiera, redondeará usando las reglas bancarias (no usando float por cierto). Sin embargo, cuando se muestrea, el redondeo hacia arriba static_cast<int>(floor(f + .5))produce menos ruido de muestreo, lo que aumenta el rango dinámico. Al alinear píxeles, es decir, convertir una posición en coordenadas de pantalla, el uso de cualquier otro método de redondeo producirá agujeros, huecos y otros artefactos.

Michaël Roy
fuente
"esto incrementa el rango dinámico" - parece un texto extra sin sentido; copiado y pegado desde algún lugar por error? Es posible que desee eliminarlo.
anatolyg
No. Disminuir el ruido de muestreo disminuye el ruido de fondo y eso de hecho aumenta el rango dinámico.
Michaël Roy
¿Podría proporcionar un ejemplo simple o una referencia para ilustrar el aumento del rango dinámico / disminución del ruido?
Ruslan
1
Con el redondeo de enteros estándar (falta de) redondeo, todos los valores entre cero y menos uno "desaparecen", o más bien cambian de signo (el mismo valor y para entradas de 0 a + 1), todos los valores negativos se compensan con al menos un bit. Aquí hay un poco de ruido con distorsión agregada.
Michaël Roy
4

Una razón simple podría ser que existen diferentes métodos para redondear números, por lo que, a menos que conozca el método utilizado, puede obtener resultados diferentes.

Con floor (), puede ser coherente con los resultados. Si el flotador es .5 o mayor, agregarlo aumentará al siguiente int. Pero .49999 simplemente eliminará el decimal.

MivaScott
fuente
+1 El punto de esta respuesta se basa en los propios comentarios sobre la pregunta, en los que hay desacuerdo sobre lo que round()hace.
Loduwijk
@Aaron Sin embargo, la respuesta es incorrecta sobre lo que floor(x + 0.5)hace.
EOF
¡Oh, ja! Bueno, entonces. Eso es irónico. "Utilice la X más conocida, ya que no estamos de acuerdo o no tenemos pleno conocimiento sobre Y". Entonces, ¿qué haces cuando lo mismo se aplica a X?
Loduwijk
1
@ Aaron Easy. Usted hace la única cosa sana y usa nearbyint(x), que usa un redondeo sensato (al par más cercano), siempre que no se haya metido con el entorno de punto flotante.
EOF
@EOF: Su elección de redondeo no es sensata. Una función de redondeo que no tiene período 1 en el componente no lineal es una locura.
Eric Towers
2

Muchos programadores adaptan modismos que aprendieron al programar con otros lenguajes. No todos los idiomas tienen una round()función, y en esos idiomas es normal usarlos floor(x + 0.5)como sustitutos. Cuando estos programadores comienzan a usar C ++, no siempre se dan cuenta de que hay unround() , continúan usando el estilo al que están acostumbrados.

En otras palabras, solo porque vea mucho código que hace algo, no significa que haya una buena razón para hacerlo. Puede encontrar ejemplos de esto en todos los lenguajes de programación. Recuerde la ley de Sturgeon :

el noventa por ciento de todo es basura

Barmar
fuente