Filosofía detrás del comportamiento indefinido

59

Las especificaciones C \ C ++ dejan fuera una gran cantidad de comportamientos abiertos para que los compiladores los implementen a su manera. Hay una serie de preguntas que siempre se siguen haciendo aquí sobre lo mismo y tenemos algunas publicaciones excelentes al respecto:

Mi pregunta no es sobre qué es el comportamiento indefinido o si es realmente malo. Conozco los peligros y la mayoría de las citas de comportamiento indefinido relevantes del estándar, por lo que debe abstenerse de publicar respuestas sobre lo malo que es. Esta pregunta es acerca de la filosofía detrás de dejar muchos comportamientos abiertos para la implementación del compilador.

Leí una excelente publicación de blog que dice que el rendimiento es la razón principal. Me preguntaba si el rendimiento es el único criterio para permitirlo, o ¿hay otros factores que influyan en la decisión de dejar las cosas abiertas para la implementación del compilador?

Si tiene algún ejemplo para citar sobre cómo un comportamiento indefinido en particular proporciona suficiente espacio para que el compilador lo optimice, por favor enlistelos. Si conoce otros factores además del rendimiento, respalde su respuesta con suficiente detalle.

Si no comprende la pregunta o no tiene suficientes evidencias / fuentes para respaldar su respuesta, no publique respuestas especulativas generales.

Alok Save
fuente
77
¿Quién ha oído hablar de una computadora determinista de todos modos?
sova
1
como indica la excelente respuesta de litb programmers.stackexchange.com/a/99741/192238 , el título y el cuerpo de esta pregunta parecen un poco desajustados: "los comportamientos abiertos para que los compiladores los implementen a su manera" generalmente se denominan definidos por la implementación . claro, el autor de implementación puede definir la UB real, pero la mayoría de las veces, no molestan (y optimizan todo, etc.)
subrayado_d

Respuestas:

49

Primero, notaré que aunque solo mencione "C" aquí, lo mismo se aplica igualmente a C ++ también.

El comentario que mencionaba a Godel fue en parte (pero solo en parte) acertado.

Cuando te pones a ello, el comportamiento indefinido en los estándares C es en gran medida solo señalar el límite entre lo que el estándar intenta definir y lo que no.

Los teoremas de Godel (hay dos) básicamente dicen que es imposible definir un sistema matemático que pueda probarse (por sus propias reglas) que sea completo y consistente. Puede hacer sus reglas para que estén completas (el caso con el que trató fueron las reglas "normales" para los números naturales), o puede hacer posible demostrar su consistencia, pero no puede tener ambas.

En el caso de algo como C, eso no se aplica directamente; en su mayor parte, la "capacidad de prueba" de la integridad o consistencia del sistema no es una alta prioridad para la mayoría de los diseñadores de idiomas. Al mismo tiempo, sí, probablemente fueron influenciados (al menos en cierto grado) al saber que es probablemente imposible definir un sistema "perfecto", uno que sea demostrablemente completo y consistente. Saber que tal cosa es imposible puede haber hecho un poco más fácil dar un paso atrás, respirar un poco y decidir sobre los límites de lo que tratarían de definir.

A riesgo de (una vez más) ser acusado de arrogancia, caracterizaría el estándar C como gobernado (en parte) por dos ideas básicas:

  1. El lenguaje debe admitir una variedad de hardware lo más amplia posible (idealmente, todo el hardware "sano" hasta un límite inferior razonable).
  2. El lenguaje debe admitir la escritura de la mayor variedad de software posible para el entorno dado.

El primero significa que si alguien define una nueva CPU, debería ser posible proporcionar una implementación buena, sólida y utilizable de C para eso, siempre y cuando el diseño esté al menos razonablemente cerca de algunas pautas simples, básicamente, si sigue algo en el orden general del modelo de Von Neumann y proporciona al menos una cantidad mínima de memoria razonable, que debería ser suficiente para permitir una implementación en C. Para una implementación "alojada" (una que se ejecuta en un sistema operativo), debe admitir alguna noción que corresponda razonablemente a los archivos, y tener un conjunto de caracteres con un cierto conjunto mínimo de caracteres (se requieren 91).

