c++ c casting type-punning

c++ - estricto aliasing y alineamiento de memoria



casting type-punning (4)

Simplemente deshabilite la optimización basada en alias y llámela un día

Si sus problemas son de hecho causados ​​por optimizaciones relacionadas con el aliasing estricto, entonces -fno-strict-aliasing resolverá el problema. Además, en ese caso, no tiene que preocuparse por perder la optimización porque, por definición, esas optimizaciones no son seguras para su código y no puede usarlas.

Buen punto de Praetorian . Recuerdo la histeria de un desarrollador provocada por la introducción del análisis de alias en gcc. Un cierto autor del kernel de Linux quería (A) alias cosas, y (B) aún obtener esa optimización. (Eso es una simplificación excesiva, pero parece que el -fno-strict-aliasing resolvería el problema, no cuesta mucho, y todos deben tener otros pescados para freír).

Tengo un código de rendimiento crítico y hay una gran función que asigna como 40 matrices de diferentes tamaños en la pila al comienzo de la función. La mayoría de estas matrices deben tener cierta alineación (porque se accede a estas matrices en algún otro lugar de la cadena utilizando instrucciones de CPU que requieren alineación de memoria (para Intel y CPU de brazo).

Como algunas versiones de gcc simplemente no logran alinear las variables de pila correctamente (especialmente para el código de armado), o incluso a veces dice que la alineación máxima para la arquitectura de destino es inferior a la que realmente solicita mi código, simplemente no tengo más remedio que asignar estas matrices en la pila y alinearlos manualmente.

Entonces, para cada arreglo, necesito hacer algo así para alinearlo correctamente:

short history_[HIST_SIZE + 32]; short * history = (short*)((((uintptr_t)history_) + 31) & (~31));

De esta forma, la history ahora está alineada en un límite de 32 bytes. Hacer lo mismo es tedioso para las 40 matrices, además esta parte del código es realmente intensiva en CPU y simplemente no puedo hacer la misma técnica de alineación para cada una de las matrices (este desorden de alineación confunde al optimizador y la asignación de registros diferentes ralentiza la función a lo grande , para una mejor explicación vea la explicación al final de la pregunta).

Entonces ... obviamente, quiero hacer esa alineación manual solo una vez y asumir que estas matrices están ubicadas una después de la otra. También agregué relleno adicional a estas matrices para que siempre sean múltiples de 32 bytes. Entonces, simplemente creo una matriz jumbo char en la pila y la lanzo a una estructura que tiene todas estas matrices alineadas:

struct tmp { short history[HIST_SIZE]; short history2[2*HIST_SIZE]; ... int energy[320]; ... }; char buf[sizeof(tmp) + 32]; tmp * X = (tmp*)((((uintptr_t)buf) + 31) & (~31));

Algo como eso. Quizás no sea el más elegante, pero produjo un resultado realmente bueno y la inspección manual del montaje generado demuestra que el código generado es más o menos adecuado y aceptable. El sistema de compilación se actualizó para usar GCC más nuevo y de repente comenzamos a tener algunos artefactos en los datos generados (p. Ej., El resultado del conjunto de pruebas de validación ya no es exacto ni siquiera en compilación C pura con código asm deshabilitado). Tomó mucho tiempo depurar el problema y parecía estar relacionado con las reglas de aliasing y las versiones más nuevas de GCC.

Entonces, ¿cómo puedo hacerlo? Por favor, no pierdas el tiempo tratando de explicar que no es estándar, no portátil, indefinido, etc. (He leído muchos artículos al respecto). Además, no hay forma de que pueda cambiar el código (tal vez considere modificar también GCC para solucionar el problema, pero no refactorizar el código) ... básicamente, todo lo que quiero es aplicar algún hechizo de magia negra para que GCC más nuevo produce el mismo código funcional para este tipo de código sin deshabilitar las optimizaciones?

Editar:

  • Usé este código en varios sistemas operativos / compiladores, pero empecé a tener problemas cuando cambié a un NDK más nuevo que se basa en GCC 4.6. Obtengo el mismo mal resultado con GCC 4.7 (desde NDK r8d)
  • Menciono la alineación de 32 bytes. Si le duele los ojos, sustitúyalo con cualquier otro número que desee, por ejemplo, 666 si ayuda. No tiene sentido mencionar que la mayoría de las arquitecturas no necesitan esa alineación. Si alineo 8 KB de matrices locales en la pila, pierdo 15 bytes para la alineación de 16 bytes y pierdo 31 para la alineación de 32 bytes. Espero que esté claro a qué me refiero.
  • Yo digo que hay como 40 arreglos en la pila en el código de rendimiento crítico. Probablemente también necesite decir que es un código antiguo de terceros que ha funcionado bien y no quiero meterme con eso. No es necesario decir si es bueno o malo, no tiene sentido.
  • Este código / función tiene un comportamiento bien probado y definido. Tenemos números exactos de los requisitos de ese código, por ejemplo, asigna Xkb o RAM, usa Y kb de tablas estáticas, y consume hasta Z kb de espacio de pila y no puede cambiar, ya que el código no se cambiará.
  • Al decir que "el desorden de alineación confunde al optimizador" quiero decir que si trato de alinear cada matriz por separado, el optimizador de código asigna registros adicionales para el código de alineación y el rendimiento partes críticas del código no tienen suficientes registros y comienzan a destrozar para apilar resulta en una desaceleración del código. Este comportamiento se observó en CPU ARM (por cierto, no estoy preocupado por Intel).
  • Por artefactos quise decir que la salida se convierte en no bitexact, se agrega algo de ruido. Ya sea debido a este tipo de problema de alias o hay algún error en el compilador que finalmente produce un resultado incorrecto de la función.

    En resumen, el punto de la pregunta ... ¿cómo puedo asignar una cantidad aleatoria de espacio de pila (usando matrices o alloca , y luego alinear el puntero a ese espacio de pila y reinterpretar este trozo de memoria como una estructura que tiene un diseño bien definido? eso garantiza la alineación de ciertas variables, siempre y cuando la propia estructura esté alineada. Estoy tratando de lanzar la memoria usando todo tipo de enfoques, muevo la asignación de la gran pila a una función separada, aún así obtengo un mal resultado y acumulo corrupción. Realmente estoy empezando a pensar cada vez más que esta gran función golpea algún tipo de error en gcc. Es bastante extraño, que al hacer este elenco no puedo hacer esto sin importar lo que intento. Por cierto, yo deshabilité todas las optimizaciones que requieren cualquier alineación, ahora es un código puro de estilo C, aún obtengo malos resultados (resultados que no son bitexact y fallas ocasionales de daños en la pila). La solución simple que lo arregla todo, escribo en lugar de:

    char buf[sizeof(tmp) + 32]; tmp * X = (tmp*)((((uintptr_t)buf) + 31) & (~31));

    este código:

    tmp buf; tmp * X = &buf;

    ¡entonces todos los errores desaparecen! El único problema es que este código no hace la alineación adecuada para las matrices y se bloqueará con las optimizaciones habilitadas.

    Observación interesante:
    Mencioné que este enfoque funciona bien y produce el resultado esperado:

    tmp buf; tmp * X = &buf;

    En otro archivo, agregué una función noinline independiente que simplemente arroja un puntero vacío a esa estructura tmp *:

    struct tmp * to_struct_tmp(void * buffer32) { return (struct tmp *)buffer32; }

    Inicialmente, pensé que si lanzo memoria asignada usando to_struct_tmp, engañará a gcc para que produzca los resultados que esperaba obtener, sin embargo, todavía produce resultados no válidos. Si trato de modificar el código de trabajo de esta manera:

    tmp buf; tmp * X = to_struct_tmp(&buf);

    ¡entonces tengo el mismo mal resultado! WOW, ¿qué más puedo decir? Tal vez, en base a la regla de alias estrictos, gcc asume que tmp * X no está relacionado con tmp buf y quitó tmp buf como variable no utilizada justo después de regresar de to_struct_tmp? O hace algo extraño que produce un resultado inesperado. También intenté inspeccionar el ensamblaje generado, sin embargo, cambiando tmp * X = &buf; a tmp * X = to_struct_tmp(&buf); produce un código extremadamente diferente para la función, por lo que, de alguna manera, esa regla de alias afecta la generación de código a gran velocidad.

    Conclusión:
    Después de todo tipo de pruebas, tengo una idea de por qué posiblemente no pueda hacer que funcione sin importar lo que intento. En función del aliasing de tipo estricto, GCC piensa que la matriz estática no se utiliza y, por lo tanto, no asigna pila para ella. Entonces, las variables locales que también usan la pila se escriben en la misma ubicación donde se almacena mi estructura tmp ; en otras palabras, mi estructura jumbo comparte la misma memoria de pila que otras variables de la función. Solo esto podría explicar por qué siempre da como resultado el mismo mal resultado. -fno-strict-aliasing corrige el problema, como se esperaba en este caso.


  • En primer lugar, me gustaría decir que estoy definitivamente con usted cuando me pide que no hable sobre "violación estándar", "dependiente de la implementación", etc. Su pregunta es absolutamente legítima en mi humilde opinión.

    Su enfoque para empaquetar todas las matrices dentro de una struct también tiene sentido, eso es lo que haría.

    No está claro a partir de la formulación de la pregunta qué "artefactos" observas. ¿Hay algún código innecesario generado? O desalineación de datos? Si este es el caso, puede (con suerte) utilizar cosas como STATIC_ASSERT para garantizar en tiempo de compilación que las cosas estén alineadas correctamente. O al menos tener algún ASSERT en tiempo de ejecución en la compilación de depuración.

    Como propuso Eric Postpischil, puede considerar declarar esta estructura como global (si esto es aplicable para el caso, me refiero a que la repetición y el subprocesamiento múltiple no son una opción).

    Un punto más que me gustaría notar es las llamadas sondas de pila. Cuando asigna mucha memoria de la pila en una sola función (más de 1 página para ser exactos), en algunas plataformas (como Win32) el compilador agrega un código de inicialización adicional, conocido como sondas de pila. Esto también puede tener algún impacto en el rendimiento (aunque es probable que sea menor).

    Además, si no necesita todas las 40 matrices simultáneamente, puede organizar algunas de ellas en una union . Es decir, tendrá una gran struct , dentro de la cual algunas subestructuras se agruparán en union .


    Hay una serie de problemas aquí.

    Alineación: hay poco que requiera alineación de 32 bytes. La alineación de 16 bytes es beneficiosa para los tipos SIMD en los procesadores Intel y ARM actuales. Con AVX en los procesadores Intel actuales, el costo de rendimiento de usar direcciones alineadas en 16 bytes pero no alineadas en 32 bytes es generalmente leve. Puede haber una gran penalización para las tiendas de 32 bytes que cruzan una línea de caché, por lo que la alineación de 32 bytes puede ser útil allí. De lo contrario, la alineación de 16 bytes puede estar bien. (En OS X e iOS, malloc devuelve memoria alineada de 16 bytes).

    Asignación en código crítico: debe evitar asignar memoria en el código crítico de rendimiento. En general, la memoria debe asignarse al inicio del programa, o antes de que comience el trabajo crítico de rendimiento, y reutilizarse durante el código crítico de rendimiento. Si asigna memoria antes de que comience el código de rendimiento crítico, entonces el tiempo que se tarda en asignar y preparar la memoria es esencialmente irrelevante.

    Conjuntos grandes y numerosos en la pila: la pila no está pensada para grandes asignaciones de memoria, y existen límites para su uso. Incluso si no tiene problemas ahora, cambios aparentemente no relacionados en su código en el futuro podrían interactuar con el uso de mucha memoria en la pila y causar desbordamientos de pila.

    Numerosas matrices: 40 matrices es mucho. A menos que todos estén en uso para diferentes datos al mismo tiempo, y necesariamente, debe intentar reutilizar parte del mismo espacio para diferentes datos y propósitos. Usar diferentes arreglos innecesariamente puede causar más aglomeración de la memoria caché de lo necesario.

    Optimización: No está claro a qué se refiere al decir que el "desorden de alineación confunde al optimizador y la asignación de registros diferentes ralentiza la función a lo grande". Si tiene varias matrices automáticas dentro de una función, generalmente esperaría que el optimizador supiera que son diferentes, incluso si obtiene punteros de las matrices por aritmética de direcciones. Por ejemplo, un código dado como a[i] = 3; b[i] = c[i]; a[i] = 4; a[i] = 3; b[i] = c[i]; a[i] = 4; , Esperaría que el optimizador supiera que a , b , y c son matrices diferentes, y por lo tanto c[i] no puede ser igual que a[i] , por lo que está bien eliminar a[i] = 3; . Quizás un problema que tiene es que, con 40 arrays, tiene 40 punteros a las matrices, por lo que el compilador termina moviendo punteros dentro y fuera de los registros.

    En ese caso, reutilizar menos matrices para múltiples propósitos podría ayudar a reducir eso. Si tiene un algoritmo que realmente usa 40 arreglos a la vez, entonces podría considerar reestructurar el algoritmo para que use menos matrices a la vez. Si un algoritmo tiene que apuntar a 40 lugares diferentes en la memoria, entonces esencialmente necesita 40 punteros, independientemente de dónde o cómo se asignan, y 40 punteros es más de lo que hay registros disponibles.

    Si tiene otras preocupaciones sobre la optimización y el uso de registros, debería ser más específico al respecto.

    Aliasing y artefactos: informa que hay algunos problemas de aliasing y artefactos, pero no proporciona los detalles suficientes para comprenderlos. Si tiene una gran matriz de caracteres que reinterpreta como una estructura que contiene todas sus matrices, entonces no hay alias dentro de la estructura. Entonces no está claro qué problemas te encuentras.


    La alineación de 32 bytes suena como si estuviera presionando demasiado el botón. Ninguna instrucción de CPU debería requerir una alineación tan grande como esa. Básicamente, una alineación tan amplia como el tipo de datos más grande de su arquitectura debería ser suficiente.

    C11 tiene el concepto de maxalign_t , que es un tipo ficticio de alineación máxima para la arquitectura. Si su compilador aún no lo tiene, puede simularlo fácilmente por algo como

    union maxalign0 { long double a; long long b; ... perhaps a 128 integer type here ... }; typedef union maxalign1 maxalign1; union maxalign1 { unsigned char bytes[sizeof(union maxalign0)]; union maxalign0; }

    Ahora tiene un tipo de datos que tiene la alineación máxima de su plataforma y que se inicializa por defecto con todos los bytes configurados en 0 .

    maxalign1 history_[someSize]; short * history = history_.bytes;

    Esto evita los espantosos cálculos de dirección que realiza actualmente, solo tendría que hacer alguna adopción de someSize para tener en cuenta que siempre asigna múltiplos de sizeof(maxalign1) .

    También asegúrate de que esto no tenga problemas de aliasing. En primer lugar, las unions en C creadas para esto, y luego los punteros de caracteres (de cualquier versión) siempre pueden alias de cualquier otro puntero.