2012-09-19 20 views
23

He escrito una clase de vectores tridimensionales utilizando una gran cantidad de 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.SSE, intrínsecos y alineación

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 me acaba de agregar _MM_ALIGN16 (__declspec(align(16)) delante de las clases de este modo:

_MM_ALIGN16 struct Sphere 
{ 
    // .... 

    Vector3 point; 
    float radius 
}; 

que parecía 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. Añadí nuevo y borrar los operadores a mis clases de la siguiente 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 abordaje podría ser problemático también, pero él sólo enlaces a un foro que ya no existe, sin explicar por qué podría ser problemático.

Así que mis preguntas son:

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

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

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

+0

En el primer caso, ¿estás asignando tus estructuras en la pila o en el montón? No estoy seguro de que malloc regrese la memoria alineada de forma predeterminada, mientras que _mm_malloc sin duda lo haría, ¿a qué te refieres con "después de un tiempo mi programa comenzó a colapsar de nuevo"? ¿Te refieres después de dejarlo funcionando por un tiempo (y qué estaba haciendo)? – Thomas

+0

Los problemas comenzaron cuando comencé a asignar las estructuras en el montón. Con la frase "después de un tiempo" quiero decir que comenzó a fallar después de que cambie el código. Supongo que la alineación fue correcta por accidente y luego la destruí. Creo que malloc no devuelve la memoria 16 bytes alineados, que es el problema, supongo. Mi pregunta es realmente cuál es el problema con el enfoque del operador y cuál es la mejor manera de administrar el código utilizando los intrínsecos de SSE. –

+2

De hecho, no necesita especificar la alineación de 'Esfera' (usando esta cosa' _MM_ALIGN16'), ya que el compilador es lo suficientemente inteligente como para ver que 'Esfera' tiene un miembro alineado 16 y ajusta automáticamente' Esfera' s requisitos de alineación (dado que 'Vector3' está alineado correctamente). Esa es la razón por la que no tiene que alinear explícitamente 'Vector3' si ya tiene un miembro' __m128'. Solo la asignación dinámica es un problema y esto puede superarse al sobrecargar 'operator new/delete', como está escrito en el blog (y generalmente cosas adicionales, como 'std :: allocator'). –

Respuesta

18

En primer lugar hay que cuidar a los 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. Por lo tanto, no necesita esto para usted 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 para implementar su propia asignación de memoria y usar una función de asignación alineada bajo el capó en lugar del antiguo malloc. La sobrecarga de 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 sobre esto en this excellent FAQ question.

    Lamentablemente tiene que hacer esto para cada tipo que tenga un miembro que necesite alineación no estándar, en su caso ambos Sphere y 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 la gente a veces tienden a olvidar es que el asignador estándar std::alocator utiliza el operator new global para toda la asignación de memoria, por lo que sus tipos no funcionarán con contenedores estándar (y una std::vector<Vector3> no es raro que 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 especializarse en 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 que use 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 usted tiene que mirar hacia fuera para otras cosas mediante el mundial operator new/delete en lugar de su encargo uno, como std::get_temporary_buffer y std::return_temporary_buffer, y atender a las personas en caso de necesidad.

Lamentablemente todavía no existe una mejor aproximación a estos problemas, creo que, a menos que esté en una plataforma que se alinea de forma nativa a 16 y conocer este. O puede simplemente sobrecargar el operator new/delete global 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 no conozco las implicaciones de este 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).

Para resumir:

  • Atención a la correcta alineación de la memoria estática por medio de cosas como __declspec(align(16)), pero sólo si aún no lo está atendido por cualquier miembro, que suele ser el caso.

  • Sobrecarga operator new/delete para cada tipo que tenga 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.

+0

¿El 'std :: aligned_storage' de C++ 11 permitiría todo esto sin la necesidad de convenciones de llamadas especializadas? –

+1

@ graham.reeds En lugar de la palabra clave 'alignas'. 'std :: aligned_storage' no es realmente necesario, dado que' __m128' ya está alineado correctamente y preferiría un miembro '__m128' en lugar de un miembro' std :: aligned_storage'. Pero seguro, 'alignas' es la nueva plataforma independiente de decir' __declspec (align()) '(o lo que gcc le gusta), incluso si ninguno de ellos es necesario en absoluto. Pero todo esto solo ayuda a la alineación de la memoria estática de todos modos. –

1
  1. El problema con los operadores es que por sí mismos que 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 objetos en la memoria, si y solo si tiene la opción. Para objetos nuevos, el compilador no tiene más remedio que usar la memoria devuelta por operator new.

  3. Idealmente, utilice un compilador que maneje de forma nativa. No hay ninguna razón teórica por la que deba tratarse de manera diferente al 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.

+0

Gracias! Del comentario de Christian Rau tomo que el '__declspec (alinear (16))' es obsoleto. ¿Supongo que esa parte depende del compilador? No estoy seguro de si entiendo la 3. parte de su respuesta. ¿Qué quieres decir con "manejarlos de forma nativa"? Uso el compilador que viene con Visual Studio 2012 Express. –

+1

@FairDinkumThinkum: Lo que quiero decir con "un compilador que maneja de forma nativa" es un compilador que puede alinear los tipos de SSE igual que alinea los tipos de FP, es decir, sin la ayuda del programador. No necesita un '__declspec (alinear (8))' para eso. No tengo VS2012, así que no puedo decir con certeza si ya es inteligente. – MSalters

+1

Es muy probable que también deba implementar un asignador personalizado. –

2

Básicamente, es necesario asegurarse de que los vectores están correctamente alineados, porque SIMD tipos de vectores normalmente tienen requisitos de alineación más grandes que cualquiera de los tipos incorporados.

que requiere hacer las siguientes cosas:

  1. Asegúrese de que Vector3 está alineado de forma correcta cuando se está en la pila o un miembro de una estructura. Esto se hace aplicando __attribute__((aligned(32))) a la clase Vector3 (o cualquier atributo que admita su compilador). Tenga en cuenta que no es necesario aplicar el atributo a las estructuras que contienen Vector3, que no es necesario y no es suficiente (p.no es necesario aplicarlo al Sphere).

  2. Asegúrese de que Vector3 o su estructura envolvente esté correctamente alineada cuando usa la asignación de pila. Esto se hace utilizando posix_memalign() (o una función similar para su plataforma) en lugar de utilizar malloc() o operator new() simples porque los dos últimos alinean la memoria para tipos incorporados (normalmente 8 o 16 bytes) que no garantiza que sea suficiente para los tipos SIMD .

Cuestiones relacionadas