El segundo significa que debería ser posible escribir código que manipule el hardware directamente, por lo que puede escribir cosas como cargadores de arranque, sistemas operativos, software integrado que se ejecuta sin ningún sistema operativo, etc. En última instancia, hay algunos límites a este respecto, por lo que casi cualquier Es probable que el sistema operativo práctico, el gestor de arranque, etc., contenga al menos un poco de código escrito en lenguaje ensamblador. Del mismo modo, es probable que incluso un pequeño sistema integrado incluya al menos algún tipo de rutinas de biblioteca preescritas para dar acceso a los dispositivos en el sistema host. Aunque es difícil definir un límite preciso, la intención es que la dependencia de dicho código se mantenga al mínimo.

El comportamiento indefinido en el lenguaje está impulsado en gran medida por la intención del lenguaje de admitir estas capacidades. Por ejemplo, el idioma le permite convertir un entero arbitrario en un puntero y acceder a lo que sea que esté en esa dirección. El estándar no intenta decir qué sucederá cuando lo haga (por ejemplo, incluso leer desde algunas direcciones puede tener efectos visibles desde el exterior). Al mismo tiempo, no intenta evitar que haga tales cosas, porque necesita algunos tipos de software que se supone que puede escribir en C.

Existe también un comportamiento indefinido impulsado por otros elementos de diseño. Por ejemplo, otra intención de C es admitir una compilación separada. Esto significa (por ejemplo) que se pretende que pueda "vincular" piezas utilizando un vinculador que sigue aproximadamente lo que la mayoría de nosotros vemos como el modelo habitual de un vinculador. En particular, debería ser posible combinar módulos compilados por separado en un programa completo sin el conocimiento de la semántica del lenguaje.

Hay otro tipo de comportamiento indefinido (que es mucho más común en C ++ que C), que está presente simplemente debido a los límites en la tecnología del compilador: cosas que básicamente sabemos que son errores, y probablemente le gustaría que el compilador diagnostique como errores, pero dados los límites actuales en la tecnología de compilación, es dudoso que puedan diagnosticarse en todas las circunstancias. Muchos de estos están impulsados ​​por otros requisitos, como la compilación por separado, por lo que se trata principalmente de equilibrar los requisitos en conflicto, en cuyo caso el comité generalmente ha optado por apoyar mayores capacidades, incluso si eso significa la falta de diagnóstico de algunos posibles problemas, en lugar de limitar las capacidades para garantizar que se diagnostiquen todos los posibles problemas.

Estas diferencias en la intención impulsan la mayoría de las diferencias entre C y algo como Java o los sistemas basados ​​en CLI de Microsoft. Estos últimos están bastante explícitamente limitados a trabajar con un conjunto de hardware mucho más limitado, o requieren que el software emule el hardware más específico al que se dirigen. También tienen la intención específica de evitar cualquier manipulación directa de hardware, en su lugar requieren que use algo como JNI o ​​P / Invoke (y código escrito en algo como C) para incluso hacer tal intento.

Volviendo a los teoremas de Godel por un momento, podemos trazar una especie de paralelo: Java y CLI han optado por la alternativa "internamente consistente", mientras que C ha optado por la alternativa "completa". Por supuesto, esto es una analogía muy áspera - Dudo que alguien está intentando una prueba formal de cualquiera de consistencia interna o la integridad en ambos casos. No obstante, la noción general encaja bastante estrechamente con las elecciones que han tomado.

Jerry Coffin
fuente
25
Creo que los teoremas de Godel son una pista falsa. Tratan de probar un sistema desde sus propios axiomas, que no es el caso aquí: C no necesita especificarse en C. Es muy posible tener un lenguaje completamente especificado (considere una máquina de Turing).
Poolie
99
Lo siento, pero me temo que has entendido mal los Teoremas de Godel. Se ocupan de la imposibilidad de probar todas las declaraciones verdaderas en un sistema coherente de lógica; En términos de computación, el teorema de incompletitud es análogo a decir que hay problemas que no pueden ser resueltos por ningún programa: los problemas son análogos a las declaraciones verdaderas, los programas a las pruebas y el modelo de computación al sistema lógico. No tiene ninguna conexión con el comportamiento indefinido. Consulte la explicación de la analogía aquí: scottaaronson.com/blog/?p=710 .
Alex ten Brink
55
Debo señalar que no se requiere una máquina Von Neumann para una implementación en C. Es perfectamente posible (y ni siquiera muy difícil) desarrollar una implementación en C para una arquitectura de Harvard (y no me sorprendería ver muchas de estas implementaciones en sistemas embebidos)
bdonlan
1
Desafortunadamente, la filosofía moderna del compilador de C lleva a UB a un nivel completamente nuevo. Incluso en los casos en que un programa estaba preparado para lidiar con casi todas las consecuencias "naturales" plausibles de una forma particular de Comportamiento indefinido, y aquellas con las que no podía tratar serían al menos reconocibles (por ejemplo, desbordamiento de enteros atrapados), la nueva filosofía favorece omitiendo cualquier código que no pueda ejecutarse a menos que UB fuera a ocurrir, convirtiendo el código que se habría comportado correctamente en la mayoría de las implementaciones en código que es "más eficiente" pero simplemente incorrecto.
supercat
20

