c++ alignment sse intrinsics

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:

  1. ¿Cuál es el problema con la definición de los operadores?

  2. ¿Por qué no se agrega _MM_ALIGN16 a la definición de clase?

  3. ¿Cuál es la mejor manera de manejar los problemas de alineación que vienen con los intrínsecos de SSE?


  1. 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)) .

  2. __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 el operator new .

  3. 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:

  1. 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 clase Vector3 (o cualquier atributo que sea compatible con su compilador). Tenga en cuenta que no necesita aplicar el atributo a las estructuras que contienen Vector3 , que no es necesario y no es suficiente (es decir, no es necesario aplicarlo a Sphere ).

  2. Asegúrese de que Vector3 o su estructura Vector3 esté alineada correctamente al usar la asignación de montón. Esto se hace usando posix_memalign() (o una función similar para su plataforma) en lugar de usar plain malloc() o operator 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 para Sphere , dado que su Vector3 ya está alineado correctamente. Y si su Vector3 contiene un miembro __m128 (que es bastante probable, de lo contrario sugeriría que lo haga), entonces ni siquiera lo necesita para Vector3 . 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 incorporado operator 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 viejo malloc . Sobrecargar al operator 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 como Vector3 . 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 el operator new global operator new para todas las asignaciones de memoria, por lo que sus tipos no funcionarán con contenedores estándar (y un std::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 especializar std::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 vez std::vector un std::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 global operator new/delete lugar de su función personalizada, como std::get_temporary_buffer y std::return_temporary_buffer std::get_temporary_buffer y std::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 sobrecarga operator 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.