switch - ¿C++ permite que un compilador de optimización ignore los efectos secundarios en la condición forzada?
for() c++ (2)
El estándar aclara clara e inequívocamente que las dos declaraciones de upper_bound
refieren al mismo objeto.
3.5 Programa y enlace [basic.link]
9 Dos nombres que son iguales (Cláusula 3) y que se declaran en diferentes ámbitos denotan la misma variable, función, tipo, enumerador, plantilla o espacio de nombres si
- ambos nombres tienen un enlace externo o bien ambos nombres tienen un enlace interno y se declaran en la misma unidad de traducción; y
- ambos nombres se refieren a miembros del mismo espacio de nombres o a miembros, no por herencia, de la misma clase; y
- cuando ambos nombres denotan funciones, las listas de tipos de parámetros de las funciones (8.3.5) son idénticas; y
- cuando ambos nombres denotan plantillas de funciones, las firmas (14.5.6.1) son las mismas.
Ambos nombres tienen un enlace externo. Ambos nombres se refieren a un miembro en el espacio de nombres global. Ninguno de los nombres denota una función o una plantilla de función. Por lo tanto, ambos nombres se refieren al mismo objeto. Sugerir que el hecho de que haya declaraciones separadas invalide hechos tan básicos es como decir que int i = 0; int &j = i; j = 1; return i;
int i = 0; int &j = i; j = 1; return i;
podría devolver cero, porque el compilador podría haber olvidado a qué se refiere j
. Por supuesto que debe regresar 1. Esto tiene que funcionar, simple y llanamente. Si no es así, has encontrado un error en el compilador.
Mientras depuraba algún código heredado tropecé con el comportamiento del compilador sorprendente (para mí). Ahora quisiera saber si alguna cláusula en la especificación de C ++ permite la siguiente optimización, donde se ignoran los efectos secundarios de una llamada de función en la condición for:
void bar()
{
extern int upper_bound;
upper_bound--;
}
void foo()
{
extern int upper_bound; // from some other translation unit, initially ~ 10
for (int i = 0; i < upper_bound; ) {
bar();
}
}
En el ensamblaje resultante hay una ruta de control en la que upper_bound
se conserva en un registro y la disminución de upper_bound
en bar()
nunca tiene efecto.
Mi compilador es Microsoft Visual C ++ 11.00.60610.1.
Honestamente, no veo mucho margen de N3242 en 6.5.3 y 6.5.1 de N3242 pero quiero estar seguro de que no me falta algo obvio.
Este comportamiento parece ser correcto si profundiza un poco en el estándar.
La primera pista está en la nota en la sección 3.3.1 / 4, que dice:
Las declaraciones externas locales (3.5) pueden introducir un nombre en la región declarativa donde aparece la declaración y también introducir un nombre (posiblemente no visible) en un espacio de nombre adjunto;
Que es un poco vago y parece implicar que el compilador no necesita introducir el nombre upper_bound
en el contexto global al pasar por la función bar()
, y por lo tanto, cuando upper_bound
aparece en la función foo()
, no hay conexión hecho entre esas dos variables externas, y por lo tanto, bar()
no tiene ningún efecto secundario en la medida que el compilador sabe, y por lo tanto, la optimización se convierte en un bucle infinito (a menos que upper_bound sea cero para empezar).
Pero este lenguaje vago no es suficiente, y es solo una nota de advertencia, no un requisito formal.
Afortunadamente, hay una precisión más adelante, en la sección 3.5 / 7, que dice lo siguiente:
Cuando una declaración de alcance de bloque de una entidad con enlace no se encuentra para referirse a alguna otra declaración, entonces esa entidad es un miembro del espacio de nombre más interno que lo rodea. Sin embargo, tal declaración no introduce el nombre del miembro en su ámbito de espacio de nombres.
E incluso brindan un ejemplo:
namespace X {
void p() {
q(); // error: q not yet declared
extern void q(); // q is a member of namespace X
}
void middle() {
q(); // error: q not yet declared
}
}
que es directamente aplicable al ejemplo que diste.
Entonces, el núcleo del problema es que se requiere que el compilador no haga la asociación entre la primera declaración upper_bound
(en la barra) y la segunda (en foo).
Entonces, examinemos la implicación para la optimización de las dos declaraciones upper_bound
se supone que están desconectadas. El compilador entiende el código de esta manera:
void bar()
{
extern int upper_bound_1;
upper_bound_1--;
}
void foo()
{
extern int upper_bound_2;
for (int i = 0; i < upper_bound_2; ) {
bar();
}
}
Que se convierte en lo siguiente, debido a la función de alineación de la barra:
void foo()
{
extern int upper_bound_1;
extern int upper_bound_2;
while( 0 < upper_bound_2 ) {
upper_bound_1--;
}
}
Lo cual es claramente un bucle infinito (en la medida en que el compilador lo sabe), e incluso si upper_bound
se declaró volatile
, simplemente tendría un punto de terminación indefinido (siempre que upper_bound
se establezca externamente en 0 o menos). Y la disminución de una variable ( upper_bound_1
) una cantidad infinita (o indefinida) de veces tiene un comportamiento indefinido, debido al desbordamiento. Por lo tanto, el compilador puede elegir no hacer nada, obviamente es un comportamiento permitido cuando se trata de un comportamiento indefinido. Y entonces, el código se convierte en:
void foo()
{
extern int upper_bound_2;
while( 0 < upper_bound_2 ) { };
}
Que es exactamente lo que se ve en la lista de ensambles para la función que produce GCC 4.8.2 (con -O3
):
.globl _Z3foov
.type _Z3foov, @function
_Z3foov:
.LFB1:
.cfi_startproc
movl upper_bound(%rip), %eax
testl %eax, %eax
jle .L6
.L5:
jmp .L5
.p2align 4,,10
.p2align 3
.L6:
rep ret
.cfi_endproc
.LFE1:
.size _Z3foov, .-_Z3foov
Que se puede arreglar agregando una declaración de alcance global de la variable extern, como tal:
extern int upper_bound;
void bar()
{
extern int upper_bound;
upper_bound--;
}
void foo()
{
extern int upper_bound;
for (int i = 0; i < upper_bound; ) {
bar();
}
}
Que produce este conjunto:
_Z3foov:
.LFB1:
.cfi_startproc
movl upper_bound(%rip), %eax
testl %eax, %eax
jle .L2
movl $0, upper_bound(%rip)
.L2:
rep ret
.cfi_endproc
.LFE1:
.size _Z3foov, .-_Z3foov
Cuál es el comportamiento previsto, es decir, el comportamiento observable de foo()
es equivalente a:
void foo()
{
extern int upper_bound;
upper_bound = 0;
}