La justificación de C explica

Los términos comportamiento no especificado, comportamiento indefinido y comportamiento definido por la implementación se utilizan para clasificar el resultado de escribir programas cuyas propiedades el Estándar no describe o no puede describir por completo. El objetivo de adoptar esta categorización es permitir una cierta variedad de implementaciones que permita que la calidad de implementación sea una fuerza activa en el mercado, así como permitir ciertas extensiones populares , sin eliminar el prestigio de conformidad con el Estándar. El Apéndice F de la Norma cataloga aquellos comportamientos que se incluyen en una de estas tres categorías.

El comportamiento no especificado le da al implementador cierta libertad para traducir programas. Esta latitud no se extiende hasta no poder traducir el programa.

El comportamiento indefinido otorga al implementador la licencia para no detectar ciertos errores del programa que son difíciles de diagnosticar. También identifica áreas de posible extensión de lenguaje conforme: el implementador puede aumentar el lenguaje al proporcionar una definición del comportamiento oficialmente indefinido.

El comportamiento definido por la implementación le da al implementador la libertad de elegir el enfoque apropiado, pero requiere que se explique esta opción al usuario. Los comportamientos designados como definidos por la implementación son generalmente aquellos en los que un usuario podría tomar decisiones de codificación significativas basadas en la definición de implementación. Los implementadores deben tener en cuenta este criterio al decidir qué tan extensa debe ser una definición de implementación. Al igual que con el comportamiento no especificado, simplemente no traducir la fuente que contiene el comportamiento definido por la implementación no es una respuesta adecuada.

También es importante el beneficio para los programas, no solo el beneficio para las implementaciones. Un programa que depende de un comportamiento indefinido aún puede ser conforme , si es aceptado por una implementación conforme. La existencia de un comportamiento indefinido permite que un programa use características no portátiles explícitamente marcadas como tales ("comportamiento indefinido"), sin volverse no conforme. La justificación señala:

El código C puede no ser portátil. Aunque se esforzó por dar a los programadores la oportunidad de escribir programas verdaderamente portátiles, el Comité no quería obligar a los programadores a escribir de manera portátil, para impedir el uso de C como un `` ensamblador de alto nivel '': la capacidad de escribir en máquinas específicas El código es uno de los puntos fuertes de C. Es este principio el que motiva en gran medida la distinción entre un programa estrictamente conforme y un programa conforme (§1.7).

Y en 1.7 se nota

La triple definición de cumplimiento se utiliza para ampliar la población de programas conformes y distinguir entre los programas conformes utilizando una sola implementación y los programas conformes portátiles.

Un programa estrictamente conforme es otro término para un programa máximamente portátil. El objetivo es darle al programador una oportunidad de luchar para crear programas C potentes que también sean altamente portátiles, sin degradar los programas C perfectamente útiles que no son portátiles. Así el adverbio estrictamente.

Por lo tanto, este pequeño programa sucio que funciona perfectamente bien en GCC todavía se está conformando .

Johannes Schaub - litb
fuente
15

La cuestión de la velocidad es especialmente un problema en comparación con C. Si C ++ hiciera algunas cosas que podrían ser sensatas, como inicializar grandes matrices de tipos primitivos, perdería una tonelada de puntos de referencia con el código C. Entonces, C ++ inicializa sus propios tipos de datos, pero deja los tipos C tal como estaban.

Otro comportamiento indefinido solo refleja la realidad. Un ejemplo es el desplazamiento de bits con un recuento mayor que el tipo. Eso realmente difiere entre las generaciones de hardware de la misma familia. Si tiene una aplicación de 16 bits, exactamente el mismo binario dará resultados diferentes en un 80286 y un 80386. ¡Entonces el estándar del lenguaje dice que no lo sabemos!

