ejemplos - c++ online
¿Cuándo debo preocuparme por la alineación? (4)
He aprendido un poco acerca de la alineación recientemente, pero no estoy seguro en qué situaciones será un problema o no. Hay dos casos sobre los que me pregunto:
El primero es cuando se usan matrices:
struct Foo {
char data[3]; // size is 3, my arch is 64-bit (8 bytes)
};
Foo array[4]; // total memory is 3 * 4 = 12 bytes.
// will this be padded to 16?
void testArray() {
Foo foo1 = array[0];
Foo foo2 = array[1]; // is foo2 pointing to a non-aligned location?
// should one expect issues here?
}
El segundo caso es cuando se usa un conjunto de memoria:
struct Pool {
Pool(std::size_t size = 256) : data(size), used(0), freed(0) { }
template<class T>
T * allocate() {
T * result = reinterpret_cast<T*>(&data[used]);
used += sizeof(T);
return result;
}
template<class T>
void deallocate(T * ptr) {
freed += sizeof(T);
if (freed == used) {
used = freed = 0;
}
}
std::vector<char> data;
std::size_t used;
std::size_t freed;
};
void testPool() {
Pool pool;
Foo * foo1 = pool.allocate<Foo>(); // points to data[0]
Foo * foo2 = pool.allocate<Foo>(); // points to data[3],
// alignment issue here?
pool.deallocate(foo2);
pool.deallocate(foo1);
}
Mis preguntas son:
- ¿Hay problemas de alineación en los dos ejemplos de código?
- Si es así, entonces, ¿cómo se pueden arreglar?
- ¿Dónde puedo aprender más sobre esto?
Actualizar
Estoy usando un procesador Intel i7 de 64 bits con Darwin GCC. Pero también utilizo Linux, Windows (VC2008) para sistemas de 32 y 64 bits.
Actualización 2
Pool ahora usa un vector en lugar de una matriz.
En general, es decir, para la mayoría de las estructuras de datos, no se preocupe por la alineación por adelantado. El compilador generalmente hará lo correcto. Los días de penalización por tiempo de transpiración por datos no alineados son por lo menos 20 años atrás.
Los únicos problemas que quedan son el acceso ilegal a datos no alineados que se produce solo en una minoría de arquitecturas de CPU. Escribe el código para que tenga sentido. Pruébalo. Si se produce una excepción de datos no alineados, es hora de descubrir cómo evitarla. La mayoría de los casos se solucionan fácilmente agregando una opción de línea de comando. Algunos requieren alterar la estructura: reordenar elementos, o insertar explícitamente elementos de relleno no utilizados.
La alineación se maneja de forma transparente por el compilador; el tamaño de la matriz y los accesos siempre tienen en cuenta cualquier alineación y no tiene que preocuparse por ello.
Sin embargo, hay un error en el ejemplo del grupo de memoria: si llama a deallocate (), siempre se desasigna el último puntero asignado en lugar del puntero dado.
Nadie ha mencionado el grupo de memoria todavía. Esto tiene enormes problemas de alineación.
T * result = reinterpret_cast<T*>(&data[used]);
Eso no es bueno. Cuando se hace cargo de la administración de la memoria, debe hacerse cargo de todos los aspectos de la administración de la memoria, no solo de la asignación. Si bien es posible que haya asignado la cantidad correcta de memoria, no ha abordado la alineación en absoluto.
Supongamos que utiliza new
o malloc
para asignar un byte. Imprimir su dirección. Haz esto otra vez, e imprime esta nueva dirección:
char * addr1 = new char;
std::cout << "Address #1 = " << (void*) addr1 << "/n";
char * addr2 = new char;
std::cout << "Address #2 = " << (void*) addr2 << "/n";
En una máquina de 64 bits, como tu Mac, verás que ambas direcciones impresas terminan con un cero y generalmente están separadas por 16 bytes. No has asignado dos bytes aquí. ¡Has asignado 32! Esto se debe a que malloc
siempre devuelve un puntero alineado de tal manera que se puede usar para cualquier tipo de datos.
Coloque una int doble o larga en una dirección que no termine con 8 o 0 cuando se imprima en hexadecimal y es probable que obtenga un volcado de memoria. Los dobles y los largos largos deben alinearse con los límites de 8 bytes. Se aplican restricciones similares a los enteros de vainilla antiguos (int32_t); estos deben estar alineados en los límites de 4 bytes. Tu grupo de memoria no está haciendo esto.
struct Foo {
char data[3]; // size is 3, my arch is 64-bit (8 bytes)
};
[Editar: debería haber sido más explícito: se permite el relleno aquí, en la estructura después del miembro de data
(pero no antes)].
Foo array[4]; // total memory is 3 * 4 = 12 bytes.
No se permite el relleno aquí. Se requiere que las matrices sean contiguas. [editar: pero no se permite el relleno entre las estructuras en la matriz (una struct
en la matriz debe seguir inmediatamente después de otra), pero como se señaló anteriormente, cada estructura puede contener relleno.]
void testArray() {
Foo * foo1 = array[0];
Foo * foo2 = array[1]; // is foo2 pointing to a non-aligned location?
// should I expect issues here?
}
De nuevo, perfectamente bien - el compilador debe permitir esto 1 .
Para su grupo de memoria, el pronóstico no es tan bueno. Has asignado una serie de caracteres char
, que deben estar lo suficientemente alineados para poder acceder a ellos como char
, pero no se garantiza que el acceso a ellos como cualquier otro tipo funcione. No obstante, la implementación no puede imponer ningún límite de alineación al acceder a los datos como char
en cualquier caso.
Normalmente, para una situación como esta, creas una unión de todos los tipos que te interesan y asignas una matriz de eso. Esto garantiza que los datos estén alineados para ser utilizados como un objeto de cualquier tipo en la unión.
Alternativamente, puede asignar su bloque dinámicamente, tanto malloc
como operator ::new
garantiza que cualquier bloque de memoria esté alineado para ser utilizado como cualquier tipo.
Edición: cambiar la agrupación para usar vector<char>
mejora la situación, pero solo un poco. Significa que el primer objeto que asigne funcionará porque el bloque de memoria retenido por el vector se asignará (indirectamente) con operator ::new
(ya que no ha especificado lo contrario). Desafortunadamente, eso no ayuda mucho; la segunda asignación puede estar completamente desalineada.
Por ejemplo, supongamos que cada tipo requiere alineación "natural", es decir, alineación con un límite igual a su propio tamaño. Un char se puede asignar en cualquier dirección. Asumiremos que short es de 2 bytes, y requiere una dirección uniforme y int y long son 4 bytes y requieren una alineación de 4 bytes.
En este caso, considera lo que pasa si lo haces:
char *a = Foo.Allocate<char>();
long *b = Foo.Allocate<long>();
El bloque con el que comenzamos tenía que estar alineado para cualquier tipo, por lo que definitivamente era una dirección uniforme. Cuando asignamos el char
, usamos solo un byte, por lo que la siguiente dirección disponible es impar. Luego, asignamos suficiente espacio para un long
, pero está en una dirección impar, por lo que intentar desreferenciarlo da a UB.
1 Casi siempre, en última instancia, en última instancia, un compilador puede rechazar casi cualquier cosa bajo el pretexto de que se haya excedido un límite de implementación. Sin embargo, me sorprendería que un compilador real tenga un problema con esto.