señales - teorema de nyquist demostracion
¿Cuál es la regla estricta de aliasing? (11)
Nota
Esto es un extracto de mi "¿Qué es la regla de alias estricta y por qué nos importa?" escribir
¿Qué es el aliasing estricto?
En C y C ++, el alias tiene que ver con los tipos de expresión a los que se nos permite acceder a los valores almacenados. Tanto en C como en C ++, el estándar especifica qué tipos de expresión tienen permitido alias que tipos. El compilador y el optimizador pueden asumir que seguimos estrictamente las reglas de alias, de ahí el término regla de alias estricta . Si intentamos acceder a un valor utilizando un tipo no permitido, se clasifica como comportamiento indefinido ( UB ). Una vez que tenemos un comportamiento indefinido, todas las apuestas están apagadas, los resultados de nuestro programa ya no son confiables.
Desafortunadamente, con infracciones estrictas de alias, a menudo obtendremos los resultados que esperamos, dejando la posibilidad de que una versión futura de un compilador con una nueva optimización romperá el código que pensamos que era válido. Esto es indeseable y es un objetivo que vale la pena entender las reglas estrictas de alias y cómo evitar violarlas.
Para entender más acerca de por qué nos importa, discutiremos los problemas que surgen cuando se violan las reglas estrictas de aliasing, el punteo de tipo ya que las técnicas comunes utilizadas en el punteo de tipo a menudo violan las reglas de aliasing estrictas y cómo tipear el punteo correctamente.
Ejemplos preliminares
Veamos algunos ejemplos, luego podemos hablar exactamente sobre lo que dicen los estándares, examinar algunos ejemplos adicionales y luego ver cómo evitar las infracciones estrictas de aliasing y captura que omitimos. Aquí hay un ejemplo que no debería ser sorprendente ( ejemplo en vivo ):
int x = 10;
int *ip = &x;
std::cout << *ip << "/n";
*ip = 12;
std::cout << x << "/n";
Tenemos un int * que apunta a la memoria ocupada por un int y este es un alias válido. El optimizador debe asumir que las asignaciones a través de ip podrían actualizar el valor ocupado por x .
El siguiente ejemplo muestra un alias que conduce a un comportamiento indefinido ( ejemplo en vivo ):
int foo( float *f, int *i ) {
*i = 1;
*f = 0.f;
return *i;
}
int main() {
int x = 0;
std::cout << x << "/n"; // Expect 0
x = foo(reinterpret_cast<float*>(&x), &x);
std::cout << x << "/n"; // Expect 0?
}
En la función foo tomamos un int * y un float * , en este ejemplo llamamos foo y configuramos ambos parámetros para que apunten a la misma ubicación de memoria que en este ejemplo contiene un int . Tenga en cuenta que reinterpret_cast le dice al compilador que trate la expresión como si tuviera el tipo específico por su parámetro de plantilla. En este caso, le indicamos que trate la expresión & x como si tuviera el tipo float * . Podemos ingenuamente esperar que el resultado del segundo cout sea 0, pero con la optimización habilitada usando -O2, tanto gcc como clang producen el siguiente resultado:
0
1
Lo que puede no ser esperado, pero es perfectamente válido ya que hemos invocado un comportamiento indefinido. Un flotante no puede alias válidamente un objeto int . Por lo tanto, el optimizador puede asumir la constante 1 almacenada cuando la desreferenciación i será el valor de retorno, ya que un almacenamiento a través de f no podría afectar válidamente un objeto int . Al conectar el código en el Explorador del compilador se muestra que esto es exactamente lo que está sucediendo ( ejemplo en vivo ):
foo(float*, int*): # @foo(float*, int*)
mov dword ptr [rsi], 1
mov dword ptr [rdi], 0
mov eax, 1
ret
El optimizador que utiliza el análisis de alias basado en tipo (TBAA) supone que se devolverá 1 y mueve directamente el valor constante al registro eax que lleva el valor de retorno. TBAA usa las reglas de idiomas sobre qué tipos están permitidos al alias para optimizar cargas y almacenes. En este caso, TBAA sabe que un flotante no puede alias e int y optimiza la carga de i .
Ahora, al libro de reglas
¿Qué dice exactamente la norma que se nos permite y no se nos permite hacer? El lenguaje estándar no es sencillo, por lo que para cada elemento intentaré proporcionar ejemplos de código que demuestren su significado.
¿Qué dice la norma C11?
El estándar C11 dice lo siguiente en la sección 6.5, Expresiones, párrafo 7 :
Un objeto debe tener acceso a su valor almacenado solo por una expresión de valor l que tenga uno de los siguientes tipos: 88) - un tipo compatible con el tipo efectivo del objeto,
int x = 1;
int *p = &x;
printf("%d/n", *p); // *p gives us an lvalue expression of type int which is compatible with int
- una versión calificada de un tipo compatible con el tipo efectivo del objeto,
int x = 1;
const int *p = &x;
printf("%d/n", *p); // *p gives us an lvalue expression of type const int which is compatible with int
- un tipo que es el tipo con signo o sin signo correspondiente al tipo efectivo del objeto,
int x = 1;
unsigned int *p = (unsigned int*)&x;
printf("%u/n", *p ); // *p gives us an lvalue expression of type unsigned int which corresponds to
// the effective type of the object
gcc / clang tiene una extensión y also permite asignar int * sin signo a int * aunque no sean tipos compatibles.
- un tipo que es el tipo firmado o sin firmar correspondiente a una versión calificada del tipo efectivo del objeto,
int x = 1;
const unsigned int *p = (const unsigned int*)&x;
printf("%u/n", *p ); // *p gives us an lvalue expression of type const unsigned int which is a unsigned type
// that corresponds with to a qualified verison of the effective type of the object
- un tipo agregado o de unión que incluye uno de los tipos mencionados anteriormente entre sus miembros (incluido, recursivamente, un miembro de una subagregación o unión contenida), o
struct foo {
int x;
};
void foobar( struct foo *fp, int *ip ); // struct foo is an aggregate that includes int among its members so it can
// can alias with *ip
foo f;
foobar( &f, &f.x );
- Un tipo de personaje.
int x = 65;
char *p = (char *)&x;
printf("%c/n", *p ); // *p gives us an lvalue expression of type char which is a character type.
// The results are not portable due to endianness issues.
Lo que dice el Proyecto de Norma C ++ 17
El proyecto de norma C ++ 17 en la sección [basic.lval] párrafo 11 dice:
Si un programa intenta acceder al valor almacenado de un objeto a través de un glvalue distinto de uno de los siguientes tipos, el comportamiento no está definido: 63 (11.1) - el tipo dinámico del objeto,
void *p = malloc( sizeof(int) ); // We have allocated storage but not started the lifetime of an object
int *ip = new (p) int{0}; // Placement new changes the dynamic type of the object to int
std::cout << *ip << "/n"; // *ip gives us a glvalue expression of type int which matches the dynamic type
// of the allocated object
(11.2) - una versión calificada por cv del tipo dinámico del objeto,
int x = 1;
const int *cip = &x;
std::cout << *cip << "/n"; // *cip gives us a glvalue expression of type const int which is a cv-qualified
// version of the dynamic type of x
(11.3) - un tipo similar (como se define en 7.5) al tipo dinámico del objeto,
(11.4) - un tipo que es el tipo firmado o sin firmar correspondiente al tipo dinámico del objeto,
// Both si and ui are signed or unsigned types corresponding to each others dynamic types
// We can see from this godbolt(https://godbolt.org/g/KowGXB) the optimizer assumes aliasing.
signed int foo( signed int &si, unsigned int &ui ) {
si = 1;
ui = 2;
return si;
}
(11.5) - un tipo que es el tipo firmado o sin firmar correspondiente a una versión calificada por cv del tipo dinámico del objeto,
signed int foo( const signed int &si1, int &si2); // Hard to show this one assumes aliasing
(11.6) - un tipo agregado o de unión que incluye uno de los tipos mencionados anteriormente entre sus elementos o miembros de datos no estáticos (incluido, recursivamente, un elemento o miembro de datos no estáticos de un subagregado o unión contenida),
struct foo {
int x;
};
// Compiler Explorer example(https://godbolt.org/g/z2wJTC) shows aliasing assumption
int foobar( foo &fp, int &ip ) {
fp.x = 1;
ip = 2;
return fp.x;
}
foo f;
foobar( f, f.x );
(11.7) - un tipo que es un tipo de clase base (posiblemente cv calificado) del tipo dinámico del objeto,
struct foo { int x ; };
struct bar : public foo {};
int foobar( foo &f, bar &b ) {
f.x = 1;
b.x = 2;
return f.x;
}
(11.8) - un tipo char, unsigned char o std :: byte.
int foo( std::byte &b, uint32_t &ui ) {
b = static_cast<std::byte>(''a'');
ui = 0xFFFFFFFF;
return std::to_integer<int>( b ); // b gives us a glvalue expression of type std::byte which can alias
// an object of type uint32_t
}
Vale la pena señalar que el carácter firmado no está incluido en la lista anterior, esta es una diferencia notable de C que dice un tipo de carácter .
¿Qué es el Tipo Punning?
Hemos llegado a este punto y es posible que nos estemos preguntando, ¿para qué queremos un alias? La respuesta generalmente es escribir pun , a menudo los métodos utilizados violan las reglas estrictas de alias.
A veces queremos burlar el sistema de tipos e interpretar un objeto como un tipo diferente. Esto se denomina punning de tipo , para reinterpretar un segmento de la memoria como otro tipo. El tipo punning es útil para las tareas que desean acceder a la representación subyacente de un objeto para ver, transportar o manipular. Las áreas típicas en las que encontramos que se utiliza el punning de tipo son compiladores, serialización, código de red, etc.
Tradicionalmente, esto se ha logrado tomando la dirección del objeto, convirtiéndolo en un puntero del tipo que queremos reinterpretarlo y luego accediendo al valor, o en otras palabras, mediante el alias. Por ejemplo:
int x = 1 ;
// In C
float *fp = (float*)&x ; // Not a valid aliasing
// In C++
float *fp = reinterpret_cast<float*>(&x) ; // Not a valid aliasing
printf( "%f/n", *fp ) ;
Como hemos visto anteriormente, este no es un alias válido, por lo que estamos invocando un comportamiento indefinido. Pero tradicionalmente los compiladores no se aprovechaban de las reglas estrictas de alias y este tipo de código por lo general solo funcionaba, desafortunadamente los desarrolladores se han acostumbrado a hacer las cosas de esta manera. Un método alternativo común para el punning de tipo es a través de uniones, que es válido en C pero comportamiento indefinido en C ++ ( ver ejemplo en vivo ):
union u1
{
int n;
float f;
} ;
union u1 u;
u.f = 1.0f;
printf( "%d/n”, u.n ); // UB in C++ n is not the active member
Esto no es válido en C ++ y algunos consideran que el propósito de las uniones es únicamente para implementar tipos de variantes y sentir que el uso de uniones para tipificar el castigo es un abuso.
¿Cómo tipeamos Pun correctamente?
El método estándar para tipear punning en C y C ++ es memcpy . Esto puede parecer un poco pesado, pero el optimizador debería reconocer el uso de memcpy para el punteo de tipo y optimizarlo y generar un registro para registrar movimientos. Por ejemplo, si sabemos que int64_t es del mismo tamaño que double :
static_assert( sizeof( double ) == sizeof( int64_t ) ); // C++17 does not require a message
podemos usar memcpy :
void func1( double d ) {
std::int64_t n;
std::memcpy(&n, &d, sizeof d);
//...
A un nivel de optimización suficiente, cualquier compilador moderno decente genera un código idéntico al método reinterpret_cast o método de unión mencionado anteriormente para el punning de tipo . Al examinar el código generado, vemos que solo usa el registro mov ( ejemplo del compilador en vivo del Explorador ).
C ++ 20 y bit_cast
En C ++ 20 podemos obtener bit_cast ( implementación disponible en el enlace de la propuesta ), lo que proporciona una forma sencilla y segura de escribir con letra y ser utilizable en un contexto constexpr.
El siguiente es un ejemplo de cómo usar bit_cast para escribir pun un unsigned int para flotar , ( verlo en vivo ):
std::cout << bit_cast<float>(0x447a0000) << "/n" ; //assuming sizeof(float) == sizeof(unsigned int)
En el caso de que los tipos A y De no tengan el mismo tamaño, es necesario que utilicemos una estructura intermedia15. Usaremos una estructura que contenga una matriz de caracteres sizeof (unsigned int) ( asume 4 byte unsigned int ) para ser el tipo From y unsigned int como el tipo To . :
struct uint_chars {
unsigned char arr[sizeof( unsigned int )] = {} ; // Assume sizeof( unsigned int ) == 4
};
// Assume len is a multiple of 4
int bar( unsigned char *p, size_t len ) {
int result = 0;
for( size_t index = 0; index < len; index += sizeof(unsigned int) ) {
uint_chars f;
std::memcpy( f.arr, &p[index], sizeof(unsigned int));
unsigned int result = bit_cast<unsigned int>(f);
result += foo( result );
}
return result ;
}
Es lamentable que necesitemos este tipo intermedio, pero esa es la restricción actual de bit_cast .
Cogiendo estrictas violaciones de alias
No tenemos muchas herramientas buenas para capturar un alias estricto en C ++, las herramientas que tenemos detectarán algunos casos de violaciones estrictas de alias y algunos casos de cargas y tiendas desalineadas.
gcc utilizando la bandera -fstrict-aliasing y -Wstrict-aliasing puede detectar algunos casos, aunque no sin falsos positivos / negativos. Por ejemplo, los siguientes casos generarán una advertencia en gcc (véalo en vivo ):
int a = 1;
short j;
float f = 1.f; // Originally not initialized but tis-kernel caught
// it was being accessed w/ an indeterminate value below
printf("%i/n", j = *(reinterpret_cast<short*>(&a)));
printf("%i/n", j = *(reinterpret_cast<int*>(&f)));
aunque no detectará este caso adicional ( verlo en vivo ):
int *p;
p=&a;
printf("%i/n", j = *(reinterpret_cast<short*>(p)));
Aunque Clang permite estas banderas, aparentemente no implementa las advertencias.
Otra herramienta que tenemos disponible para nosotros es ASan, que puede detectar cargas y almacenes desalineados. Aunque estas no son violaciones de alias directamente estrictas, son un resultado común de violaciones de alias estrictas. Por ejemplo, los siguientes casos generarán errores de tiempo de ejecución cuando se construyan con Clang usando -fsanitize = address
int *x = new int[2]; // 8 bytes: [0,7].
int *u = (int*)((char*)x + 6); // regardless of alignment of x this will not be an aligned address
*u = 1; // Access to range [6-9]
printf( "%d/n", *u ); // Access to range [6-9]
La última herramienta que recomendaré es específica de C ++ y no es estrictamente una herramienta, sino una práctica de codificación, no permita conversiones de estilo C Tanto gcc como clang producirán un diagnóstico para los modelos de estilo C usando -Wold-style-cast . Esto obligará a cualquier juego de palabras de tipo no definido a usar reinterpret_cast, en general reinterpret_cast debería ser un indicador para una revisión más cercana del código. También es más fácil buscar en su base de código para reinterpret_cast para realizar una auditoría.
Para C ya tenemos todas las herramientas cubiertas y también tenemos tis-intérprete, un analizador estático que analiza exhaustivamente un programa para un gran subconjunto del lenguaje C. Dadas las verificaciones en C del ejemplo anterior donde el uso de -fstrict-aliasing pierde un caso ( véalo en vivo )
int a = 1;
short j;
float f = 1.0 ;
printf("%i/n", j = *((short*)&a));
printf("%i/n", j = *((int*)&f));
int *p;
p=&a;
printf("%i/n", j = *((short*)p));
tis-interpeter puede capturar los tres, el siguiente ejemplo invoca a tis-kernal como tis-intérprete (la salida se edita por brevedad):
./bin/tis-kernel -sa example1.c
...
example1.c:9:[sa] warning: The pointer (short *)(& a) has type short *. It violates strict aliasing
rules by accessing a cell with effective type int.
...
example1.c:10:[sa] warning: The pointer (int *)(& f) has type int *. It violates strict aliasing rules by
accessing a cell with effective type float.
Callstack: main
...
example1.c:15:[sa] warning: The pointer (short *)p has type short *. It violates strict aliasing rules by
accessing a cell with effective type int.
Finalmente está TySan que actualmente está en desarrollo. Este desinfectante agrega información de verificación de tipo en un segmento de memoria virtual y verifica los accesos para ver si violan las reglas de aliasing. La herramienta potencialmente debería poder detectar todas las violaciones de alias, pero puede tener una gran sobrecarga de tiempo de ejecución.
Al preguntar sobre el comportamiento común indefinido en C , las almas están más iluminadas de lo que me he referido a la regla estricta de aliasing.
De qué están hablando?
Como apéndice a lo que Doug T. ya escribió, aquí hay un caso de prueba simple que probablemente lo active con gcc:
cheque.c
#include <stdio.h>
void check(short *h,long *k)
{
*h=5;
*k=6;
if (*h == 5)
printf("strict aliasing problem/n");
}
int main(void)
{
long k[1];
check((short *)k,k);
return 0;
}
Compilar con gcc -O2 -o check check.c
. Generalmente (con la mayoría de las versiones de gcc que probé), esto genera un "problema de alias estricto", porque el compilador asume que "h" no puede ser la misma dirección que "k" en la función "verificar". Por eso el compilador optimiza el if (*h == 5)
distancia y siempre llama a printf.
Para aquellos que estén interesados aquí está el código de ensamblador x64, producido por gcc 4.6.3, que se ejecuta en ubuntu 12.04.2 para x64:
movw $5, (%rdi)
movq $6, (%rsi)
movl $.LC0, %edi
jmp puts
Así que la condición if ha desaparecido completamente del código del ensamblador.
El alias estricto no se refiere solo a los punteros, también afecta a las referencias, escribí un artículo al respecto para la wiki del desarrollador de boost y fue tan bien recibido que lo convertí en una página en mi sitio web de consultoría. Explica completamente qué es, por qué confunde tanto a la gente y qué hacer al respecto. Libro Blanco de Aliasing Estricto . En particular, explica por qué las uniones son un comportamiento riesgoso para C ++, y por qué usar memcpy es la única solución portátil en C y C ++. Espero que esto sea de ayuda.
Esta es la regla estricta de alias, que se encuentra en la sección 3.10 del estándar C ++ 03 (otras respuestas proporcionan una buena explicación, pero ninguna proporcionó la regla en sí):
Si un programa intenta acceder al valor almacenado de un objeto a través de un lvalor distinto de uno de los siguientes tipos, el comportamiento no está definido:
- el tipo dinámico del objeto,
- una versión cv calificada del tipo dinámico del objeto,
- un tipo que es el tipo con signo o sin signo correspondiente al tipo dinámico del objeto,
- un tipo que es el tipo firmado o sin firmar correspondiente a una versión calificada con CV del tipo dinámico del objeto,
- un tipo agregado o de unión que incluye uno de los tipos mencionados anteriormente entre sus miembros (incluido, recursivamente, un miembro de un subagregado o unión contenida),
- un tipo que es un tipo de clase base (posiblemente cv calificado) del tipo dinámico del objeto,
- un tipo
char
ounsigned char
.
Redacción de C ++ 11 y C ++ 14 (cambios enfatizados):
Si un programa intenta acceder al valor almacenado de un objeto a través de un glvalue distinto de uno de los siguientes tipos, el comportamiento no está definido:
- el tipo dinámico del objeto,
- una versión cv calificada del tipo dinámico del objeto,
- un tipo similar (como se define en 4.4) al tipo dinámico del objeto,
- un tipo que es el tipo con signo o sin signo correspondiente al tipo dinámico del objeto,
- un tipo que es el tipo firmado o sin firmar correspondiente a una versión calificada con CV del tipo dinámico del objeto,
- un tipo agregado o de unión que incluye uno de los tipos mencionados anteriormente entre sus elementos o miembros de datos no estáticos (incluido, recursivamente, un elemento o miembro de datos no estáticos de un subagregado o unión contenida),
- un tipo que es un tipo de clase base (posiblemente cv calificado) del tipo dinámico del objeto,
- un tipo
char
ounsigned char
.
Dos cambios fueron pequeños: glvalue en lugar de lvalue , y aclaración del caso agregado / unión.
El tercer cambio hace una garantía más fuerte (relaja la regla de aliasing fuerte): el nuevo concepto de tipos similares que ahora son seguros para el alias.
También la redacción en C (C99; ISO / IEC 9899: 1999 6.5 / 7; la misma redacción se usa exactamente en ISO / IEC 9899: 2011 §6.5 ¶7):
Un objeto tendrá acceso a su valor almacenado solo por una expresión de valor l que tenga uno de los siguientes tipos 73) u 88) :
- un tipo compatible con el tipo efectivo del objeto,
- una versión calificada de un tipo compatible con el tipo efectivo del objeto,
- un tipo que es el tipo firmado o sin signo correspondiente al tipo efectivo del objeto,
- un tipo que es el tipo firmado o sin firmar correspondiente a una versión cali fi cada del tipo efectivo del objeto,
- un tipo agregado o de unión que incluye uno de los tipos mencionados anteriormente entre sus miembros (incluido, recursivamente, un miembro de una subagregación o unión contenida), o
- un tipo de personaje.
73) o 88) La intención de esta lista es especificar aquellas circunstancias en las que un objeto puede o no tener un alias.
La mejor explicación que he encontrado es por Mike Acton, Understanding Strict Aliasing . Está enfocado un poco en el desarrollo de PS3, pero eso es básicamente solo GCC.
Del artículo:
"El alias estricto es una suposición, hecha por el compilador de C (o C ++), de que al eliminar la referencia de los punteros a objetos de diferentes tipos nunca se referirá a la misma ubicación de memoria (es decir, alias).
Básicamente, si tiene un int*
apunta a una memoria que contiene un int
y luego apunta un float*
a esa memoria y lo usa como un float
usted rompe la regla. Si su código no respeta esto, entonces el optimizador del compilador probablemente rompa su código.
La excepción a la regla es un char*
, que puede apuntar a cualquier tipo.
Una situación típica que encuentra problemas estrictos de alias es cuando se superpone una estructura (como un mensaje de dispositivo / red) en un búfer del tamaño de palabra de su sistema (como un puntero a uint32_t
s o uint16_t
s). Cuando superpone una estructura en un búfer de este tipo, o un búfer en una estructura de este tipo mediante el lanzamiento de punteros, puede violar fácilmente las reglas estrictas de aliasing.
Entonces, en este tipo de configuración, si deseo enviar un mensaje a algo, tendré que tener dos punteros incompatibles que apuntan a la misma porción de memoria. Entonces podría codificar ingenuamente algo como esto:
typedef struct Msg
{
unsigned int a;
unsigned int b;
} Msg;
void SendWord(uint32_t);
int main(void)
{
// Get a 32-bit buffer from the system
uint32_t* buff = malloc(sizeof(Msg));
// Alias that buffer through message
Msg* msg = (Msg*)(buff);
// Send a bunch of messages
for (int i =0; i < 10; ++i)
{
msg->a = i;
msg->b = i+1;
SendWord(buff[0]);
SendWord(buff[1]);
}
}
La regla de alias estricta hace que esta configuración sea ilegal: eliminar la referencia a un puntero que asigna un alias a un objeto que no es de un tipo compatible o uno de los otros tipos permitidos por C 2011 6.5 párrafo 7 1 es un comportamiento indefinido. Desafortunadamente, aún puede codificar de esta manera, tal vez obtener algunas advertencias, compilarlo bien, solo para tener un comportamiento extraño e inesperado cuando ejecuta el código.
(GCC parece ser algo inconsistente en su capacidad para dar advertencias de aliasing, a veces nos da una advertencia amistosa y otras no).
Para ver por qué este comportamiento no está definido, tenemos que pensar en qué es lo que la regla estricta de alias compra al compilador. Básicamente, con esta regla, no tiene que pensar en insertar instrucciones para actualizar el contenido de buff
cada ejecución del bucle. En su lugar, al optimizar, con algunas suposiciones molestas y no aplicadas sobre el alias, puede omitir esas instrucciones, cargar el buff[0]
y el buff[1
] en los registros de la CPU una vez antes de que se ejecute el bucle, y acelerar el cuerpo del bucle. Antes de que se introdujera un alias estricto, el compilador tenía que vivir en un estado de paranoia que el contenido de buff
podía cambiar en cualquier momento y desde cualquier lugar por cualquiera. Por lo tanto, para obtener una ventaja de rendimiento adicional, y suponiendo que la mayoría de las personas no escriben punteros, se introdujo la regla estricta de aliasing.
Tenga en cuenta que si cree que el ejemplo está diseñado, esto incluso podría suceder si está pasando un búfer a otra función que realiza el envío por usted, si por el contrario lo tiene.
void SendMessage(uint32_t* buff, size_t size32)
{
for (int i = 0; i < size32; ++i)
{
SendWord(buff[i]);
}
}
Y reescribió nuestro bucle anterior para aprovechar esta función conveniente
for (int i = 0; i < 10; ++i)
{
msg->a = i;
msg->b = i+1;
SendMessage(buff, 2);
}
El compilador puede o no puede o lo suficientemente inteligente como para intentar en línea SendMessage y puede o no decidir cargar o no cargar buff nuevamente. Si SendMessage
es parte de otra API que se compila por separado, es probable que tenga instrucciones para cargar el contenido de buff. Por otra parte, quizás esté en C ++ y esta es una implementación de encabezado con plantilla que el compilador cree que puede incluir en línea. O tal vez solo sea algo que escribiste en tu archivo .c para tu propia conveniencia. De todos modos, un comportamiento indefinido podría seguir. Incluso cuando sabemos algo de lo que está sucediendo bajo el capó, sigue siendo una violación de la regla, por lo que no se garantiza un comportamiento bien definido. Así que solo envolviendo una función que toma nuestro búfer delimitado por palabras no necesariamente ayuda.
Entonces, ¿cómo puedo solucionar esto?
Utilice una unión. La mayoría de los compiladores admiten esto sin quejarse de un alias estricto. Esto se permite en C99 y se permite explícitamente en C11.
union { Msg msg; unsigned int asBuffer[sizeof(Msg)/sizeof(unsigned int)]; };
Puede deshabilitar el alias estricto en su compilador ( f[no-]strict-aliasing en gcc))
Puede usar
char*
para crear alias en lugar de la palabra de su sistema. Las reglas permiten una excepción parachar*
(incluyendosigned char
yunsigned char
). Siempre se asume quechar*
alias otros tipos. Sin embargo, esto no funcionará de la otra manera: no se supone que su estructura asigne un alias a un búfer de caracteres.
Principiante cuidado
Este es solo un campo minado potencial cuando se superponen dos tipos entre sí. También debe aprender sobre el endianness , la alineación de palabras y cómo tratar los problemas de alineación a través de estructuras de empaque correctamente.
Nota
1 Los tipos a los que C 2011 6.5 7 permite que acceda un lvalue son:
- un tipo compatible con el tipo efectivo del objeto,
- una versión calificada de un tipo compatible con el tipo efectivo del objeto,
- un tipo que es el tipo firmado o sin signo correspondiente al tipo efectivo del objeto,
- un tipo que es el tipo firmado o sin firmar correspondiente a una versión calificada del tipo efectivo del objeto,
- un tipo agregado o de unión que incluye uno de los tipos mencionados anteriormente entre sus miembros (incluido, recursivamente, un miembro de una subagregación o unión contenida), o
- un tipo de personaje.
El punning de tipo a través de lanzamientos de punteros (en lugar de usar una unión) es un ejemplo importante de romper un alias estricto.
De acuerdo con la justificación del C89, los autores de la Norma no querían exigir que los compiladores reciban un código como:
int x;
int test(double *p)
{
x=5;
*p = 1.0;
return x;
}
debe requerirse que se vuelva a cargar el valor de x
entre la asignación y la declaración de retorno para permitir la posibilidad que p
podría apuntar x
, y la asignación *p
podría alterar el valor de x
. La noción de que un compilador debe tener derecho a suponer que no habrá alias en situaciones como la anterior no fue controversial.
Desafortunadamente, los autores del C89 escribieron su regla de una manera que, si se leyera literalmente, haría que incluso la siguiente función invoque un comportamiento indefinido:
void test(void)
{
struct S {int x;} s;
s.x = 1;
}
porque utiliza un lvalor de tipo int
para acceder a un objeto de tipo struct S
y int
no se encuentra entre los tipos que se pueden usar para acceder a struct S
. Debido a que sería absurdo tratar todo uso de miembros de estructuras y uniones que no son de carácter como comportamiento indefinido, casi todos reconocen que hay al menos algunas circunstancias en las que se puede usar un valor de un tipo para acceder a un objeto de otro tipo . Desafortunadamente, el Comité de Normas de C no ha definido cuáles son esas circunstancias.
Gran parte del problema es el resultado del Informe de defectos # 028, que preguntó sobre el comportamiento de un programa como:
int test(int *ip, double *dp)
{
*ip = 1;
*dp = 1.23;
return *ip;
}
int test2(void)
{
union U { int i; double d; } u;
return test(&u.i, &u.d);
}
El Informe de defectos # 28 indica que el programa invoca un comportamiento indefinido porque la acción de escribir un miembro de la unión de tipo "doble" y leer uno de tipo "int" invoca un comportamiento definido por la implementación. Este razonamiento no tiene sentido, pero forma la base de las reglas de Tipo Efectivo que complican innecesariamente el lenguaje sin hacer nada para resolver el problema original.
La mejor manera de resolver el problema original sería tratar la nota a pie de página sobre el propósito de la regla como si fuera una norma, e hizo que la regla fuera inaplicable, excepto en los casos que realmente involucran accesos en conflicto usando alias. Dado algo como:
void inc_int(int *p) { *p = 3; }
int test(void)
{
int *p;
struct S { int x; } s;
s.x = 1;
p = &s.x;
inc_int(p);
return s.x;
}
No hay conflicto interno inc_int
porque todos los accesos al almacenamiento a los que se accede *p
se realizan con un valor de tipo int
, y no hay conflicto test
porque p
se deriva de forma visible struct S
, y la próxima vez que s
se use, todos los accesos a ese almacenamiento se harán A través de p
ya habrá pasado.
Si el código fuera cambiado ligeramente ...
void inc_int(int *p) { *p = 3; }
int test(void)
{
int *p;
struct S { int x; } s;
p = &s.x;
s.x = 1; // !!*!!
*p += 1;
return s.x;
}
Aquí, hay un conflicto de aliasing p
y el acceso a sx
la línea marcada porque en ese punto de la ejecución existe otra referencia que se utilizará para acceder al mismo almacenamiento .
Defecto Informe 028 dijo que el ejemplo original invocó a UB debido a la superposición entre la creación y el uso de los dos punteros, que habría dejado las cosas mucho más claras sin tener que agregar "Tipos efectivos" u otra complejidad similar.
Después de leer muchas de las respuestas, siento la necesidad de agregar algo:
El alias estricto (que describiré en un momento) es importante porque :
El acceso a la memoria puede ser costoso (en cuanto al rendimiento), por lo que los datos se manipulan en los registros de la CPU antes de volver a escribir en la memoria física.
Si los datos en dos registros de CPU diferentes se escribirán en el mismo espacio de memoria, no podemos predecir qué datos "sobrevivirán" cuando codifiquemos C.
En el ensamblaje, donde codificamos la carga y descarga de registros de CPU manualmente, sabremos qué datos permanecen intactos. Pero C (afortunadamente) abstrae este detalle.
Dado que dos punteros pueden apuntar a la misma ubicación en la memoria, esto podría resultar en un código complejo que maneje las posibles colisiones .
Este código adicional es lento y perjudica el rendimiento, ya que realiza operaciones adicionales de lectura / escritura en la memoria que son más lentas y (posiblemente) innecesarias.
La regla de alias estricta nos permite evitar el código de máquina redundante en los casos en que debería ser seguro asumir que dos punteros no apuntan al mismo bloque de memoria (consulte también la restrict
palabra clave).
El aliasing estricto establece que es seguro asumir que los punteros a diferentes tipos apuntan a diferentes ubicaciones en la memoria.
Si un compilador nota que dos punteros apuntan a tipos diferentes (por ejemplo, an int *
y a float *
), asumirá que la dirección de memoria es diferente y no protegerá contra las colisiones de direcciones de memoria, lo que resultará en un código de máquina más rápido.
Por ejemplo :
Asumamos la siguiente función:
void merge_two_ints(int *a, int *b) {
*b += *a;
*a += *b;
}
Para manejar el caso en el que a == b
(ambos punteros apuntan a la misma memoria), debemos ordenar y probar la forma en que cargamos los datos de la memoria a los registros de la CPU, por lo que el código podría terminar así:
Carga
a
yb
desde la memoria.añadir
a
ab
.guardar
b
y recargara
.(guardar desde el registro de la CPU a la memoria y cargar desde la memoria al registro de la CPU).
añadir
b
aa
.guardar
a
(desde el registro de la CPU) a la memoria.
El paso 3 es muy lento porque necesita acceder a la memoria física. Sin embargo, se requiere proteger contra las instancias donde a
y b
apuntar a la misma dirección de memoria.
El alias estricto nos permitiría evitar esto al decirle al compilador que estas direcciones de memoria son claramente diferentes (lo que, en este caso, permitirá una optimización aún mayor que no se puede realizar si los punteros comparten una dirección de memoria).
Esto se puede decir al compilador de dos maneras, usando diferentes tipos para señalar. es decir:
void merge_two_numbers(int *a, long *b) {...}
Utilizando la
restrict
palabra clave. es decir:void merge_two_ints(int * restrict a, int * restrict b) {...}
Ahora, al cumplir la regla de Alias estricto, se puede evitar el paso 3 y el código se ejecutará significativamente más rápido.
De hecho, al agregar la restrict
palabra clave, la función completa podría optimizarse para:
Carga
a
yb
desde la memoria.añadir
a
ab
.Guardar el resultado tanto para
a
como parab
.
Esta optimización no se podría haber hecho antes, debido a la posible colisión (donde a
y b
se triplicaría en lugar de duplicarse).
El alias estricto no permite diferentes tipos de punteros a los mismos datos.
Este artículo debería ayudarlo a comprender el problema en detalle.
Técnicamente en C ++, la regla de alias estricta probablemente nunca sea aplicable.
Note la definición de direccionamiento indirecto ( * operador ):
El operador unario * realiza una indirección: la expresión a la que se aplica debe ser un puntero a un tipo de objeto o un puntero a un tipo de función y el resultado es un lvalor que se refiere al objeto o función al que apunta la expresión .
También de la definición de glvalue
Un glvalue es una expresión cuya evaluación determina la identidad de un objeto, (... snip)
Por lo tanto, en cualquier rastreo de programa bien definido, un valor de glualue se refiere a un objeto. Así que la llamada regla de alias estricta no se aplica, nunca. Esto puede no ser lo que querían los diseñadores.