Algunas cosas simplemente se mantienen como estaban, como el orden de evaluación de subexpresiones no especificado. Originalmente se creía que esto ayudaba a los escritores de compiladores a optimizar mejor. Hoy en día, los compiladores son lo suficientemente buenos como para descubrirlo de todos modos, pero el costo de encontrar todos los lugares en los compiladores existentes que aprovechan la libertad es demasiado alto.

Bo Persson
fuente
+1 para el segundo párrafo, que muestra algo que sería incómodo haber especificado como comportamiento definido por la implementación.
David Thornley
3
El cambio de bit es solo un ejemplo de aceptar un comportamiento de compilador indefinido y usar las capacidades del hardware. Sería trivial especificar un resultado C para un cambio de bit cuando el recuento es mayor que el tipo, pero costoso de implementar en algún hardware.
mattnz
7

Como un ejemplo, los accesos de puntero casi tienen que estar indefinidos y no necesariamente solo por razones de rendimiento. Por ejemplo, en algunos sistemas, cargar registros específicos con un puntero generará una excepción de hardware. En SPARC acceder a un objeto de memoria alineado incorrectamente causará un error de bus, pero en x86 sería "simplemente" lento. Es complicado especificar el comportamiento en esos casos, ya que el hardware subyacente dicta lo que sucederá, y C ++ es portátil para tantos tipos de hardware.

Por supuesto, también le da al compilador la libertad de usar conocimientos específicos de arquitectura. Para un ejemplo de comportamiento no especificado, el desplazamiento a la derecha de los valores con signo puede ser lógico o aritmético dependiendo del hardware subyacente, para permitir el uso de cualquier operación de desplazamiento disponible y no forzar la emulación del software.

Creo que también facilita el trabajo del compilador-escritor, pero no puedo recordar el ejemplo por ahora. Lo agregaré si recuerdo la situación.

Mark B
fuente
3
El lenguaje C podría haberse especificado de tal manera que siempre tuviera que usar lecturas byte por byte en sistemas con restricciones de alineación, y tal que tuviera que proporcionar trampas de excepción con un comportamiento bien definido para accesos de direcciones no válidos. Pero, por supuesto, todo esto habría sido increíblemente costoso (en tamaño, complejidad y rendimiento del código) y no habría ofrecido ningún beneficio para un código correcto y sensato.
R ..
6

Simple: velocidad y portabilidad. Si C ++ garantiza que tiene una excepción al desreferenciar un puntero no válido, entonces no sería portátil para el hardware incorporado. Si C ++ garantizara otras cosas, como primitivas siempre inicializadas, entonces sería más lento, y en el momento del origen de C ++, más lento era algo muy, muy malo.

DeadMG
fuente
1
¿Eh? ¿Qué tienen que ver las excepciones con el hardware incorporado?
Mason Wheeler
2
Las excepciones pueden bloquear el sistema de maneras que son muy malas para los sistemas integrados que necesitan responder rápidamente. Hay situaciones en las que una lectura falsa es mucho menos dañina que un sistema lento.
Ingeniero mundial el
1
@Mason: Porque el hardware tiene que atrapar el acceso no válido. Es fácil para Windows lanzar una infracción de acceso, y más difícil para el hardware integrado sin sistema operativo hacer nada excepto morir.
DeadMG
3
También recuerde que no todas las CPU tienen una MMU para protegerse contra accesos no válidos en el hardware, para empezar. Si comienza a requerir que su idioma verifique todos los accesos de puntero, entonces debe emular una MMU en CPU sin una, y así CADA acceso a la memoria se vuelve extremadamente costoso.
esponjoso
4

C se inventó en una máquina con bytes de 9 bits y sin unidad de punto flotante. Supongamos que hubiera ordenado que los bytes sean de 9 bits, palabras de 18 bits y que los flotadores se implementen utilizando la aritmática pre IEEE754.

Martin Beckett
fuente
55
Sospecho que estás pensando en Unix: C se usó originalmente en el PDP-11, que en realidad era un estándar actual bastante convencional. Creo que la idea básica se mantiene de todos modos.
Jerry Coffin
@Jerry - sí, tienes razón - ¡me estoy haciendo viejo!
Martin Beckett
Sí, nos pasa a los mejores, me temo.
Jerry Coffin
4

