¿Alguna vez trató de resumir todos los números del 1 al 2,000,000 en su lenguaje de programación favorito? El resultado es fácil de calcular manualmente: 2,000,001,000,000, que es aproximadamente 900 veces mayor que el valor máximo de un entero de 32 bits sin signo.
C # imprime -1453759936
: ¡un valor negativo! Y supongo que Java hace lo mismo.
Eso significa que hay algunos lenguajes de programación comunes que ignoran el desbordamiento aritmético de forma predeterminada (en C #, hay opciones ocultas para cambiar eso). Ese es un comportamiento que me parece muy arriesgado, y ¿no fue el accidente de Ariane 5 causado por tal desbordamiento?
Entonces: ¿cuáles son las decisiones de diseño detrás de un comportamiento tan peligroso?
Editar:
Las primeras respuestas a esta pregunta expresan los costos excesivos de la verificación. Ejecutemos un breve programa de C # para probar esta suposición:
Stopwatch watch = Stopwatch.StartNew();
checked
{
for (int i = 0; i < 200000; i++)
{
int sum = 0;
for (int j = 1; j < 50000; j++)
{
sum += j;
}
}
}
watch.Stop();
Console.WriteLine(watch.Elapsed.TotalMilliseconds);
En mi máquina, la versión comprobada tarda 11015 ms, mientras que la versión no comprobada tarda 4125 ms. Es decir, los pasos de verificación toman casi el doble de tiempo que sumar los números (en total 3 veces el tiempo original). Pero con las 10,000,000,000 de repeticiones, el tiempo que demora un cheque sigue siendo inferior a 1 nanosegundo. Puede haber una situación en la que eso sea importante, pero para la mayoría de las aplicaciones, eso no importará.
Edición 2:
Volví a compilar nuestra aplicación de servidor (un servicio de Windows que analiza los datos recibidos de varios sensores, involucrando bastantes cálculos numéricos) /p:CheckForOverflowUnderflow="false"
parámetro (normalmente, enciendo la verificación de desbordamiento) y lo implementé en un dispositivo. El monitoreo de Nagios muestra que la carga promedio de CPU se mantuvo en 17%.
Esto significa que el impacto en el rendimiento que se encuentra en el ejemplo anterior es totalmente irrelevante para nuestra aplicación.
fuente
checked { }
sección para marcar las partes del código que deben realizar comprobaciones de desbordamiento aritmético. Esto se debe al rendimiento(1..2_000_000).sum #=> 2000001000000
. Otra de mis favoritas idiomas:sum [1 .. 2000000] --=> 2000001000000
. No es mi favorito:Array.from({length: 2000001}, (v, k) => k).reduce((acc, el) => acc + el) //=> 2000001000000
. (Para ser justos, el último es hacer trampa.)Integer
en Haskell es de precisión arbitraria, contendrá cualquier número siempre que no se quede sin RAM asignable.But with the 10,000,000,000 repetitions, the time taken by a check is still less than 1 nanosecond.
eso es una indicación del bucle que se está optimizando. También esa oración contradice números anteriores que me parecen muy válidos.Respuestas:
Hay 3 razones para esto:
El costo de verificar los desbordamientos (para cada operación aritmética) en tiempo de ejecución es excesivo.
La complejidad de probar que una verificación de desbordamiento puede omitirse en tiempo de compilación es excesiva.
En algunos casos (p. Ej., Cálculos de CRC, bibliotecas de números grandes, etc.) "envolver en desbordamiento" es más conveniente para los programadores.
fuente
unsigned int
no debería venir a la mente porque un lenguaje con comprobación de desbordamiento debería verificar todos los tipos enteros de forma predeterminada. Deberías tener que escribirwrapping unsigned int
.didOverflow()
función en línea simple o incluso una variable global__carry
que permita el acceso al indicador de acarreo costaría cero tiempo de CPU si no la usa.ADD
, no establece el acarreo (lo que necesitaADDS
). Itanium ni siquiera tiene una bandera de acarreo. E incluso en x86, AVX no tiene banderas de transporte.unchecked
es bastante fácil; pero puede estar sobreestimando la frecuencia con la que el desbordamiento es importante.adds
tienen el mismo precio queadd
(es solo un indicador de instrucción de 1 bit que selecciona si se actualiza el indicador de acarreo). Lasadd
instrucciones de MIPS quedan atrapadas en el desbordamiento . ¡En su lugar, debe solicitar no atrapar el desbordamientoaddu
!¿Quién dice que es una mala compensación?
Ejecuto todas mis aplicaciones de producción con la comprobación de desbordamiento habilitada. Esta es una opción de compilador de C #. De hecho, comparé esto y no pude determinar la diferencia. El costo de acceder a la base de datos para generar HTML (que no sea de juguete) eclipsa los costos de comprobación de desbordamiento.
Aprecio el hecho de que sé que ninguna operación se desborda en la producción. Casi todo el código se comportaría de manera errática en presencia de desbordamientos. Los errores no serían benignos. La corrupción de los datos es probable, los problemas de seguridad son una posibilidad.
En caso de que necesite el rendimiento, que a veces es el caso, desactivo la comprobación de desbordamiento utilizando
unchecked {}
de forma granular. Cuando quiero decir que confío en una operación que no se desborda, podría agregar redundantementechecked {}
al código para documentar ese hecho. Soy consciente de los desbordamientos, pero no necesariamente tengo que estarlo gracias a la comprobación.Creo que el equipo de C # tomó la decisión equivocada cuando eligieron no verificar el desbordamiento de forma predeterminada, pero esa opción ahora está sellada debido a fuertes preocupaciones de compatibilidad. Tenga en cuenta que esta elección se realizó alrededor del año 2000. El hardware era menos capaz y .NET todavía no tenía mucha tracción. Tal vez .NET quería atraer a los programadores Java y C / C ++ de esta manera. .NET también está diseñado para poder estar cerca del metal. Es por eso que tiene código inseguro, estructuras y excelentes capacidades de llamadas nativas que Java no tiene.
Cuanto más rápido se vuelve nuestro hardware y los compiladores más inteligentes obtienen la comprobación de desbordamiento más atractiva por defecto.
También creo que la comprobación de desbordamiento suele ser mejor que los números de tamaño infinito. Los números de tamaño infinito tienen un costo de rendimiento que es aún mayor, más difícil de optimizar (creo) y abren la posibilidad de un consumo ilimitado de recursos.
La forma en que JavaScript trata el desbordamiento es aún peor. Los números de JavaScript son dobles de coma flotante. Un "desbordamiento" se manifiesta como un conjunto de enteros totalmente preciso. Ocurrirán resultados ligeramente incorrectos (como estar apagado por uno, esto puede convertir bucles finitos en infinitos).
Para algunos lenguajes como C / C ++, la comprobación de desbordamiento por defecto es claramente inapropiada porque los tipos de aplicaciones que se escriben en estos lenguajes necesitan un rendimiento básico. Aún así, hay esfuerzos para convertir C / C ++ en un lenguaje más seguro al permitir optar por un modo más seguro. Esto es recomendable ya que el 90-99% del código tiende a estar frío. Un ejemplo es la
fwrapv
opción del compilador que fuerza el ajuste del complemento de 2. Esta es una característica de "calidad de implementación" del compilador, no del lenguaje.Haskell no tiene una pila de llamadas lógicas ni un orden de evaluación especificado. Esto hace que ocurran excepciones en puntos impredecibles. En
a + b
queda sin especificar sia
ob
se evalúan primero y si esas expresiones terminan en absoluto o no. Por lo tanto, tiene sentido que Haskell use enteros ilimitados la mayor parte del tiempo. Esta opción es adecuada para un lenguaje puramente funcional porque las excepciones son realmente inapropiadas en la mayoría del código Haskell. Y la división por cero es de hecho un punto problemático en el diseño del lenguaje Haskells. En lugar de enteros ilimitados, también podrían haber usado enteros de ajuste de ancho fijo, pero eso no encaja con el tema "centrarse en la corrección" que presenta el lenguaje.Una alternativa a las excepciones de desbordamiento son los valores tóxicos creados por operaciones indefinidas y propagados a través de operaciones (como el
NaN
valor flotante ). Eso parece mucho más costoso que la verificación de desbordamiento y hace que todas las operaciones sean más lentas, no solo las que pueden fallar (salvo la aceleración de hardware que los flotadores comúnmente tienen y los ints comúnmente no tienen, aunque Itanium tiene NaT que no es "una cosa" ). Tampoco veo el punto de hacer que el programa continúe cojeando junto con datos erróneos. Es comoON ERROR RESUME NEXT
. Oculta errores pero no ayuda a obtener resultados correctos. Supercat señala que a veces es una optimización del rendimiento hacer esto.fuente
unsigned
solo para enteros. El comportamiento del desbordamiento de enteros con signo es en realidad un comportamiento indefinido en C y C ++. Sí, comportamiento indefinido . Sucede que casi todos lo implementan como desbordamiento del complemento de 2. C # en realidad lo hace oficial, en lugar de dejarlo UB como C / C ++gcc -O2
forx + 1 > x
(dondex
es unint
). Ver también gcc.gnu.org/onlinedocs/gcc-6.3.0/gcc/… . El comportamiento del complemento 2s en el desbordamiento firmado en C es opcional , incluso en compiladores reales, y porgcc
defecto lo ignora en los niveles normales de optimización.Porque es una mala compensación para hacer todos los cálculos mucho más caro con el fin de captar automáticamente el raro caso de que un desbordamiento hace aparecer. Es mucho mejor cargar al programador con el reconocimiento de los casos raros en los que esto es un problema y agregar prevenciones especiales que hacer que todos los programadores paguen el precio por la funcionalidad que no usan.
fuente
"No obligue a los usuarios a pagar una penalización de rendimiento por una función que no necesiten".
Es uno de los principios más básicos en el diseño de C y C ++, y se origina en un momento diferente cuando tenía que pasar por contorsiones ridículas para obtener un rendimiento apenas adecuado para las tareas que hoy se consideran triviales.
Los idiomas más nuevos rompen con esta actitud para muchas otras características, como la comprobación de límites de matriz. No estoy seguro de por qué no lo hicieron para la verificación de desbordamiento; Podría ser simplemente un descuido.
fuente
checked
yunchecked
agregaron sintaxis para cambiar entre ellos localmente y también modificadores de línea de comando (y configuraciones de proyecto en VS) para cambiarlo globalmente. Es posible que nounchecked
esté de acuerdo con la configuración predeterminada (lo hago), pero todo esto es claramente muy deliberado.Legado
Yo diría que el problema probablemente esté enraizado en el legado. Cía:
Esto se hizo para obtener el mejor rendimiento posible, siguiendo el principio de que el programador sabe lo que está haciendo .
Conduce a Statu-Quo
El hecho de que C (y por extensión C ++) no requiera la detección de desbordamiento por turnos significa que la comprobación de desbordamiento es lenta.
El hardware abastece principalmente a C / C ++ (en serio, x86 tiene una
strcmp
instrucción (también conocida como PCMPISTRI a partir de SSE 4.2)), y dado que a C no le importa, las CPU comunes no ofrecen formas eficientes de detectar desbordamientos. En x86, debe verificar un indicador por núcleo después de cada operación potencialmente desbordante; cuando lo que realmente desea es una bandera "contaminada" en el resultado (al igual que los propagadores de NaN). Y las operaciones vectoriales pueden ser aún más problemáticas. Algunos nuevos jugadores pueden aparecer en el mercado con un manejo eficiente de desbordamiento; pero por ahora a x86 y ARM no les importa.Los optimizadores del compilador no son buenos para optimizar las comprobaciones de desbordamiento, o incluso para optimizar en presencia de desbordamientos. Algunos académicos como John Regher se quejan de este statu quo , pero el hecho es que cuando el simple hecho de hacer "desbordamientos" de desbordamientos impide optimizaciones incluso antes de que el ensamblaje llegue a la CPU puede ser paralizante. Especialmente cuando evita la auto-vectorización ...
Con efectos en cascada
Por lo tanto, en ausencia de estrategias de optimización eficientes y soporte de CPU eficiente, la verificación de desbordamiento es costosa. Mucho más costoso que envolver.
Agregue un comportamiento molesto, como
x + y - 1
puede desbordarse cuandox - 1 + y
no lo hace, lo que puede molestar legítimamente a los usuarios, y la verificación de desbordamiento generalmente se descarta a favor del ajuste (que maneja este ejemplo y muchos otros con gracia).Aún así, no toda la esperanza está perdida
Se ha realizado un esfuerzo en los compiladores de clang y gcc para implementar "desinfectantes": formas de instrumentar los binarios para detectar casos de comportamiento indefinido. Cuando se usa
-fsanitize=undefined
, se detecta un desbordamiento firmado y aborta el programa; Muy útil durante las pruebas.El lenguaje de programación Rust tiene habilitada la verificación de desbordamiento de forma predeterminada en el modo de depuración (utiliza la aritmética de ajuste en el modo Release por razones de rendimiento).
Por lo tanto, existe una creciente preocupación por la verificación de desbordamiento y los peligros de que los resultados falsos no se detecten, y con suerte esto despertará el interés en la comunidad de investigación, la comunidad de compiladores y la comunidad de hardware.
fuente
jo
y Más efectos globales de la contaminación que agregan al estado predictivo de la rama y el aumento del tamaño del código. Si esa bandera fuera pegajosa, ofrecería un potencial real ... y aún así no puede hacerlo correctamente en código vectorizado.1..100
tipos de Pascal-ish en su lugar: sea explícito sobre los rangos esperados, en lugar de ser "forzado" a 2 ^ 31, etc. Algunos idiomas ofrecen esto, por supuesto, y tienden a hacer una verificación de desbordamiento por defecto (a veces en tiempo de compilación, incluso).x * 2 - 2
puede desbordarse cuandox
sea 51 a pesar de que el resultado se ajuste, lo que le obligará a reorganizar su cálculo (a veces de manera no natural). En mi experiencia, descubrí que generalmente prefiero ejecutar el cálculo en un tipo más grande y luego verificar si el resultado se ajusta o no.x = x * 2 - 2
debería funcionar para todosx
donde la asignación da como resultado un 1 válido. .100 número). Es decir, las operaciones en el tipo numérico pueden tener una precisión más alta que el tipo en sí, siempre y cuando la asignación se ajuste. Esto sería bastante útil en casos como(a + b) / 2
ignorar desbordamientos (sin signo) puede ser la opción correcta.Los lenguajes que intentan detectar desbordamientos han definido históricamente la semántica asociada de formas que restringieron severamente lo que de otro modo habrían sido optimizaciones útiles. Entre otras cosas, si bien a menudo será útil realizar cálculos en una secuencia diferente de la especificada en el código, la mayoría de los lenguajes que atrapan desbordamientos garantizan ese código dado como:
si el valor inicial de x causaría un desbordamiento en la 47ª pasada a través del ciclo, la Operación1 se ejecutará 47 veces y la Operación2 ejecutará 46. En ausencia de tal garantía, si nada más dentro del ciclo usa x, y nada usará el valor de x después de una excepción lanzada por Operation1 u Operation2, el código podría reemplazarse con:
Desafortunadamente, realizar tales optimizaciones al tiempo que garantiza una semántica correcta en los casos en que se hubiera producido un desbordamiento dentro del ciclo es difícil, esencialmente requiere algo como:
Si se considera que una gran cantidad de código del mundo real usa bucles que están más involucrados, será obvio que optimizar el código mientras se preserva la semántica de desbordamiento es difícil. Además, debido a problemas de almacenamiento en caché, es completamente posible que el aumento en el tamaño del código haga que el programa general se ejecute más lentamente, a pesar de que hay menos operaciones en la ruta de ejecución común.
Lo que se necesitaría para que la detección de desbordamiento sea económica sería un conjunto definido de semánticas de detección de desbordamiento más flexibles, lo que facilitaría que el código informara si se realizó un cálculo sin desbordamientos que pudieran haber afectado los resultados (*), pero sin sobrecargarlo el compilador con detalles más allá de eso. Si una especificación de idioma se enfocara en reducir el costo de la detección de desbordamiento al mínimo necesario para lograr lo anterior, podría hacerse mucho menos costoso de lo que es en los idiomas existentes. Sin embargo, no conozco ningún esfuerzo para facilitar la detección eficiente de desbordamiento.
(*) Si un idioma promete que se informarán todos los desbordamientos,
x*y/y
no se puede simplificar una expresión como ax
menos quex*y
se pueda garantizar que no se desborde. Del mismo modo, incluso si se ignorara el resultado de un cálculo, un lenguaje que prometa informar todos los desbordamientos deberá realizarlo de todos modos para que pueda realizar la verificación de desbordamiento. Dado que el desbordamiento en tales casos no puede dar lugar a un comportamiento aritméticamente incorrecto, un programa no necesitaría realizar tales comprobaciones para garantizar que ningún desbordamiento haya causado resultados potencialmente inexactos.Por cierto, los desbordamientos en C son especialmente malos. Aunque casi todas las plataformas de hardware que admiten C99 utilizan una semántica envolvente silenciosa de dos complementos, está de moda que los compiladores modernos generen código que puede causar efectos secundarios arbitrarios en caso de desbordamiento. Por ejemplo, dado algo como:
GCC generará código para test2 que incrementa incondicionalmente (* p) una vez y devuelve 32768 independientemente del valor pasado a q. Por su razonamiento, el cálculo de (32769 * 65535) y 65535u causaría un desbordamiento y, por lo tanto, no es necesario que el compilador considere ningún caso en el que (q | 32768) arroje un valor mayor que 32768. Aunque no hay Debido a que el cálculo de (32769 * 65535) y 65535u debe preocuparse por los bits superiores del resultado, gcc utilizará un desbordamiento firmado como justificación para ignorar el bucle.
fuente
-fwrapv
da como resultado un comportamiento definido, aunque no el comportamiento que el interlocutor desea. Por supuesto, la optimización de gcc convierte cualquier tipo de desarrollo de C en un examen exhaustivo del comportamiento estándar y del compilador.x+y > z
de una manera que nunca hará otra cosa que no sea el rendimiento 0 o el rendimiento 1, pero cualquiera de los resultados sería igualmente aceptable en caso de desbordamiento, un compilador que ofrece esa garantía a menudo podría generar un mejor código para expresiónx+y > z
que cualquier compilador podría generar para una versión de la expresión escrita a la defensiva. Hablando de manera realista, ¿qué fracción de optimizaciones útiles relacionadas con el desbordamiento se vería impedida por una garantía de que los cálculos enteros distintos de la división / resto se ejecutarán sin efectos secundarios?-fwhatever-makes-sense
parche", me sugiere fuertemente que hay más a eso de fantasía por su parte. Los argumentos habituales que he escuchado son que la alineación de código (e incluso la expansión de macros) se benefician de deducir tanto como sea posible sobre el uso específico de una construcción de código, ya que cualquiera de las dos cosas comúnmente da como resultado un código insertado que trata casos que no necesita a, que el código circundante "prueba" imposible.foo(i + INT_MAX + 1)
, los escritores de compiladores están interesados en aplicar optimizaciones alfoo()
código en línea que se basan en la corrección de su argumento como no negativo (trucos diabólicos divmod, quizás). Bajo sus restricciones adicionales, solo podrían aplicar optimizaciones cuyo comportamiento para entradas negativas tenga sentido para la plataforma. Por supuesto, personalmente me alegraría que esa sea una-f
opción que se enciende-fwrapv
, etc., y probablemente deba desactivar algunas optimizaciones para las que no hay bandera. Pero no es que me moleste en hacer todo ese trabajo yo mismo.No todos los lenguajes de programación ignoran los desbordamientos de enteros. Algunos idiomas proporcionan operaciones enteras seguras para todos los números (la mayoría de los dialectos de Lisp, Ruby, Smalltalk, ...) y otros a través de bibliotecas; por ejemplo, hay varias clases BigInt para C ++.
Si un lenguaje hace que el entero esté a salvo del desbordamiento de forma predeterminada o no, depende de su propósito: los lenguajes del sistema como C y C ++ necesitan proporcionar abstracciones de costo cero y el "entero grande" no es uno. Los lenguajes de productividad, como Ruby, pueden proporcionar grandes números enteros listos para usar. Los lenguajes como Java y C # que están en algún punto intermedio deberían, en mi humilde opinión, ir con los enteros seguros listos para usar, por no decirlo.
fuente
Como ha demostrado, C # habría sido 3 veces más lento si tuviera habilitadas las comprobaciones de desbordamiento de forma predeterminada (suponiendo que su ejemplo sea una aplicación típica para ese idioma). Estoy de acuerdo en que el rendimiento no siempre es la característica más importante, pero los lenguajes / compiladores generalmente se comparan con su rendimiento en tareas típicas. Esto se debe en parte al hecho de que la calidad de las características del lenguaje es algo subjetiva, mientras que una prueba de rendimiento es objetiva.
Si tuviera que introducir un nuevo lenguaje que sea similar a C # en la mayoría de los aspectos pero 3 veces más lento, obtener una cuota de mercado no sería fácil, incluso si al final la mayoría de los usuarios finales se beneficiarían de las comprobaciones de desbordamiento más de lo que lo harían de mayor rendimiento.
fuente
Más allá de las muchas respuestas que justifican la falta de comprobación de desbordamiento en función del rendimiento, hay dos tipos diferentes de aritmética a tener en cuenta:
cálculos de indexación (indexación de matriz y / o aritmética de puntero)
otra aritmética
Si el lenguaje usa un tamaño entero que es el mismo que el tamaño del puntero, entonces un programa bien construido no se desbordará haciendo cálculos de indexación porque necesariamente tendría que quedarse sin memoria antes de que los cálculos de indexación causen desbordamiento.
Por lo tanto, verificar las asignaciones de memoria es suficiente cuando se trabaja con aritmética de puntero y expresiones de indexación que involucran estructuras de datos asignadas. Por ejemplo, si tiene un espacio de direcciones de 32 bits y usa números enteros de 32 bits y permite asignar un máximo de 2 GB de almacenamiento dinámico (aproximadamente la mitad del espacio de direcciones), los cálculos de indexación / puntero (básicamente) no se desbordarán.
Además, es posible que se sorprenda de cuánto de la suma / resta / multiplicación implica la indexación de matriz o el cálculo del puntero, por lo tanto, cae en la primera categoría. El puntero de objeto, el acceso a campo y las manipulaciones de matriz son operaciones de indexación, ¡y muchos programas no hacen más cálculos aritméticos que estos! Esencialmente, esta es la razón principal por la que los programas funcionan tan bien como lo hacen sin verificación de desbordamiento de enteros.
Todos los cálculos sin indexación y sin puntero deben clasificarse como aquellos que desean / esperan un desbordamiento (por ejemplo, cálculos de hashing) y aquellos que no lo hacen (por ejemplo, su ejemplo de suma).
En el último caso, los programadores a menudo usarán tipos de datos alternativos, como
double
o algunosBigInt
. Muchos cálculos requieren undecimal
tipo de datos en lugar dedouble
, por ejemplo, cálculos financieros. Si no lo hacen y se adhieren a los tipos enteros, deben tener cuidado de verificar el desbordamiento de enteros, o de lo contrario, sí, el programa puede alcanzar una condición de error no detectada como usted señala.Como programadores, debemos ser sensibles a nuestras elecciones en los tipos de datos numéricos y sus consecuencias en términos de posibilidades de desbordamiento, sin mencionar la precisión. En general (y especialmente cuando trabajamos con la familia de lenguajes C con el deseo de usar los tipos enteros rápidos) debemos ser sensibles y conscientes de las diferencias entre los cálculos de indexación frente a otros.
fuente
El lenguaje Rust proporciona un compromiso interesante entre la comprobación de desbordamientos y no, al agregar las comprobaciones para la compilación de depuración y eliminarlas en la versión de lanzamiento optimizada. Esto le permite encontrar los errores durante las pruebas, mientras obtiene el rendimiento completo en la versión final.
Debido a que el entorno de desbordamiento a veces es un comportamiento deseado, también hay versiones de los operadores que nunca comprueban el desbordamiento.
Puede leer más sobre el razonamiento detrás de la elección en el RFC para el cambio. También hay mucha información interesante en esta publicación de blog , incluida una lista de errores que esta función ha ayudado a detectar.
fuente
checked_mul
, que comprueba si se ha producido un desbordamiento y devuelveNone
si es así, de loSome
contrario. Esto se puede usar tanto en producción como en modo de depuración: doc.rust-lang.org/std/primitive.i32.html#examples-15En Swift, cualquier desbordamiento de entero se detecta por defecto y detiene instantáneamente el programa. En los casos en que necesite un comportamiento envolvente, existen diferentes operadores & +, & - y & * que lo logran. Y hay funciones que realizan una operación y dicen si hubo un desbordamiento o no.
Es divertido ver a los principiantes intentar evaluar la secuencia de Collatz y tener su código bloqueado :-)
Ahora, los diseñadores de Swift también son los diseñadores de LLVM y Clang, por lo que saben un par o dos sobre la optimización y son bastante capaces de evitar verificaciones innecesarias de desbordamiento. Con todas las optimizaciones habilitadas, la comprobación de desbordamiento no agrega mucho al tamaño del código y al tiempo de ejecución. Y dado que la mayoría de los desbordamientos conducen a resultados absolutamente incorrectos, el tamaño del código y el tiempo de ejecución bien empleado.
PD. En C, C ++, el desbordamiento aritmético de enteros con signo de Objective-C es un comportamiento indefinido. Eso significa que todo lo que hace el compilador en el caso de desbordamiento de entero con signo es correcto, por definición. Las formas típicas de hacer frente al desbordamiento de enteros con signo es ignorarlo, tomando cualquier resultado que la CPU le dé, construyendo suposiciones en el compilador de que tal desbordamiento nunca sucederá (y concluya, por ejemplo, que n + 1> n siempre es cierto, ya que el desbordamiento es se supone que nunca sucederá), y una posibilidad que rara vez se usa es verificar y bloquearse si ocurre un desbordamiento, como lo hace Swift.
fuente
x+1>x
como incondicionalmente verdadero no requeriría que un compilador haga "suposiciones" sobre x si un compilador puede evaluar expresiones enteras usando tipos arbitrarios más grandes como sea conveniente (o comportarse como si lo estuviera haciendo). Un ejemplo más desagradable de "supuestos" basados en el desbordamiento sería decidir queuint32_t mul(uint16_t x, uint16_t y) { return x*y & 65535u; }
un compilador dado puede usarsum += mul(65535, x)
para decidir quex
no puede ser mayor que 32768 [comportamiento que probablemente sorprendería a las personas que escribieron el Fundamento C89, lo que sugiere que uno de los factores decisivos. ..unsigned short
promover ensigned int
el hecho de que en complemento a dos implementaciones silencio-envolventes (es decir, la mayoría de las implementaciones de C entonces en uso) trataría de código como el de arriba de la misma manera ya seaunsigned short
promovido aint
ounsigned
. El estándar no requería implementaciones en hardware de complemento silencioso para dos para tratar el código de la manera más acertada, pero los autores del estándar parecen haber esperado que lo hicieran de todos modos.En realidad, la verdadera causa de esto es puramente técnica / histórica: el signo de ignorar de la CPU en su mayor parte. Por lo general, solo hay una sola instrucción para agregar dos enteros en los registros, y a la CPU no le importa un poco si interpreta estos dos enteros como con signo o sin signo. Lo mismo vale para la resta, e incluso para la multiplicación. La única operación aritmética que necesita ser consciente de signos es la división.
La razón por la cual esto funciona es la representación complementaria de 2 de enteros con signo que es utilizada por prácticamente todas las CPU. Por ejemplo, en el complemento 2 de 4 bits, la adición de 5 y -3 se ve así:
Observe cómo el comportamiento envolvente de tirar la broca produce el resultado firmado correcto. Del mismo modo, las CPU generalmente implementan la resta
x - y
comox + ~y + 1
:Esto implementa la resta como una adición en el hardware, ajustando solo las entradas a la unidad lógica-aritmética (ALU) de manera trivial. ¿Qué podría ser más simple?
Como la multiplicación no es más que una secuencia de adiciones, se comporta de una manera similarmente agradable. El resultado de usar la representación del complemento de 2 e ignorar la realización de operaciones aritméticas es un circuito simplificado y conjuntos de instrucciones simplificados.
Obviamente, dado que C fue diseñado para trabajar cerca del metal, adoptó exactamente el mismo comportamiento que el comportamiento estandarizado de la aritmética sin signo, permitiendo que solo la aritmética con signo produzca un comportamiento indefinido. Y esa elección se trasladó a otros lenguajes como Java y, obviamente, C #.
fuente
x==INT_MAX
, entoncesx+1
podría comportarse arbitrariamente como +2147483648 o -2147483648 en el compilador conveniencia), pero ...x
yy
sonuint16_t
y el código en un sistema de 32 bits calculax*y & 65535u
cuandoy
es 65535, no se llegará a un compilador debe asumir que el código cuandox
es mayor que 32768.Algunas respuestas han discutido el costo de la verificación, y usted ha editado su respuesta para disputar que esta es una justificación razonable. Trataré de abordar esos puntos.
En C y C ++ (como ejemplos), uno de los principios de diseño de los lenguajes es no proporcionar una funcionalidad que no se solicitó. Esto se resume comúnmente en la frase "no pague por lo que no usa". Si el programador desea verificar el desbordamiento, puede solicitarlo (y pagar la multa). Esto hace que el lenguaje sea más peligroso de usar, pero usted elige trabajar con el idioma sabiendo eso, por lo que acepta el riesgo. Si no desea ese riesgo, o si está escribiendo un código donde la seguridad es de rendimiento primordial, puede seleccionar un lenguaje más apropiado donde el equilibrio entre el rendimiento y el riesgo sea diferente.
Hay algunas cosas mal con este razonamiento:
Esto es específico del entorno. Por lo general, tiene muy poco sentido citar cifras específicas como esta, porque el código está escrito para todo tipo de entornos que varían en orden de magnitud en términos de su rendimiento. Su 1 nanosegundo en una máquina de escritorio (supongo) puede parecer increíblemente rápido para alguien que codifica para un entorno incrustado, e insoportablemente lento para alguien que codifica para un súper grupo de computadoras.
1 nanosegundo puede parecer nada para un segmento de código que se ejecuta con poca frecuencia. Por otro lado, si está en un bucle interno de algún cálculo que es la función principal del código, entonces cada fracción de tiempo que puede eliminar puede hacer una gran diferencia. Si está ejecutando una simulación en un clúster, esas fracciones guardadas de un nanosegundo en su bucle interno pueden traducirse directamente en dinero gastado en hardware y electricidad.
Para algunos algoritmos y contextos, 10,000,000,000 de iteraciones podrían ser insignificantes. Nuevamente, generalmente no tiene sentido hablar sobre escenarios específicos que solo se aplican en ciertos contextos.
Quizás tengas razón. Pero, de nuevo, se trata de cuáles son los objetivos de un idioma en particular. De hecho, muchos idiomas están diseñados para satisfacer las necesidades de "la mayoría" o para favorecer la seguridad sobre otras preocupaciones. Otros, como C y C ++, priorizan la eficiencia. En ese contexto, hacer que todos paguen una penalización de rendimiento simplemente porque la mayoría de las personas no se molestarán, va en contra de lo que el idioma está tratando de lograr.
fuente
Hay buenas respuestas, pero creo que hay un punto perdido aquí: los efectos de un desbordamiento de enteros no son necesariamente algo malo, y después de los hechos es difícil saber si
i
pasó de serMAX_INT
a serMIN_INT
debido a un problema de desbordamiento o si se hizo intencionalmente multiplicando por -1.Por ejemplo, si quiero agregar todos los enteros representables mayores que 0 juntos, simplemente usaría un
for(i=0;i>=0;++i){...}
ciclo de suma y, cuando se desborda, detiene la suma, que es el comportamiento objetivo (arrojar un error significaría que tengo que eludir una protección arbitraria porque está interfiriendo con la aritmética estándar). Es una mala práctica restringir la aritmética primitiva porque:fuente
INT_MAX
aINT_MIN
multiplicando por -1.for(i=0;i>=0;++i){...}
es el estilo de código que trato de desalentar en mi equipo: se basa en efectos especiales / efectos secundarios y no expresa claramente lo que debe hacer. Pero aún así aprecio su respuesta, ya que muestra un paradigma de programación diferente.i
trata de un tipo de 64 bits, incluso en una implementación con un comportamiento de complemento de dos silencioso consistente y envolvente, ejecutando mil millones de iteraciones por segundo, tal ciclo solo podría garantizarse para encontrar elint
valor más grande si se le permite ejecutar cientos de años. En los sistemas que no prometen un comportamiento coherente silencioso, estos comportamientos no estarán garantizados sin importar cuánto tiempo se proporcione el código.