Estoy trabajando con un compilador para un chip DSP que genera deliberadamente código que accede a uno más allá del final de una matriz de código C que no lo hace.
Esto se debe a que los bucles están estructurados de modo que el final de una iteración capta previamente algunos datos para la siguiente iteración. Por lo tanto, el datum captado previamente al final de la última iteración nunca se usa realmente.
Escribir código C como ese invoca un comportamiento indefinido, pero eso es solo una formalidad de un documento estándar que se ocupa de la máxima portabilidad.
Más a menudo que no, un programa que accede fuera de los límites no está inteligentemente optimizado. Es simplemente con errores. El código obtiene algún valor de basura y, a diferencia de los bucles optimizados del compilador mencionado anteriormente, el código luego usa el valor en cálculos posteriores, corrompiéndolos.
Vale la pena detectar errores como ese, por lo que vale la pena hacer que el comportamiento sea indefinido, incluso por esa sola razón: para que el tiempo de ejecución pueda producir un mensaje de diagnóstico como "desbordamiento de matriz en la línea 42 de main.c".
En los sistemas con memoria virtual, se podría asignar una matriz de manera que la dirección que sigue esté en un área no asignada de memoria virtual. El acceso luego bombardeará el programa.
Como comentario adicional, tenga en cuenta que en C se nos permite crear un puntero que está uno más allá del final de una matriz. Y este puntero tiene que comparar más que cualquier puntero con el interior de una matriz. Esto significa que una implementación en C no puede colocar una matriz justo al final de la memoria, donde la dirección uno más se ajustaría y se vería más pequeña que otras direcciones en la matriz.
Sin embargo, el acceso a valores no inicializados o fuera de los límites a veces es una técnica de optimización válida, incluso si no es máximamente portátil. Esta es, por ejemplo, la razón por la cual la herramienta Valgrind no informa accesos a datos no inicializados cuando esos accesos suceden, sino solo cuando el valor se usa más tarde de alguna manera que podría afectar el resultado del programa. Obtiene un diagnóstico como "rama condicional en xxx: nnn depende del valor no inicializado" y a veces puede ser difícil rastrear dónde se origina. Si todos estos accesos quedaran atrapados de inmediato, habría muchos falsos positivos derivados del código optimizado del compilador, así como del código correctamente optimizado a mano.
Hablando de eso, estaba trabajando con un códec de un proveedor que emitía estos errores cuando se transfirió a Linux y se ejecutó bajo Valgrind. Pero el vendedor me convenció de que solo varios bitsdel valor que se usa en realidad proviene de la memoria no inicializada, y esos bits fueron cuidadosamente evitados por la lógica. Solo se estaban usando los bits buenos del valor y Valgrind no tiene la capacidad de rastrear el bit individual. El material no inicializado proviene de leer una palabra más allá del final de un flujo de bits de datos codificados, pero el código sabe cuántos bits hay en el flujo y no utilizará más bits de los que realmente hay. Dado que el acceso más allá del final de la matriz de flujo de bits no causa ningún daño en la arquitectura DSP (no hay memoria virtual después de la matriz, no hay puertos mapeados en memoria y la dirección no se ajusta) es una técnica de optimización válida.
"Comportamiento indefinido" en realidad no significa mucho, porque de acuerdo con ISO C, simplemente incluir un encabezado que no está definido en el estándar C, o llamar a una función que no está definida en el propio programa o el estándar C, son ejemplos de indefinido comportamiento. El comportamiento indefinido no significa "no definido por nadie en el planeta" simplemente "no definido por el estándar ISO C". Pero, por supuesto, el comportamiento a veces sin definir realmente es absolutamente no definido por cualquiera.