No creo que la primera razón para UB sea dejar espacio al compilador para optimizar, pero solo la posibilidad de usar la implementación obvia para los objetivos en un momento en que las arquitecturas tenían más variedad que ahora (recuerde si C fue diseñado en un PDP-11, que tiene una arquitectura algo familiar, el primer puerto fue Honeywell 635, que es mucho menos familiar: direccionable por palabras, usando palabras de 36 bits, bytes de 6 o 9 bits, direcciones de 18 bits ... bueno, al menos usó 2's complemento). Pero si la optimización no era un objetivo, la implementación obvia no incluye agregar comprobaciones de tiempo de ejecución para el desbordamiento, el recuento de turnos sobre el tamaño del registro, que alias en expresiones que modifican múltiples valores.

Otra cosa que se tuvo en cuenta fue la facilidad de implementación. El compilador de CA en ese momento tenía múltiples pases utilizando múltiples procesos porque tener un proceso que manejara todo no hubiera sido posible (el programa habría sido demasiado grande). Solicitar una gran verificación de coherencia estaba fuera del camino, especialmente cuando involucraba varias CU. (Para eso se utilizó otro programa que los compiladores de C, lint).

Un programador
fuente
Me pregunto qué impulsó la filosofía cambiante de UB de "Permitir a los programadores usar comportamientos expuestos por su plataforma" a "Encontrar excusas para permitir que los compiladores implementen un comportamiento totalmente loco". También me pregunto cuánto terminan tales optimizaciones mejorando el tamaño del código después de que el código se modifica para funcionar bajo el nuevo compilador. No me sorprendería si en muchos casos el único efecto de agregar tales "optimizaciones" al compilador es obligar a los programadores a escribir código más grande y más lento para evitar que el compilador lo rompa.
supercat
Es una deriva en POV. Las personas se volvieron menos conscientes de la máquina en la que se ejecuta su programa, se preocuparon más por la portabilidad, por lo que evitaron depender de comportamientos indefinidos, no especificados y definidos por la implementación. Hubo presión sobre los optimizadores para obtener los mejores resultados en el punto de referencia, y eso significa hacer uso de cada indulgencia que dejan las especificaciones de los idiomas. También existe el hecho de que Internet, Usenet a la vez, SE hoy en día, los abogados de idiomas también tienden a dar una visión parcial de la lógica subyacente y el comportamiento de los escritores de compiladores.
Programador
1
Lo que me parece curioso son las declaraciones que he visto en el sentido de "C supone que los programadores nunca se involucrarán en comportamientos indefinidos", un hecho que históricamente no ha sido cierto. Una afirmación correcta habría sido "C asumió que los programadores no desencadenarían un comportamiento indefinido por el estándar a menos que estuvieran preparados para lidiar con las consecuencias naturales de la plataforma de ese comportamiento. Dado que C fue diseñado como un lenguaje de programación de sistemas, una gran parte de su propósito era permitir que los programadores hicieran cosas específicas del sistema no definidas por el estándar del lenguaje; la idea de que nunca lo harían es absurda
supercat
Es bueno que los programadores realicen esfuerzos adicionales para garantizar la portabilidad en los casos en que diferentes plataformas harían cosas inherentemente diferentes , pero los escritores de compiladores pierden el tiempo de todos cuando eliminan comportamientos que históricamente los programadores podrían haber esperado que fueran comunes a todos los compiladores futuros. Enteros dados iy n, de tal manera que n < INT_BITSy i*(1<<n)no se desborde, consideraría i<<=n;a ser más claro que i=(unsigned)i << n;; en muchas plataformas sería más rápido y más pequeño que i*=(1<<N);. ¿Qué se gana si los compiladores lo prohíben?
supercat
Si bien creo que sería bueno para el estándar permitir trampas para muchas cosas que llama UB (por ejemplo, desbordamiento de enteros), y hay buenas razones para que no requiera que las trampas hagan algo predecible, creo que desde cualquier punto de vista imaginable el estándar se mejoraría si requiriera que la mayoría de las formas de UB deben arrojar un valor indeterminado o documentar el hecho de que se reservan el derecho de hacer otra cosa, sin estar absolutamente obligados a documentar qué podría ser esa otra cosa. Los compiladores que hicieron todo "UB" serían legales, pero probablemente desfavorecidos ...
supercat
3

Uno de los primeros casos clásicos fue la suma de enteros firmados. En algunos de los procesadores en uso, eso causaría una falla, y en otros simplemente continuaría con un valor (probablemente el valor modular apropiado). Especificar cualquier caso significaría que los programas para máquinas con el estilo aritmético desfavorable tendrían que tener un código adicional, incluida una rama condicional, para algo tan similar como la suma de enteros.

David Thornley
fuente
La suma de enteros es un caso interesante; más allá de la posibilidad de un comportamiento de captura que en algunos casos sería útil pero que en otros casos podría causar la ejecución de código aleatorio, hay situaciones en las que sería razonable que un compilador haga inferencias basadas en el hecho de que el desbordamiento de enteros no está especificado para ajustarse. Por ejemplo, un compilador donde inthay 16 bits y los turnos con signo extendido son caros podría calcular (uchar1*uchar2) >> 4utilizando un turno sin signo extendido. Desafortunadamente, algunos compiladores extienden las inferencias no solo a los resultados, sino también a los operandos.
supercat
2

Diría que se trató menos de filosofía que de realidad: C siempre ha sido un lenguaje multiplataforma, y ​​el estándar debe reflejar eso y el hecho de que en el momento en que se publique cualquier estándar, habrá un gran cantidad de implementaciones en una gran cantidad de hardware diferente. Un estándar que prohíbe el comportamiento necesario sería ignorado o produciría un cuerpo de estándares competidores.

jmoreno
fuente
Originalmente, muchos comportamientos se dejaron sin definir para permitir la posibilidad de que diferentes sistemas hicieran cosas diferentes, incluido desencadenar una trampa de hardware con un controlador que puede o no ser configurable (y podría, si no está configurado, causar un comportamiento arbitrariamente impredecible). Requerir que un desplazamiento a la izquierda de un valor negativo no atrape, por ejemplo, rompería cualquier código que se diseñó para un sistema donde lo hizo y se basó en dicho comportamiento. En resumen, se dejaron sin definir para no evitar que los implementadores proporcionen comportamientos que consideraban útiles .
supercat
Desafortunadamente, sin embargo, eso se ha cambiado de tal manera que incluso el código que sabe que se está ejecutando en un procesador que haría algo útil en un caso particular no puede aprovechar ese comportamiento, porque los compiladores pueden usar el hecho de que el estándar C no No especifique el comportamiento (aunque la plataforma lo haría) para aplicar reescrituras de bizarro-world al código.
supercat
1

Algunos comportamientos no pueden definirse por ningún medio razonable. Me refiero a acceder a un puntero eliminado. La única forma de detectarlo sería prohibir el valor del puntero después de la eliminación (memorizar su valor en algún lugar y no permitir que ninguna función de asignación lo devuelva más). No solo una memorización de este tipo sería exagerada, sino que para un programa de ejecución prolongada se agotarían los valores de punteros permitidos.

Tadeusz Kopec
fuente
o podría asignar todos los punteros como weak_ptry anular todas las referencias a un puntero que se deleted ... oh, espera, nos acercamos a la recolección de basura: /
Matthieu M.
boost::weak_ptrLa implementación es una plantilla bastante buena para comenzar con este patrón de uso. En lugar de rastrear y anular weak_ptrsexternamente, un weak_ptrsolo contribuye al shared_ptrrecuento débil del s, y el recuento débil es básicamente un recuento al puntero mismo. Por lo tanto, puede anular el shared_ptrsin tener que eliminarlo de inmediato. No es perfecto (todavía puede tener muchos vencidos weak_ptrmanteniendo el subyacente shared_countsin una buena razón), pero al menos es rápido y eficiente.
esponjoso
0

Le daré un ejemplo en el que prácticamente no hay otra opción sensata que no sea un comportamiento indefinido. En principio, cualquier puntero podría apuntar a la memoria que contiene cualquier variable, con la pequeña excepción de las variables locales que el compilador puede saber que nunca han tomado su dirección. Sin embargo, para obtener un rendimiento aceptable en una CPU moderna, un compilador debe copiar valores variables en registros. Operar completamente sin memoria no es un arranque.

Esto básicamente te da dos opciones:

1) Elimine todo de los registros antes de cualquier acceso a través de un puntero, en caso de que el puntero apunte a la memoria de esa variable en particular. Luego cargue todo lo necesario nuevamente en el registro, en caso de que los valores se hayan cambiado a través del puntero.

