c++ - SSE, intrínsecos y alineación
alignment intrinsics (3)
He escrito una clase de vector 3D usando muchos intrínsecos del compilador SSE. Todo funcionó bien hasta que comencé a instalar clases con el vector 3D como miembro con nuevas. Experimenté caídas extrañas en el modo de lanzamiento, pero no en el modo de depuración y viceversa.
Así que leí algunos artículos y pensé que necesitaba alinear las clases que poseen una instancia de la clase de vector 3D a 16 bytes también. Así que acabo de agregar _MM_ALIGN16
( __declspec(align(16)
) al frente de las clases, así:
_MM_ALIGN16 struct Sphere
{
// ....
Vector3 point;
float radius
};
Eso pareció resolver el problema al principio. Pero después de cambiar un código, mi programa comenzó a fallar de maneras extrañas. Busqué en la web un poco más y encontré un artículo de blog . Probé lo que el autor, Ernst Hot, hizo para resolver el problema y también me funciona. Agregué operadores nuevos y eliminados a mis clases de esta manera:
_MM_ALIGN16 struct Sphere
{
// ....
void *operator new (unsigned int size)
{ return _mm_malloc(size, 16); }
void operator delete (void *p)
{ _mm_free(p); }
Vector3 point;
float radius
};
Ernst menciona que este enfoque también podría ser problemático, pero simplemente se vincula a un foro que ya no existe sin explicar por qué podría ser problemático.
Entonces mis preguntas son:
¿Cuál es el problema con la definición de los operadores?
¿Por qué no se agrega
_MM_ALIGN16
a la definición de clase?¿Cuál es la mejor manera de manejar los problemas de alineación que vienen con los intrínsecos de SSE?
El problema con los operadores es que, por sí solos, no son suficientes. No afectan las asignaciones de pila, para las cuales todavía necesita
__declspec(align(16))
.__declspec(align(16))
afecta cómo el compilador coloca los objetos en la memoria, si y solo si tiene la opción. Para objetos nuevos, el compilador no tiene más remedio que utilizar la memoria devuelta por eloperator new
.Idealmente, use un compilador que los maneje de forma nativa. No existe una razón teórica por la que deba tratarse de manera diferente que el
double
. De lo contrario, lea la documentación del compilador para soluciones provisionales. Cada compilador con discapacidad tendrá su propio conjunto de problemas y, por lo tanto, su propio conjunto de soluciones.
Básicamente, debe asegurarse de que sus vectores estén alineados correctamente porque los tipos de vectores SIMD normalmente tienen requisitos de alineación más grandes que cualquiera de los tipos incorporados.
Eso requiere hacer lo siguiente:
Asegúrese de que
Vector3
esté alineado correctamente cuando está en la pila o en un miembro de una estructura. Esto se hace aplicando__attribute__((aligned(32)))
a la claseVector3
(o cualquier atributo que sea compatible con su compilador). Tenga en cuenta que no necesita aplicar el atributo a las estructuras que contienenVector3
, que no es necesario y no es suficiente (es decir, no es necesario aplicarlo aSphere
).Asegúrese de que
Vector3
o su estructuraVector3
esté alineada correctamente al usar la asignación de montón. Esto se hace usandoposix_memalign()
(o una función similar para su plataforma) en lugar de usar plainmalloc()
ooperator new()
porque los dos últimos alinean la memoria para tipos incorporados (normalmente 8 o 16 bytes) que no es garantizado para ser suficiente para tipos SIMD.
En primer lugar, debe cuidar dos tipos de asignación de memoria:
Asignación estática. Para que las variables automáticas se alineen correctamente, su tipo necesita una especificación de alineación adecuada (por ejemplo,
__declspec(align(16))
,__attribute__((aligned(16)))
, o su_MM_ALIGN16
). Pero afortunadamente solo necesita esto si los requisitos de alineación dados por los miembros del tipo (si hay alguno) no son suficientes. Entonces, no necesita esto paraSphere
, dado que suVector3
ya está alineado correctamente. Y si suVector3
contiene un miembro__m128
(que es bastante probable, de lo contrario sugeriría que lo haga), entonces ni siquiera lo necesita paraVector3
. Por lo tanto, normalmente no tiene que meterse con los atributos de alineación específicos del compilador.Asignación dinámica Demasiado para la parte fácil. El problema es que C ++ usa, en el nivel más bajo, una función de asignación de memoria más bien agnóstica para asignar cualquier memoria dinámica. Esto solo garantiza una alineación adecuada para todos los tipos estándar, que podría ser de 16 bytes, pero no está garantizado.
Para compensar esto, tiene que sobrecargar el
operator new/delete
incorporadooperator new/delete
para implementar su propia asignación de memoria y usar una función de asignación alineada bajo el capó en lugar del viejomalloc
. Sobrecargar aloperator new/delete
es un tema en sí mismo, pero no es tan difícil como podría parecer al principio (aunque su ejemplo no es suficiente) y puede leer al respecto en esta excelente pregunta de preguntas frecuentes .Lamentablemente, tiene que hacer esto para cada tipo que tenga un miembro que necesite alineación no estándar, en su caso, tanto
Sphere
comoVector3
. Pero lo que puede hacer para hacerlo un poco más fácil es simplemente hacer una clase base vacía con las sobrecargas adecuadas para esos operadores y luego derivar todas las clases necesarias de esta clase base.Lo que la mayoría de las personas a veces tienden a olvidar es que el allocator estándar
std::alocator
utiliza eloperator new
globaloperator new
para todas las asignaciones de memoria, por lo que sus tipos no funcionarán con contenedores estándar (y unstd::vector<Vector3>
no es eso raro un caso de uso). Lo que debe hacer es crear su propio asignador conforme y usarlo. Pero para mayor comodidad y seguridad, en realidad es mejor solo especializarstd::allocator
para su tipo (tal vez solo derivarlo de su asignador personalizado) para que siempre se use y no tenga que preocuparse por usar el asignador adecuado cada vezstd::vector
unstd::vector
. Desafortunadamente, en este caso, debe volver a especializarlo para cada tipo alineado, pero una pequeña macro malvada ayuda con eso.Además, debe buscar otras cosas utilizando el
operator new/delete
globaloperator new/delete
lugar de su función personalizada, comostd::get_temporary_buffer
ystd::return_temporary_buffer
std::get_temporary_buffer
ystd::get_temporary_buffer
de ellas si fuera necesario.
Desafortunadamente, aún no existe un enfoque mucho mejor para esos problemas, creo, a menos que esté en una plataforma que se alinee de forma nativa con 16 y sepa al respecto . O bien, podría sobrecargar el operator new/delete
global operator new/delete
para alinear siempre cada bloque de memoria a 16 bytes y no preocuparse por la alineación de cada una de las clases que contengan un miembro SSE, pero desconozco las implicaciones de esto. enfoque. En el peor de los casos, debería ocasionar pérdida de memoria, pero por lo general, no se asignan objetos pequeños dinámicamente en C ++ (aunque std::list
y std::map
pueden pensar de forma diferente al respecto).
Así que para resumir:
__declspec(align(16))
cuidado de alinear correctamente la memoria estática usando cosas como__declspec(align(16))
, pero solo si ya no está siendo atendido por ningún miembro, lo cual es generalmente el caso.operator new/delete
sobrecargaoperator new/delete
para cada tipo con un miembro con requisitos de alineación no estándar.Haga un distribuidor de acuerdo con el estándar cunstom para usar en contenedores estándar de tipos alineados, o mejor aún, especialice
std::allocator
para cada tipo alineado.
Finalmente algunos consejos generales. A menudo, solo obtiene ganancias de SSE en bloques pesados en computación cuando realiza muchas operaciones vectoriales. Para simplificar todos estos problemas de alineación, especialmente los problemas de cuidado de la alineación de todos y cada uno de los tipos que contienen un Vector3
, podría ser un buen enfoque hacer un tipo especial de vector SSE y usarlo solo dentro de cómputos largos, usando un no normal -SSE vector para almacenamiento y variables miembro.