Por que o gcc pode carregar especulativamente a partir de uma estrutura?
A função 'foo' no snippet abaixo carregará apenas um dos membros da estrutura A ou B; bem, pelo menos essa é a intenção do código não otimizado.
typedef struct {
int A;
int B;
} Pair;
int foo(const Pair *P, int c) {
int x;
if (c)
x = P->A;
else
x = P->B;
return c/102 + x;
}
Aqui está o que o gcc -O3 fornece:
mov eax, esi
mov edx, -1600085855
test esi, esi
mov ecx, DWORD PTR [rdi+4] <-- ***load P->B**
cmovne ecx, DWORD PTR [rdi] <-- ***load P->A***
imul edx
lea eax, [rdx+rsi]
sar esi, 31
sar eax, 6
sub eax, esi
add eax, ecx
ret
Portanto, parece que o gcc pode carregar especulativamente os dois membros da estrutura, a fim de eliminar ramificações. Mas então, o código a seguir é considerado um comportamento indefinido ou a otimização do gcc acima é ilegal?
#include <stdlib.h>
int naughty_caller(int c) {
Pair *P = (Pair*)malloc(sizeof(Pair)-1); // *** Allocation is enough for A but not for B ***
if (!P) return -1;
P->A = 0x42; // *** Initializing allocation only where it is guaranteed to be allocated ***
int res = foo(P, 1); // *** Passing c=1 to foo should ensure only P->A is accessed? ***
free(P);
return res;
}
Se a especulação de carga ocorrer no cenário acima, é possível que carregar P-> B cause uma exceção, pois o último byte de P-> B pode estar na memória não alocada. Essa exceção não ocorrerá se a otimização estiver desativada.
A questãoA otimização do gcc mostrada acima da especulação de carga é legal? Onde as especificações dizem ou sugerem que está tudo bem? Se a otimização é legal, como o código no 'naughtly_caller' acaba sendo um comportamento indefinido?