Buffers de memoria compartida en C++ sin violar reglas de alias estrictas
memory strict-aliasing (3)
Siempre es válido interpretar un objeto como una secuencia de bytes (es decir, no es una violación de aliasing tratar cualquier puntero de objeto como el puntero al primer elemento de una matriz de caracteres), y puede construir un objeto en cualquier parte de memoria lo suficientemente grande y adecuadamente alineada.
Por lo tanto, puede asignar una gran variedad de caracteres (cualquier signo) y localizar un desplazamiento que esté alineado en alignof(maxalign_t)
; ahora puede interpretar ese puntero como un puntero de objeto una vez que haya construido el objeto apropiado allí (por ejemplo, utilizando la ubicación-nueva en C ++).
Por supuesto, debe asegurarse de no escribir en la memoria de un objeto existente; de hecho, la duración del objeto está íntimamente ligada a lo que sucede con la memoria que representa el objeto.
Ejemplo:
char buf[50000];
int main()
{
uintptr_t n = reinterpret_cast<uintptr_t>(buf);
uintptr_t e = reinterpret_cast<uintptr_t>(buf + sizeof buf);
while (n % alignof(maxalign_t) != 0) { ++n; }
assert(e > n + sizeof(T));
T * p = :: new (reinterpret_cast<void*>(n)) T(1, false, ''x'');
// ...
p->~T();
}
Tenga en cuenta que la memoria obtenida por malloc
o new char[N]
siempre está alineada para la alineación máxima (pero no más, y es posible que desee utilizar direcciones sobrealineadas).
Estoy luchando con la implementación de un buffer de memoria compartido sin romper las estrictas reglas de aliasing de C99.
Supongamos que tengo un código que procesa algunos datos y necesita tener alguna memoria ''scratch'' para operar. Podría escribirlo como algo así como:
void foo(... some arguments here ...) {
int* scratchMem = new int[1000]; // Allocate.
// Do stuff...
delete[] scratchMem; // Free.
}
Luego tengo otra función que hace otras cosas que también necesitan un buffer de scratch:
void bar(...arguments...) {
float* scratchMem = new float[1000]; // Allocate.
// Do other stuff...
delete[] scratchMem; // Free.
}
El problema es que foo () y bar () se pueden llamar muchas veces durante la operación y tener asignaciones de montón por todos lados puede ser bastante malo en términos de rendimiento y fragmentación de la memoria. Una solución obvia sería asignar un búfer de memoria compartida común del tamaño adecuado una vez y luego pasarlo a foo () y bar () como un argumento, estilo BYOB:
void foo(void* scratchMem);
void bar(void* scratchMem);
int main() {
const int iAmBigEnough = 5000;
int* scratchMem = new int[iAmBigEnough];
foo(scratchMem);
bar(scratchMem);
delete[] scratchMem;
return 0;
}
void foo(void* scratchMem) {
int* smem = (int*)scratchMem;
// Dereferencing smem will break strict-aliasing rules!
// ...
}
void bar(void* scratchMem) {
float* smem = (float*)scratchMem;
// Dereferencing smem will break strict-aliasing rules!
// ...
}
Supongo que tengo dos preguntas ahora:
- ¿Cómo puedo implementar un búfer de memoria temporal común compartido que no viole las reglas de aliasing?
- Aunque el código anterior infringe reglas de aliasing estrictas, no se realiza ningún "daño" con el alias. Por lo tanto, ¿podría algún compilador sano generar código (optimizado) que todavía me meta en problemas?
Gracias
En realidad, lo que has escrito no es una violación estricta de aliasing.
C ++ 11 especificación 3.10.10 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
Entonces, lo que causa el comportamiento indefinido es acceder al valor almacenado, no solo crear un puntero al mismo. Tu ejemplo no viola nada. Tendría que hacer el siguiente paso: float badValue = smem [0]. smem [0] obtiene el valor almacenado del buffer compartido, creando una violación de aliasing.
Por supuesto, no vas a agarrar smem [0] antes de configurarlo. Vas a escribir primero. Asignar a la misma memoria no tiene acceso al valor almacenado, por lo que no hay ailiasing. Sin embargo, ES ilegal escribir sobre la parte superior de un objeto mientras aún está activo. Para demostrar que estamos seguros, necesitamos duración de vida objetiva de 3.8.4:
Un programa puede finalizar la vida útil de cualquier objeto mediante la reutilización del almacenamiento que ocupa el objeto o llamando explícitamente al destructor para un objeto de un tipo de clase con un destructor no trivial. Para un objeto de un tipo de clase con un destructor no trivial, no es necesario que el programa llame al destructor explícitamente antes de que se reutilice o libere el almacenamiento que ocupa el objeto; ... [continúa con las consecuencias de no llamar a los destructores]
Tienes un tipo de POD, un destructor tan trivial, por lo que puedes simplemente declarar verbalmente que "los objetos int están todos al final de su vida útil, estoy usando el espacio para flotadores". A continuación, vuelve a utilizar el espacio para flotantes y no se produce una violación de aliasing.
Si una unión se usa para contener las variables int y float, entonces puede pasar el aliasing estricto. Se ofrece más información sobre esto en http://cellperformance.beyond3d.com/articles/2006/06/understanding-strict-aliasing.html
Ver también el siguiente artículo.
http://blog.regehr.org/archives/959
Él le da una forma de usar sindicatos para hacer esto.