2) Tener un conjunto de reglas para cuando un puntero puede alias una variable y cuando el compilador puede asumir que un puntero no alias una variable.

C opta por la opción 2, porque 1 sería terrible para el rendimiento. Pero entonces, ¿qué sucede si un puntero alias una variable de una manera que las reglas C prohíben? Dado que el efecto depende de si el compilador de hecho almacenó la variable en un registro, no hay forma de que el estándar C garantice definitivamente resultados específicos.

David Schwartz
fuente
Habría una diferencia semántica entre decir "Un compilador puede comportarse como si X fuera verdadero" y decir "Cualquier programa en el que X no sea verdadero participará en Comportamiento indefinido", aunque desafortunadamente los estándares para no aclarar la distinción. En muchas situaciones, incluido su ejemplo de alias, la declaración anterior permitiría muchas optimizaciones del compilador que de otro modo serían imposibles; este último permite algunas "optimizaciones" más, pero muchas de estas últimas optimizaciones son cosas que los programadores no desearían.
supercat
Por ejemplo, si algún código establece un fooa 42, y luego llama a un método que utiliza un puntero modificado de forma ilegítima para establecer foo44, puedo ver el beneficio de decir que hasta la próxima escritura "legítima" foo, los intentos de leerlo pueden legítimamente produce 42 o 44, y una expresión como foo+foopodría incluso producir 86, pero veo mucho menos beneficio al permitir que el compilador haga inferencias extendidas e incluso retroactivas, cambiando el Comportamiento indefinido cuyos comportamientos "naturales" plausibles habrían sido benignos, en una licencia para generar código sin sentido.
supercat
0

