Cada vez que necesito división, por ejemplo, comprobación de condición, me gustaría refactorizar la expresión de división en multiplicación, por ejemplo:
Versión original:
if(newValue / oldValue >= SOME_CONSTANT)
Nueva versión:
if(newValue >= oldValue * SOME_CONSTANT)
Porque creo que puede evitar:
División por cero
Desbordarse cuando
oldValue
es muy pequeño
¿Está bien? ¿Hay algún problema para este hábito?
coding-style
language-agnostic
math
ocomfd
fuente
fuente
oldValue >= 0
?Respuestas:
Dos casos comunes a considerar:
Aritmética de enteros
Obviamente, si está utilizando aritmética de enteros (que se trunca) obtendrá un resultado diferente. Aquí hay un pequeño ejemplo en C #:
Salida:
Aritmética de punto flotante
Además del hecho de que la división puede producir un resultado diferente cuando se divide por cero (genera una excepción, mientras que la multiplicación no), también puede generar errores de redondeo ligeramente diferentes y un resultado diferente. Ejemplo simple en C #:
Salida:
En caso de que no me creas, aquí hay un Fiddle que puedes ejecutar y ver por ti mismo.
Otros idiomas pueden ser diferentes; Sin embargo, tenga en cuenta que C #, como muchos lenguajes, implementa una biblioteca de punto flotante estándar IEEE (IEEE 754) , por lo que debería obtener los mismos resultados en otros tiempos de ejecución estandarizados.
Conclusión
Si está trabajando en zonas verdes , probablemente esté bien.
Si está trabajando en código heredado, y la aplicación es una aplicación financiera u otra aplicación sensible que realiza operaciones aritméticas y debe proporcionar resultados consistentes, tenga mucho cuidado al cambiar las operaciones. Si es necesario, asegúrese de tener pruebas unitarias que detecten cualquier cambio sutil en la aritmética.
Si solo está haciendo cosas como contar elementos en una matriz u otras funciones computacionales generales, probablemente estará bien. Sin embargo, no estoy seguro de que el método de multiplicación aclare su código.
Si está implementando un algoritmo para una especificación, no cambiaría nada en absoluto, no solo por el problema de los errores de redondeo, sino para que los desarrolladores puedan revisar el código y asignar cada expresión a la especificación para garantizar que no haya implementación defectos
fuente
Me gusta su pregunta, ya que potencialmente cubre muchas ideas. En general, sospecho que la respuesta es que depende , probablemente de los tipos involucrados y el posible rango de valores en su caso específico.
Mi instinto inicial es reflexionar sobre el estilo , es decir. su nueva versión es menos clara para el lector de su código. Me imagino que tendría que pensar por un segundo o dos (o tal vez más) para determinar la intención de su nueva versión, mientras que su versión anterior es inmediatamente clara. La legibilidad es un atributo importante del código, por lo que su nueva versión tiene un costo.
Tienes razón en que la nueva versión evita una división por cero. Ciertamente no es necesario agregar un protector (en la línea de
if (oldValue != 0)
). ¿Pero esto tiene sentido? Su versión anterior refleja una relación entre dos números. Si el divisor es cero, entonces su relación no está definida. Esto puede ser más significativo en su situación, es decir. no deberías producir un resultado en este caso.La protección contra el desbordamiento es discutible. Si sabes que
newValue
siempre es más grande queoldValue
, entonces quizás puedas hacer ese argumento. Sin embargo, puede haber casos en los(oldValue * SOME_CONSTANT)
que también se desborde. Entonces no veo mucha ganancia aquí.Puede haber un argumento de que obtienes un mejor rendimiento porque la multiplicación puede ser más rápida que la división (en algunos procesadores). Sin embargo, tendría que haber muchos cálculos como estos para obtener una ganancia significativa, es decir. cuidado con la optimización prematura.
Reflexionando sobre todo lo anterior, en general no creo que haya mucho que ganar con su nueva versión en comparación con la versión anterior, en particular dada la reducción en la claridad. Sin embargo, puede haber casos específicos en los que haya algún beneficio.
fuente
No.
Probablemente llamaría a eso optimización prematura , en un sentido amplio, independientemente de si está optimizando para el rendimiento , como generalmente se refiere a la frase, o cualquier otra cosa que pueda optimizarse, como conteo de bordes , líneas de código o aún más ampliamente, cosas como "diseño".
Implementar ese tipo de optimización como un procedimiento operativo estándar pone en riesgo la semántica de su código y potencialmente oculta los bordes. De todos modos, es posible que sea necesario abordar explícitamente los casos límite que considere adecuados para eliminar en silencio . Y, es infinitamente más fácil depurar problemas alrededor de bordes ruidosos (aquellos que arrojan excepciones) sobre aquellos que fallan silenciosamente.
Y, en algunos casos, es incluso ventajoso "des-optimizar" en aras de la legibilidad, la claridad o la explicidad. En la mayoría de los casos, sus usuarios no notarán que ha guardado algunas líneas de código o ciclos de CPU para evitar el manejo de casos extremos o el manejo de excepciones. Torpe o no en silencio código, por el contrario, va a afectar a la gente - sus compañeros de trabajo como mínimo. (Y también, por lo tanto, el costo de construir y mantener el software).
El valor predeterminado es el que sea más "natural" y legible con respecto al dominio de la aplicación y el problema específico. Mantenlo simple, explícito e idiomático. Optimice según sea necesario para obtener ganancias significativas o para lograr un umbral de usabilidad legítimo.
También tenga en cuenta: los compiladores a menudo optimizan la división para usted de todos modos, cuando es seguro hacerlo.
fuente
Utilice el que tenga menos errores y tenga más sentido lógico.
Por lo general , la división por una variable es una mala idea de todos modos, ya que generalmente el divisor puede ser cero.
La división por una constante generalmente depende de cuál es el significado lógico.
Aquí hay algunos ejemplos para mostrar que depende de la situación:
División buena:
Multiplicación mala:
Multiplicación buena:
División mala:
Multiplicación buena:
División mala:
fuente
(ptr2 - ptr1) * 3 >= n
tan fácil de entender como la expresiónptr2 - ptr1 >= n / 3
? ¿No hace que su cerebro se tropiece y vuelva a intentar tratar de descifrar el significado de triplicar la diferencia entre dos punteros? Si es realmente obvio para usted y su equipo, supongo que tendrá más poder; Debo estar en la minoría lenta.n
y un número arbitrario 3 son confusos en ambos casos pero, reemplazados por nombres razonables, no, no encuentro ninguno más confuso que el otro.Hacer algo "siempre que sea posible" rara vez es una buena idea.
Su prioridad número uno debe ser la corrección, seguida de legibilidad y facilidad de mantenimiento. Reemplazar ciegamente la división con la multiplicación siempre que sea posible a menudo fallará en el departamento de corrección, a veces solo en casos raros y, por lo tanto, difíciles de encontrar.
Haz lo correcto y lo más legible. Si tiene evidencia sólida de que escribir código de la manera más fácil de leer causa un problema de rendimiento, entonces puede considerar cambiarlo. El cuidado, las matemáticas y las revisiones de códigos son tus amigos.
fuente
Con respecto a la legibilidad del código, creo que la multiplicación es realmente más legible en algunos casos. Por ejemplo, si hay algo que debe verificar si
newValue
ha aumentado un 5 por ciento o más por encimaoldValue
, entonces1.05 * oldValue
hay un umbral contra el cual evaluarnewValue
, y es natural escribirPero tenga cuidado con los números negativos cuando refactorice las cosas de esta manera (ya sea reemplazando la división con multiplicación o reemplazando la multiplicación con división). Las dos condiciones que consideró son equivalentes si
oldValue
se garantiza que no sean negativas; pero supongamosnewValue
que en realidad es -13.5 yoldValue
es -10.1. Entoncesse evalúa como verdadero , pero
se evalúa como falsa .
fuente
Tenga en cuenta la famosa división de papel por enteros invariantes usando multiplicación .
El compilador en realidad está haciendo multiplicación, si el entero es invariante. No es una division. Esto sucede incluso para no poder de 2 valores. La potencia de 2 divisiones usa obviamente cambios de bit y, por lo tanto, son aún más rápidos.
Sin embargo, para enteros no invariantes, es su responsabilidad optimizar el código. Asegúrese antes de optimizar que realmente está optimizando un cuello de botella genuino, y que la corrección no se sacrifica. Cuidado con el desbordamiento de enteros.
Me importa la microoptimización, por lo que probablemente eche un vistazo a las posibilidades de optimización.
Piense también en las arquitecturas en las que se ejecuta su código. Especialmente ARM tiene una división extremadamente lenta; necesita llamar a una función para dividir, no hay instrucción de división en ARM.
Además, en arquitecturas de 32 bits, la división de 64 bits no está optimizada, como descubrí .
fuente
Retomando su punto 2, de hecho, evitará el desbordamiento de un muy pequeño
oldValue
. Sin embargo, siSOME_CONSTANT
también es muy pequeño, su método alternativo terminará con un flujo inferior, donde el valor no puede representarse con precisión.Y a la inversa, ¿qué sucede si
oldValue
es muy grande? Tienes los mismos problemas, todo lo contrario.Si se quiere evitar (o minimizar) el riesgo de desbordamiento / subdesbordamiento, la mejor manera es comprobar si
newValue
es más cercano en magnitud aoldValue
oSOME_CONSTANT
. Luego puede elegir la operación de división adecuada, ya seao
y el resultado será más exacto.
Para dividir por cero, en mi experiencia, esto casi nunca es apropiado para ser "resuelto" en las matemáticas. Si tiene una división por cero en sus comprobaciones continuas, entonces es casi seguro que tiene una situación que requiere un análisis y cualquier cálculo basado en estos datos no tiene sentido. Una verificación explícita de dividir por cero es casi siempre el movimiento apropiado. (Tenga en cuenta que sí digo "casi" aquí, porque no pretendo ser infalible. Solo notaré que no recuerdo haber visto una buena razón para esto en 20 años de escribir software incorporado, y seguir adelante .)
Sin embargo, si tiene un riesgo real de desbordamiento / subflujo en su aplicación, probablemente esta no sea la solución correcta. Lo más probable es que, por lo general, deba verificar la estabilidad numérica de su algoritmo, o tal vez simplemente pasar a una representación de mayor precisión.
Y si no tiene un riesgo comprobado de desbordamiento / subflujo, entonces no se preocupa por nada. Eso significa que, literalmente, debe demostrar que lo necesita, con números, en los comentarios al lado del código que explican al mantenedor por qué es necesario. Como ingeniero principal que revisa el código de otras personas, si me encuentro con alguien que esté haciendo un esfuerzo adicional por esto, personalmente no aceptaría nada menos. Esto es algo opuesto a la optimización prematura, pero generalmente tendría la misma causa raíz: la obsesión por los detalles que no hace una diferencia funcional.
fuente
Encapsula la aritmética condicional en métodos y propiedades significativos. No sólo buena de nombres que decir lo "A / B" medios , la comprobación de parámetros y control de errores perfectamente pueden ocultar allí también.
Es importante destacar que, dado que estos métodos se componen de una lógica más compleja, la complejidad extrínseca sigue siendo muy manejable.
Yo diría que la sustitución de multiplicación parece una solución razonable porque el problema está mal definido.
fuente
Creo que no podría ser una buena idea reemplazar las multiplicaciones con divisiones porque la ALU (Unidad Aritmética-Lógica) de la CPU ejecuta algoritmos, aunque están implementados en hardware. Técnicas más sofisticadas están disponibles en procesadores más nuevos. En general, los procesadores se esfuerzan por paralelizar las operaciones de pares de bits para minimizar los ciclos de reloj necesarios. Los algoritmos de multiplicación se pueden paralelizar de manera bastante efectiva (aunque se requieren más transistores). Los algoritmos de división no se pueden paralelizar tan eficientemente. Los algoritmos de división más eficientes son bastante complejos. En general, requieren más ciclos de reloj por bit.
fuente