Mirando este código:
static int global_var = 0;
int update_three(int val)
{
global_var = val;
return 3;
}
int main()
{
int arr[5];
arr[global_var] = update_three(2);
}
¿Qué entrada de matriz se actualiza? 0 o 2?
¿Hay alguna parte en la especificación de C que indique la precedencia de la operación en este caso particular?
c
language-lawyer
order-of-execution
Jiminion
fuente
fuente
clang
que este código active una advertencia en mi humilde opinión.Respuestas:
Orden de operandos izquierdo y derecho
Para realizar la asignación
arr[global_var] = update_three(2)
, la implementación de C debe evaluar los operandos y, como efecto secundario, actualizar el valor almacenado del operando izquierdo. C 2018 6.5.16 (que trata sobre las asignaciones), el párrafo 3 nos dice que no hay secuencia en los operandos izquierdo y derecho:Esto significa que la implementación de C es libre de calcular primero el valor de l
arr[global_var]
(al calcular el valor de l, nos referimos a averiguar a qué se refiere esta expresión), luego evaluarupdate_three(2)
y finalmente asignar el valor de este último al primero; o para evaluarupdate_three(2)
primero, luego calcular el valor l, luego asignar el primero al segundo; o para evaluar el valor de l yupdate_three(2)
de alguna manera entremezclada y luego asignar el valor correcto al valor de l izquierdo.En todos los casos, la asignación del valor al valor l debe ser la última, porque 6.5.16 3 también dice:
Violación de secuencia
Algunos podrían reflexionar sobre el comportamiento indefinido debido al uso
global_var
y la actualización por separado en violación de 6.5 2, que dice:Es muy familiar para muchos practicantes de C que el comportamiento de expresiones tales como
x + x++
no está definido por el estándar C porque ambos usan el valorx
y lo modifican por separado en la misma expresión sin secuenciar. Sin embargo, en este caso, tenemos una llamada de función, que proporciona una secuencia.global_var
se usaarr[global_var]
y se actualiza en la llamada de funciónupdate_three(2)
.6.5.2.2 10 nos dice que hay un punto de secuencia antes de llamar a la función:
Dentro de la función,
global_var = val;
hay una expresión completa , y también lo es3
inreturn 3;
, por 6.8 4:Luego hay un punto de secuencia entre estas dos expresiones, nuevamente por 6.8 4:
Por lo tanto, la implementación de C puede evaluar
arr[global_var]
primero y luego hacer la llamada a la función, en cuyo caso hay un punto de secuencia entre ellos porque hay uno antes de la llamada a la función, o puede evaluarglobal_var = val;
en la llamada a la función y luegoarr[global_var]
, en cuyo caso hay un punto de secuencia entre ellos porque hay uno después de la expresión completa. Por lo tanto, el comportamiento no está especificado (cualquiera de esas dos cosas puede evaluarse primero), pero no está indefinido.fuente
El resultado aquí no está especificado .
Si bien el orden de las operaciones en una expresión, que dicta cómo se agrupan las subexpresiones, está bien definido, el orden de evaluación no se especifica. En este caso, significa que se
global_var
podría leer primero o que la llamadaupdate_three
podría ocurrir primero, pero no hay forma de saber cuál.Aquí no hay un comportamiento indefinido porque una llamada a la función introduce un punto de secuencia, al igual que cada declaración en la función, incluida la que modifica
global_var
.Para aclarar, el estándar C define el comportamiento indefinido en la sección 3.4.3 como:
y define el comportamiento no especificado en la sección 3.4.4 como:
El estándar establece que el orden de evaluación de los argumentos de la función no está especificado, lo que en este caso significa que
arr[0]
se establece en 3 oarr[2]
se establece en 3.fuente
Lo intenté y obtuve la entrada 0 actualizada.
Sin embargo, de acuerdo con esta pregunta: el lado derecho de una expresión siempre se evalúa primero
El orden de evaluación no está especificado ni secuenciado. Así que creo que un código como este debería evitarse.
fuente
Como tiene poco sentido emitir código para una asignación antes de tener un valor para asignar, la mayoría de los compiladores de C emitirán primero código que llama a la función y guardarán el resultado en algún lugar (registro, apilamiento, etc.), luego emitirán código que escribe este valor en su destino final y, por lo tanto, leerán la variable global después de que se haya cambiado. Llamemos a esto el "orden natural", no definido por ningún estándar sino por pura lógica.
Sin embargo, en el proceso de optimización, los compiladores intentarán eliminar el paso intermedio de almacenar temporalmente el valor en algún lugar e intentar escribir el resultado de la función lo más directamente posible en el destino final y, en ese caso, a menudo tendrán que leer primero el índice , por ejemplo, a un registro, para poder mover directamente el resultado de la función a la matriz. Esto puede hacer que la variable global se lea antes de que se cambie.
Entonces, este es básicamente un comportamiento indefinido con la muy mala propiedad de que es bastante probable que el resultado sea diferente, dependiendo de si se realiza la optimización y qué tan agresiva es esta optimización. Es su tarea como desarrollador resolver ese problema codificando:
o codificación:
Como buena regla general: a menos que las variables globales sean
const
(o no lo sean, pero usted sabe que ningún código las cambiará como efecto secundario), nunca debe usarlas directamente en el código, como en un entorno de subprocesos múltiples, incluso esto puede ser indefinido:Como el compilador puede leerlo dos veces y otro hilo puede cambiar el valor entre las dos lecturas. Sin embargo, una vez más, la optimización definitivamente haría que el código solo lo lea una vez, por lo que es posible que nuevamente tenga resultados diferentes que ahora también dependen del momento de otro hilo. Por lo tanto, tendrá mucho menos dolor de cabeza si almacena variables globales en una variable de pila temporal antes de su uso. Tenga en cuenta que si el compilador cree que esto es seguro, lo más probable es que optimice incluso eso y, en su lugar, use la variable global directamente, por lo que al final, puede que no haya diferencia en el rendimiento o el uso de la memoria.
(En caso de que alguien pregunte por qué haría alguien en
x + 2 * x
lugar de hacerlo3 * x
, en algunas CPU la adición es ultrarrápida y también lo es la multiplicación por una potencia dos, ya que el compilador los convertirá en cambios de bits (2 * x == x << 1
), aunque la multiplicación con números arbitrarios puede ser muy lenta , por lo tanto, en lugar de multiplicar por 3, obtienes un código mucho más rápido cambiando bit x por 1 y agregando x al resultado, e incluso ese truco lo realizan los compiladores modernos si multiplicas por 3 y activas la optimización agresiva a menos que sea un objetivo moderno CPU donde la multiplicación es igual de rápida que la suma, ya que el truco ralentizaría el cálculo).fuente
3 * x
en dos lecturas de x. Puede leer x una vez y luego hacer el método x + 2 * x en el registro en el que lee xlanguage-lawyer
, donde el idioma en cuestión tiene su propio "significado muy especial" para indefinido , solo causarás confusión al no usar La definición del lenguaje.Edición global: lo siento chicos, me emocioné mucho y escribí muchas tonterías. Sólo un viejo geezer despotricando.
Quería creer que C se había salvado, pero desgraciadamente desde C11 se ha equiparado a C ++. Aparentemente, saber qué hará el compilador con los efectos secundarios en las expresiones requiere ahora resolver un pequeño acertijo matemático que involucra un ordenamiento parcial de secuencias de código basado en un "se ubica antes del punto de sincronización de".
Por casualidad, diseñé e implementé algunos sistemas integrados críticos en tiempo real en los días de K&R (incluido el controlador de un automóvil eléctrico que podría hacer que las personas se estrellaran contra la pared más cercana si el motor no se controlaba, un industrial de 10 toneladas robot que podría aplastar a las personas hasta convertirlo en una pulpa si no se ordena adecuadamente, y una capa de sistema que, aunque inofensiva, tendría unas pocas docenas de procesadores que absorberían su bus de datos con menos del 1% de sobrecarga del sistema).
Podría ser demasiado senil o estúpido para obtener la diferencia entre indefinido y no especificado, pero creo que todavía tengo una muy buena idea de lo que significa la ejecución concurrente y el acceso a datos. En mi opinión posiblemente informada, esta obsesión por los C ++ y ahora los chicos C con sus lenguajes de mascotas asumiendo los problemas de sincronización es un sueño costoso. O sabes lo que es la ejecución concurrente, y no necesitas ninguno de estos artilugios, o no, y le harías al mundo en general un favor sin tratar de meterse con él.
Toda esta carga de abstracciones de barrera de memoria que hacen agua la vista se debe simplemente a un conjunto temporal de limitaciones de los sistemas de caché de múltiples CPU, todos los cuales se pueden encapsular de forma segura en objetos de sincronización de SO comunes como, por ejemplo, los mutexes y las variables de condición C ++ ofertas.
El costo de esta encapsulación no es más que una caída diminuta en el rendimiento en comparación con lo que podría lograr el uso de instrucciones específicas de CPU específicas en algunos casos.
La
volatile
palabra clave (o un#pragma dont-mess-with-that-variable
para todos, como programador del sistema, cuidado) hubiera sido suficiente para decirle al compilador que deje de reordenar los accesos a la memoria. El código óptimo se puede generar fácilmente con directivas asm directas para rociar el controlador de bajo nivel y el código del sistema operativo con instrucciones específicas de CPU específicas. Sin un conocimiento íntimo de cómo funciona el hardware subyacente (sistema de caché o interfaz de bus), de todos modos está obligado a escribir código inútil, ineficiente o defectuoso.Un ajuste minucioso de la
volatile
palabra clave y Bob habría sido todo el mundo menos el tío de programadores de bajo nivel más duro. En lugar de eso, la pandilla habitual de fanáticos de las matemáticas de C ++ tuvo un día de campo diseñando otra abstracción incomprensible, cediendo a su tendencia típica de diseñar soluciones en busca de problemas inexistentes y confundiendo la definición de un lenguaje de programación con las especificaciones de un compilador.Solo que esta vez el cambio requirió desfigurar un aspecto fundamental de C también, ya que estas "barreras" tuvieron que generarse incluso en código C de bajo nivel para funcionar correctamente. Eso, entre otras cosas, causó estragos en la definición de expresiones, sin explicación o justificación alguna.
Como conclusión, el hecho de que un compilador pueda producir un código de máquina consistente a partir de esta pieza absurda de C es solo una consecuencia distante de la forma en que los chicos de C ++ hicieron frente a posibles inconsistencias de los sistemas de caché de finales de la década de 2000.
Se hizo un desastre terrible de un aspecto fundamental de C (definición de expresión), de modo que la gran mayoría de los programadores de C, a quienes no les importan los sistemas de caché, y con razón, ahora se ven obligados a confiar en los gurús para explicar el diferencia entre
a = b() + c()
ya = b + c
.Intentar adivinar qué será de este desafortunado conjunto es una pérdida neta de tiempo y esfuerzo de todos modos. Independientemente de lo que haga el compilador, este código es patológicamente incorrecto. Lo único responsable de hacerlo es enviarlo a la papelera.
Conceptualmente, los efectos secundarios siempre se pueden eliminar de las expresiones, con el esfuerzo trivial de permitir explícitamente que la modificación ocurra antes o después de la evaluación, en una declaración separada.
Este tipo de código de mierda podría haberse justificado en los años 80, cuando no se podía esperar que un compilador optimizara nada. Pero ahora que los compiladores se han vuelto más inteligentes que la mayoría de los programadores, todo lo que queda es un código de mierda.
Tampoco entiendo la importancia de este debate indefinido / no especificado. O puede confiar en el compilador para generar código con un comportamiento consistente o no puede. Si llama a eso indefinido o no especificado parece un punto discutible.
En mi opinión posiblemente informada, C ya es lo suficientemente peligroso en su estado K&R. Una evolución útil sería agregar medidas de seguridad de sentido común. Por ejemplo, al hacer uso de esta herramienta avanzada de análisis de código, las especificaciones obligan al compilador a implementar al menos para generar advertencias sobre el código de bonkers, en lugar de generar en silencio un código potencialmente poco confiable al extremo.
Pero en cambio, los chicos decidieron, por ejemplo, definir un orden de evaluación fijo en C ++ 17. Ahora, cada imbécil de software se incita activamente para poner efectos secundarios en su código a propósito, disfrutando de la certeza de que los nuevos compiladores manejarán ansiosamente la ofuscación de una manera determinista.
K&R fue una de las verdaderas maravillas del mundo de la informática. Por veinte dólares obtuviste una especificación completa del idioma (he visto a individuos solteros escribir compiladores completos simplemente usando este libro), un excelente manual de referencia (la tabla de contenido generalmente te indicará en un par de páginas de la respuesta a tu pregunta), y un libro de texto que le enseñaría a usar el lenguaje de una manera sensata. Completo con fundamentos, ejemplos y sabias palabras de advertencia sobre las numerosas formas en que podría abusar del lenguaje para hacer cosas muy, muy estúpidas.
Destruir esa herencia por tan poco beneficio me parece un desperdicio cruel. Pero de nuevo, bien podría dejar de ver el punto por completo. ¿Quizás algún alma amable podría señalarme en la dirección de un ejemplo de nuevo código C que aproveche significativamente estos efectos secundarios?
fuente
0,expr,0
.