Históricamente, el comportamiento indefinido tenía dos propósitos principales:

  1. Para evitar exigir a los autores del compilador que generen código para manejar condiciones que nunca se supone que ocurran.

  2. Para permitir la posibilidad de que en ausencia de código para manejar explícitamente tales condiciones, las implementaciones pueden tener varios tipos de comportamientos "naturales" que, en algunos casos, serían útiles.

Como un ejemplo simple, en algunas plataformas de hardware, intentar sumar dos enteros con signo positivo cuya suma es demasiado grande para caber en un entero con signo producirá un entero con signo negativo en particular. En otras implementaciones, activará una trampa de procesador. Para que el estándar C imponga cualquiera de los comportamientos, se requerirá que los compiladores para plataformas cuyo comportamiento natural difiera del estándar tengan que generar un código adicional para obtener el comportamiento correcto, código que puede ser más costoso que el código para realizar la adición real. Peor aún, significaría que los programadores que quisieran el comportamiento "natural" tendrían que agregar aún más código adicional para lograrlo (y ese código adicional sería nuevamente más costoso que la adición).

Desafortunadamente, algunos autores de compiladores han tomado la filosofía de que los compiladores deben hacer todo lo posible para encontrar condiciones que evoquen comportamientos indefinidos y, suponiendo que tales situaciones nunca ocurran, sacar inferencias extensas de eso. Por lo tanto, en un sistema con 32 bits int, un código dado como:

uint32_t foo(uint16_t q, int *p)
{
  if (q > 46340)
    *p++;
  return q*q;
}

el estándar C permitiría al compilador decir que si q es 46341 o mayor, la expresión q * q producirá un resultado demasiado grande para caber en un int, causando en consecuencia un comportamiento indefinido, y como resultado el compilador tendrá derecho a asumir que no puede suceder y, por lo tanto, no sería necesario que se incremente *psi lo hace. Si el código de llamada se usa *pcomo un indicador de que debe descartar los resultados del cálculo, el efecto de la optimización puede ser tomar un código que hubiera arrojado resultados sensibles en sistemas que funcionan de casi cualquier forma imaginable con desbordamiento de enteros (la captura puede ser feo, pero al menos sería sensato), y lo convirtió en un código que puede comportarse sin sentido.

Super gato
fuente
-6

La eficiencia es la excusa habitual, pero sea cual sea la excusa, el comportamiento indefinido es una idea terrible para la portabilidad. En efecto, los comportamientos indefinidos se convierten en suposiciones no verificadas y no declaradas.

ddyer
fuente
77
El OP especificó esto: "Mi pregunta no es sobre qué es el comportamiento indefinido, o si es realmente malo. Conozco los peligros y la mayoría de las citas relevantes de comportamiento indefinido del estándar, así que abstente de publicar respuestas sobre lo malo que es ". Parece que no leíste la pregunta.
Etienne de Martel