señales - funcion de muestreo sampling
¿Se especifica incorrectamente la regla de aliasing estricta? (9)
Antes de la norma C89, la gran mayoría de las implementaciones definían el comportamiento de la desreferenciación de escritura al puntero de un tipo en particular al establecer los bits del almacenamiento subyacente en la forma definida para ese tipo y definían el comportamiento de la desreferenciación de la lectura de un puntero de un tipo particular como la lectura de los bits del almacenamiento subyacente en la forma definida para ese tipo. Si bien esas habilidades no hubieran sido útiles en todas las implementaciones, hubo muchas implementaciones en las que el rendimiento de los bucles en caliente podría mejorar mucho, por ejemplo, utilizando cargas de 32 bits y almacenes para operar en grupos de cuatro bytes a la vez. Además, en muchas de estas implementaciones, apoyar tales comportamientos no costó nada.
Los autores del Estándar C89 afirman que uno de sus objetivos era evitar romper irreparablemente el código existente, y hay dos formas fundamentales en que las reglas podrían haber sido interpretadas de manera consistente:
Las reglas de C89 podrían haber sido pensadas para ser aplicables solo en los casos similares a los dados en el razonamiento (acceder a un objeto con tipo declarado tanto directamente a través de ese tipo como indirectamente a través del puntero), y donde los compiladores no tendrían razón para esperar el aliasing. . Realizar un seguimiento de cada variable si actualmente se almacena en caché en un registro es bastante simple, y poder mantener dichas variables en registros mientras se accede a punteros de otros tipos es una optimización simple y útil y no excluiría la compatibilidad con el código que usa el más común los patrones de aliasing (hacer que un compilador interprete un
float*
aint*
cast como un vaciado de cualquier valorfloat
caché de registro es simple y directo; dichos cambios son lo suficientemente raros como para que tal enfoque no afecte adversamente el rendimiento).Dado que el Estándar es generalmente agnóstico con respecto a lo que hace una implementación de buena calidad para una plataforma dada, las reglas podrían interpretarse como que permiten que las implementaciones rompan el código que utiliza el alias de formas que serían útiles y obvias, sin sugerir que sea bueno. Las implementaciones de calidad no deben tratar de evitar hacerlo.
Si la Norma define una forma práctica de permitir la creación de alias en el lugar que no es de ninguna manera significativamente inferior a otros enfoques, los enfoques distintos a los definidos podrían razonablemente considerarse como obsoletos. Si no existen medios definidos por el Estándar, entonces las implementaciones de calidad para plataformas donde el aliasing es necesario para lograr un buen desempeño deben esforzarse por apoyar eficientemente las plataformas de aliasing comunes en esas plataformas, ya sea que el Standard lo requiera o no.
Desafortunadamente, la falta de claridad en cuanto a lo que exige el Estándar ha dado lugar a una situación en la que algunas personas consideran construcciones obsoletas para las cuales no existen reemplazos. Tener la existencia de una definición de tipo de unión completa que implique dos tipos primitivos se debe interpretar como una indicación de que cualquier acceso a través de un puntero de un tipo debe considerarse como un acceso probable al otro permitiría ajustar los programas que dependen del alias en el lugar hacerlo sin un comportamiento indefinido, algo que no se puede lograr de ninguna otra manera práctica dado el presente estándar. Desafortunadamente, tal interpretación también limitaría muchas optimizaciones en el 99% de los casos en que serían inofensivos, haciendo así imposible para los compiladores que interpretan la Norma de esa manera ejecutar el código existente tan eficientemente como de otra manera sería posible.
En cuanto a si la regla está correctamente especificada, eso dependerá de lo que se supone que significa. Son posibles múltiples interpretaciones razonables, pero al combinarlas se obtienen algunos resultados poco razonables.
PS: la única interpretación de las reglas con respecto a las comparaciones de punteros y memcpy
que tendría sentido sin darle al término "objeto" un significado diferente de su significado en las reglas de alias, sugeriría que no se puede usar una región asignada para mantener más de una tipo de objeto Si bien algunos tipos de código podrían cumplir con dicha restricción, haría imposible que los programas utilicen su propia lógica de administración de memoria para reciclar el almacenamiento sin un número excesivo de llamadas malloc / free. Es posible que los autores de la Norma hayan intentado decir que las implementaciones no requieren que los programadores creen una región grande y la dividan en trozos de tipo mixto más pequeños, pero eso no significa que pretendan que las implementaciones de propósito general no funcionen. asi que.
Como se estableció anteriormente , una unión de la forma
union some_union {
type_a member_a;
type_b member_b;
...
};
con n miembros comprende n + 1 objetos en almacenamiento superpuesto: un objeto para la propia unión y un objeto para cada miembro de la unión. Está claro que puede leer y escribir libremente a cualquier miembro del sindicato en cualquier orden, incluso si lee a un miembro del sindicato que no fue el último en escribir. La regla de aliasing estricta nunca se infringe, ya que el lvalor a través del cual accede al almacenamiento tiene el tipo efectivo correcto.
Esto se ve respaldado por la nota 95, que explica cómo el tipo punning es un uso intencional de uniones.
Un ejemplo típico de las optimizaciones habilitadas por la regla estricta de aliasing es esta función:
int strict_aliasing_example(int *i, float *f)
{
*i = 1;
*f = 1.0;
return (*i);
}
que el compilador puede optimizar a algo como
int strict_aliasing_example(int *i, float *f)
{
*i = 1;
*f = 1.0;
return (1);
}
porque puede asumir con seguridad que la escritura en *f
no afecta el valor de *i
.
Sin embargo, ¿qué sucede cuando pasamos dos punteros a miembros de la misma unión? Considere este ejemplo, asumiendo que una plataforma típica donde float
es un número de punto flotante de precisión simple IEEE 754 e int
es un entero de complemento de dos bits de 32 bits:
int breaking_example(void)
{
union {
int i;
float f;
} fi;
return (strict_aliasing_example(&fi.i, &fi.f));
}
Como se estableció anteriormente, fi.i
y fi.f
refieren a una región de memoria superpuesta. Leerlos y escribirlos es incondicionalmente legal (escribir solo es legal una vez que el sindicato ha sido inicializado) en cualquier orden. En mi opinión, la optimización analizada anteriormente realizada por todos los compiladores principales produce un código incorrecto ya que los dos indicadores de diferente tipo apuntan legalmente a la misma ubicación.
De alguna manera no puedo creer que mi interpretación de la regla de alias estricta sea correcta. No parece plausible que la optimización misma para la que se diseñó el alias estricto no sea posible debido al caso de esquina mencionado anteriormente.
Por favor dime porque estoy equivocado
Una pregunta relacionada apareció durante la investigación.
Lea todas las respuestas existentes y sus comentarios antes de agregar las suyas para asegurarse de que su respuesta agregue un nuevo argumento.
Bajo la definición de miembros del sindicato en §6.5.2.3:
3 Una expresión postfix seguida de la
.
operador y un identificador designan a un miembro de una estructura u objeto de unión. ...4 Una expresión de posfijo seguida por el operador
->
y un identificador designa a un miembro de una estructura u objeto de unión. ...
Véase también §6.2.3 ¶1:
- los miembros de estructuras o sindicatos; cada estructura o unión tiene un espacio de nombres separado para sus miembros (desambiguado por el tipo de expresión utilizada para acceder al miembro a través del operador
->
);
Está claro que la nota de pie de página 95 se refiere al acceso de un miembro del sindicato con el sindicato en su alcance y uso .
o ->
operador.
Dado que las asignaciones y los accesos a los bytes que componen la unión no se realizan a través de los miembros de la unión, sino a través de indicadores, su programa no invoca las reglas de aliasing de los miembros de la unión (incluidas las aclaradas en la nota 95).
Además, se violan las reglas normales de alias, ya que el tipo efectivo del objeto después de *f = 1.0
es float
, pero se accede a su valor almacenado mediante un lvalue de tipo int
(consulte §6.5 ¶7).
Nota: Todas las referencias citan this borrador estándar de C11.
Comenzando con su ejemplo:
int strict_aliasing_example(int *i, float *f)
{
*i = 1;
*f = 1.0;
return (*i);
}
Primero reconozcamos que, en ausencia de uniones, esto violaría la regla estricta de aliasing si i
y f
apuntan al mismo objeto; asumiendo que el objeto no tiene un tipo efectivo, entonces *i = 1
establece el tipo efectivo en int
y *f = 1.0
luego lo establece en float
, y la return (*i)
final return (*i)
luego accede a un objeto con un tipo efectivo de float
través de un valor de l de tipo int
, lo que claramente no está permitido.
La pregunta es si esto equivaldría a una violación de alias estricta si i
y f
apuntan a miembros de la misma unión. En el acceso de miembros de la unión a través del "." operador de acceso miembro, la especificación dice (6.5.2.3):
Una expresión postfix seguida por el. operador y un identificador designan a un miembro de una estructura u objeto de unión. El valor es el del miembro nombrado (95) y es un lvalue si la primera expresión es un lvalue.
La nota de pie de página 95 mencionada anteriormente dice:
Si el miembro utilizado para leer el contenido de un objeto de unión no es el mismo que el último utilizado para almacenar un valor en el objeto, la parte apropiada de la representación del objeto del valor se reinterpreta como una representación del objeto en el nuevo tipo como descrito en 6.2.6 (un proceso a veces llamado "tipo punning"). Esto podría ser una representación trampa.
Esto está claramente destinado a permitir el punteo de tipo a través de una unión, pero se debe tener en cuenta que (1) las notas a pie de página no son normativas, es decir, no se supone que deben proscribir el comportamiento, sino que deben aclarar la intención de alguna parte del el texto de acuerdo con el resto de la especificación, y (2) los proveedores del compilador consideran que esta asignación para el tipo punning a través de una unión se aplica solo al acceso a través del operador de acceso de miembros de la unión, ya que de otro modo el aliasing estricto carece de sentido, ya que casi Cualquier acceso potencialmente alias también podría ser miembros de la misma unión.
Su ejemplo se almacena a través de un puntero a un miembro de la unión no existente o, al menos, no activo y, por lo tanto, comete una infracción estricta de alias (ya que accede al miembro que está activo usando un lvalor de tipo inadecuado) o usa un lvalor que sí lo hace. no denote un objeto (ya que el objeto correspondiente al miembro no activo no existe): se puede argumentar de cualquier manera y el estándar no es particularmente claro, pero cualquiera de las dos interpretaciones significa que su ejemplo tiene un comportamiento indefinido.
(Podría agregar que no puedo ver cómo la nota al pie que permite la tipificación de tipos a través de una unión describe un comportamiento que, de lo contrario, es inherente a la especificación, es decir, parece romper la regla ISO de no proscribir el comportamiento; nada más en la especificación parece Además, es un poco difícil leer el texto normativo, ya que requiere que esta forma de punning de tipo requiera que el acceso se realice de forma inmediata a través del tipo de unión).
Sin embargo, a menudo hay confusión causada por otra parte de la especificación, también en 6.5.2.3:
Se otorga una garantía especial para simplificar el uso de uniones: si una unión contiene varias estructuras que comparten una secuencia inicial común (ver más abajo), y si el objeto de la unión actualmente contiene una de estas estructuras, está permitido inspeccionar la estructura común. Parte inicial de cualquiera de ellos en cualquier lugar que sea visible una declaración del tipo completado de la unión.
Aunque esto no se aplica a su ejemplo, ya que no hay una secuencia inicial común, he visto que las personas leen esto como una regla general para controlar el tipo de castigo (al menos cuando se trata de una secuencia inicial común); creen que implica que debería ser posible usar este tipo de punteo con dos punteros a diferentes miembros del sindicato siempre que la declaración completa del sindicato sea visible (dado que las palabras a ese efecto aparecen en el párrafo citado anteriormente). Sin embargo, me gustaría señalar que el párrafo anterior todavía se aplica solo al acceso de los miembros del sindicato a través del "." operador. El problema de conciliar este entendimiento es, en ese caso, que la declaración completa del sindicato debe ser visible de todos modos, ya que de lo contrario no podría referirse a los miembros del sindicato. Creo que es esta falla en la redacción, combinada con una redacción igualmente mala en el Ejemplo 3 ( el siguiente no es un fragmento válido (porque el tipo de unión no es visible ...) , cuando la visibilidad de la unión no es realmente el factor decisivo) , eso hace que algunas personas consideren que la excepción de secuencia inicial común está pensada para aplicarse globalmente, no solo para el acceso de los miembros a través del "." operador, como excepción a la regla estricta de aliasing; y, habiendo llegado a esta conclusión, un lector podría interpretar la nota al pie de página con respecto al tipo punning para aplicarse globalmente, y algunos lo hacen: vea la discusión sobre este error de GCC, por ejemplo (tenga en cuenta que el error ha estado en estado SUSPENDIDO durante mucho tiempo) ).
(Por cierto, soy consciente de varios compiladores que no implementan la regla de "secuencia inicial común común global". No soy específicamente consciente de ningún compilador que implementa la regla de "secuencia inicial común global" aunque no admito también la tipificación de tipo arbitrario, sino que no significa que tales compiladores no existan. La respuesta del comité al Informe de Defectos 257 sugiere que pretenden que la regla sea global, sin embargo, personalmente creo que la idea de que la mera visibilidad de un tipo debería cambiar la semántica del código que no existe No me refiero a que ese tipo sea profundamente defectuoso, y sé que otros están de acuerdo).
En este punto, podría cuestionar cómo la lectura de un miembro de la unión no activa a través del operador de acceso miembro no viola el alias estricto, si lo hace a través de un puntero lo hace. Esto es de nuevo un área donde la especificación es algo confusa; la clave está quizás en decidir qué valor es responsable del acceso. Por ejemplo, si un objeto de unión u
tiene un miembro a
y lo leo a través de la expresión ua
, entonces podríamos interpretarlo como un acceso del objeto miembro ( a
) o simplemente como un acceso del objeto de unión ( u
) que el valor del miembro se extrae de. En este último caso, no hay violación de alias, ya que se le permite específicamente acceder a un objeto (es decir, el objeto miembro activo) a través de un lvalue de tipo agregado que contiene un miembro adecuado (6.5¶7). De hecho, la definición del operador de acceso de miembro en 6.5.2.3 admite esta interpretación, aunque de manera un tanto débil: el valor es el del miembro nombrado ; aunque es potencialmente un valor lime, no es necesario acceder al objeto al que se hace referencia. lvalor para obtener el valor del miembro, por lo que se evita la violación estricta de aliasing. Pero esto se está estirando de nuevo un poco.
(A mí me parece que no está bien especificado, generalmente, justo cuando un objeto tiene "acceso a su valor almacenado ... por una expresión de valor l" según 6.5¶7; por supuesto, podemos hacer una determinación razonable para nosotros mismos, pero luego debemos tenga cuidado para permitir el punteo de tipo mediante uniones según lo descrito anteriormente, o esté dispuesto a ignorar la nota de pie de página 95. A pesar de la vergüenza a menudo innecesaria, la especificación a veces carece de los detalles necesarios).
Los argumentos sobre la semántica de la unión se refieren invariablemente al DR 236 en algún momento. De hecho, su código de ejemplo es superficialmente muy similar al código en ese Informe de defectos. Me gustaría señalar que:
- "El Comité cree que el Ejemplo 2 viola las reglas de aliasing en el párrafo 6.5 del artículo 6.5", esto no contradice mi razonamiento anterior;
- "Para no violar las reglas, la función f en el ejemplo debe escribirse como" - esto apoya mi razonamiento anterior; debe usar el objeto union (y el operador ".") para cambiar el tipo de miembro activo; de lo contrario, tendrá acceso a un miembro inexistente (ya que la union puede contener solo un miembro a la vez);
- El ejemplo en DR 236 no se trata de tipificación de tipos. Se trata de si está bien asignar a un miembro de la unión no activo a través de un puntero a ese miembro. El código en cuestión es sutilmente diferente al de la pregunta aquí, ya que no intenta acceder al miembro de la unión "original" después de escribir al segundo miembro. Por lo tanto, a pesar de la similitud estructural en el código de ejemplo, el Informe de Defectos no está relacionado con su pregunta.
- La Respuesta del Comité en DR 236 afirma que "Ambos programas invocan un comportamiento indefinido". Sin embargo, esto no es compatible con la discusión, que muestra solo que el Ejemplo 2 invoca un comportamiento indefinido. Creo que la respuesta es errónea.
El estándar C11 (§6.5.2.3.9 EJEMPLO 3) tiene el siguiente ejemplo:
Lo siguiente no es un fragmento válido (porque el tipo de unión no es visible dentro de la función f):
struct t1 { int m; }; struct t2 { int m; }; int f(struct t1 *p1, struct t2 *p2) { if (p1->m < 0) p2->m = -p2->m; return p1->m; } int g() { union { struct t1 s1; struct t2 s2; } u; /* ... */ return f(&u.s1, &u.s2); }
Pero no puedo encontrar más aclaraciones sobre esto.
Esencialmente, la regla de alias estricta describe las circunstancias en las que un compilador puede asumir (o, por el contrario, no puede asumir) que dos punteros de diferentes tipos no apuntan a la misma ubicación en la memoria.
Sobre esa base, se permite la optimización que describe en strict_aliasing_example()
porque el compilador puede asumir que f
e i
apuntan a direcciones diferentes.
El breaking_example()
hace que los dos punteros pasados a strict_aliasing_example()
apunten a la misma dirección. Esto rompe el supuesto de que strict_aliasing_example()
permitido hacer a strict_aliasing_example()
, por lo tanto, resulta en que la función exhibe un comportamiento indefinido.
Así que el comportamiento del compilador que usted describe es válido. Es el hecho de que breaking_example()
hace que los punteros pasados a strict_aliasing_example()
apunten a la misma dirección que causa un comportamiento indefinido; en otras palabras, breaking_example()
rompe la suposición de que el compilador puede hacer dentro de strict_aliasing_example()
.
La regla de alias estricta prohíbe el acceso al mismo objeto por dos punteros que no tienen tipos compatibles, a menos que uno sea un puntero a un tipo de carácter:
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,
- 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.
En tu ejemplo, *f = 1.0;
Está modificando fi.i
, pero los tipos no son compatibles.
Creo que el error está en pensar que una unión contiene n objetos, donde n es el número de miembros. Una unión contiene solo un objeto activo en cualquier punto durante la ejecución del programa por §6.7.2.1 ¶16
El valor de como máximo uno de los miembros puede almacenarse en un objeto de unión en cualquier momento.
El soporte para esta interpretación de que una unión no contiene simultáneamente todos sus objetos miembros se puede encontrar en §6.5.2.3:
y si el objeto de unión actualmente contiene una de estas estructuras.
Finalmente, un problema casi idéntico se planteó en el informe de defectos 236 en 2006.
Ejemplo 2
// optimization opportunities if "qi" does not alias "qd" void f(int *qi, double *qd) { int i = *qi + 2; *qd = 3.1; // hoist this assignment to top of function??? *qd *= i; return; } main() { union tag { int mi; double md; } u; u.mi = 7; f(&u.mi, &u.md); }
El Comité cree que el Ejemplo 2 viola las reglas de alias en el párrafo 6.5 del artículo 6.5:
"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)".
Para no violar las reglas, la función f en el ejemplo debe escribirse como:
union tag { int mi; double md; } u; void f(int *qi, double *qd) { int i = *qi + 2; u.md = 3.1; // union type must be used when changing effective type *qd *= i; return; }
Retrocedamos un segundo del estándar y pensemos en lo que realmente es posible para un compilador.
Supongamos que strict_aliasing_example()
está definido en strict_aliasing_example.c
, y breaking_example()
está definido en breaking_example.c
. Supongamos que ambos archivos se compilan por separado y luego se vinculan entre sí, de esta manera:
gcc -c -o strict_aliasing_example.o strict_aliasing_example.c
gcc -c -o breaking_example.o breaking_example.c
gcc -o breaking_example strict_aliasing_example.o breaking_example.o
Por supuesto, tendremos que agregar un prototipo de función a breaking_example.c
, que se ve así:
int strict_aliasing_example(int *i, float *f);
Ahora considere que las dos primeras invocaciones de gcc
son completamente independientes y no pueden compartir información, excepto la función prototipo. Es imposible que el compilador sepa que i
y j
apuntarán a miembros de la misma unión cuando genere código para strict_aliasing_example()
. No hay nada en el sistema de vinculación o tipo que especifique que estos punteros son de alguna manera especiales porque provienen de una unión.
Esto apoya la conclusión que otras respuestas han mencionado: desde el punto de vista de la norma, acceder a una unión a través de .
o ->
obedece a diferentes reglas de alias en comparación con la eliminación de referencias a un puntero arbitrario.
Aquí está la nota 95 y su contexto:
Una expresión postfix seguida por el. operador y un identificador designan a un miembro de una estructura u objeto de unión. El valor es el del miembro nombrado, (95) y es un lvalue si la primera expresión es un lvalue. Si la primera expresión tiene un tipo calificado, el resultado tiene la versión calificada del tipo del miembro designado.
(95) Si el miembro utilizado para leer el contenido de un objeto de unión no es el mismo que el último utilizado para almacenar un valor en el objeto, la parte apropiada de la representación de objeto del valor se reinterpreta como una representación de objeto en el tipo nuevo como se describe en 6.2.6 (un proceso a veces llamado "tipeado"). Esto podría ser una representación trampa.
La Nota 95 se aplica claramente a un acceso a través de un miembro de la unión. Tu código no hace eso. Se accede a dos objetos superpuestos a través de punteros a 2 tipos separados, ninguno de los cuales es un tipo de carácter, y ninguno de los cuales es una expresión de posfijo pertinente para el punning de tipo.
Esta no es una respuesta definitiva ...
El Estándar no permite que se acceda al valor almacenado de una estructura o unión mediante un valor de l del tipo de miembro. Dado que su ejemplo accede al valor almacenado de una unión utilizando lvalues cuyo tipo no es el de la unión, ni ningún tipo que contenga esa unión, el comportamiento sería indefinido solo sobre esa base.
Lo único que se complica es que, bajo una estricta lectura de la Norma, incluso algo tan sencillo como
int main(void)
{
struct { int x; } foo;
foo.x = 1;
return 0;
}
también viola N1570 6.5p7 porque foo.x
es un lvalor de tipo int
, se usa para acceder al valor almacenado de un objeto de tipo struct foo
y el tipo int
no cumple ninguna de las condiciones en esa sección.
La única forma en que el Estándar puede ser incluso útil remotamente es si se reconoce que debe haber excepciones a N1570 6.5p7 en casos que involucran valores de l derivados de otros valores de l. Si la Norma describiera los casos en que los compiladores pueden o deben reconocer dicha derivación, y especifique que N1570 6.5p7 solo se aplica en los casos en que se accede al almacenamiento utilizando más de un tipo dentro de una ejecución particular de una función o bucle, eso habría eliminado mucha complejidad incluyendo cualquier necesidad de la noción de "Tipo Efectivo".
Desafortunadamente, algunos compiladores han tomado la decisión de ignorar la derivación de valores de l y punteros, incluso en algunos casos obvios como:
s1 *p1 = &unionArr[i].v1;
p1->x ++;
Puede ser razonable que un compilador no reconozca la asociación entre p1
y unionArr[i].v1
si otras acciones involucran unionArr[i]
la creación y el uso de p1, pero ni gcc ni clang pueden reconocer dicha asociación de manera consistente, incluso en casos simples en los que el uso del puntero sigue inmediatamente a Acción que toma la dirección del sindicalista.
Nuevamente, dado que el Estándar no requiere que los compiladores reconozcan cualquier uso de valores l derivados a menos que sean de tipos de caracteres, el comportamiento de gcc y clang no los hace no conformes. Por otro lado, la única razón por la que se conforman es debido a un defecto en la Norma que es tan escandaloso que nadie lee la Norma como diciendo lo que realmente hace.