2009-04-02 13 views
5

Tengo una aplicación que transmite 250 MB de datos, aplicando una función de umbral neuronal simple y rápida a los fragmentos de datos (que son solo 2 palabras de 32 bits cada uno). Basado en el resultado del cálculo (muy simple), el fragmento es empujado imprevistamente en uno de los 64 bins. Por lo tanto, se trata de una gran transmisión y 64 secuencias más cortas (longitud variable).Uso eficiente del ancho de banda de memoria para la transmisión

Esto se repite muchas veces con diferentes funciones de detección.

El cálculo es el ancho de banda de memoria limitado. Puedo decir esto porque no hay cambio de velocidad incluso si uso una función discriminante que es mucho más intensiva en términos de computación.

¿Cuál es la mejor forma de estructurar las escrituras de las nuevas transmisiones para optimizar el ancho de banda de mi memoria? Estoy especialmente pensando que comprender el uso de la memoria caché y el tamaño de la línea de caché puede jugar un papel importante en esto. Imagine el peor caso en el que tengo mis 64 flujos de salida y, por mala suerte, muchos se asignan a la misma línea de caché. Luego, cuando escribo los siguientes 64 bits de datos en una secuencia, la CPU tiene que vaciar una línea de caché obsoleta a la memoria principal y cargar en la línea de caché adecuada. Cada uno de ellos usa 64 BYTES de ancho de banda ... por lo que mi aplicación de ancho de banda limitado puede estar desperdiciando el 95% del ancho de banda de la memoria (en este caso hipotético más desfavorable).

Incluso es difícil intentar medir el efecto, por lo que diseñar formas de evitarlo es aún más vago. ¿O incluso estoy persiguiendo un cuello de botella fantasma que de alguna manera el hardware optimiza mejor que yo podría?

Estoy usando procesadores Core II x86 si eso hace alguna diferencia.

Editar: Aquí hay un código de ejemplo. Fluye a través de una matriz y copia sus elementos a varias matrices de salida elegidas pseudoaleatoriamente. Al ejecutar el mismo programa con diferente número de bandejas de destino da diferentes tiempos de ejecución, a pesar de que la misma cantidad de cálculo y la memoria lee y se realizaron escribe:

2 flujos de salida: 13 segundos
8 flujos de salida: 13 segundos
32 corrientes de salida: 19 secs
128 flujos de salida: 29 segundos
512 flujos de salida: 47 segundos

La diferencia entre el uso de 512 frente a 2 flujos de salida es 4X, (probablemente ??) causada por línea de caché overhead desalojo.

#include <stdio.h> 
#include <stdlib.h> 
#include <ctime> 

int main() 
{ 
    const int size=1<<19; 
    int streambits=3; 
    int streamcount=1UL<<streambits; // # of output bins 
    int *instore=(int *)malloc(size*sizeof(int)); 
    int **outstore=(int **)malloc(streamcount*sizeof(int *)); 
    int **out=(int **)malloc(streamcount*sizeof(int)); 
    unsigned int seed=0; 

    for (int j=0; j<size; j++) instore[j]=j; 

    for (int i=0; i< streamcount; ++i) 
    outstore[i]=(int *)malloc(size*sizeof(int)); 

    int startTime=time(NULL); 
    for (int k=0; k<10000; k++) { 
    for (int i=0; i<streamcount; i++) out[i]=outstore[i]; 
    int *in=instore; 

    for (int j=0; j<size/2; j++) { 
     seed=seed*0x1234567+0x7162521; 
     int bin=seed>>(32-streambits); // pseudorandom destination bin 
     *(out[bin]++)=*(in++); 
     *(out[bin]++)=*(in++); 
    } 

    } 
    int endTime=time(NULL); 
    printf("Eval time=%ld\n", endTime-startTime); 
} 
+0

errr .. ¿tal vez si hubiera código? –

+0

Tal como está escrito, ese código no se compilará (falta el punto y coma, que he agregado), pero sospecho que algún ejemplo ha sido editado para su publicación. –

Respuesta

4

Al escribir en las 64 bandejas de salida, utilizará muchas ubicaciones de memoria diferentes. Si los contenedores se llenan esencialmente al azar, significa que a veces tendrás dos contenedores que pueden compartir la misma línea de caché. No es un gran problema; la memoria caché Core 2 L1 es de 8 vías asociativa. Eso significa que obtendría un problema solo con la novena línea de caché. Con solo 65 referencias de memoria en vivo en cualquier momento (1 de lectura/64 de escritura), el asociativo de 8 vías está bien.

La memoria caché L2 es aparentemente de 12 vías asociativas (3/6MB en total, por lo que 12 no es un número tan raro). Entonces, incluso si tuviera colisiones en L1, las posibilidades son bastante buenas de que aún no esté golpeando la memoria principal.

Sin embargo, si no te gusta esto, reorganiza los contenedores en la memoria. En lugar de golpear secuencialmente cada cubo, interpóralos. Para el contenedor 0, almacene los trozos 0-15 en los desplazamientos 0-63, pero almacene los trozos 16-31 en el desplazamiento 8192-8255. Para el contenedor 1, almacene los fragmentos 0-15 en los desplazamientos 64-127, etcétera. Esto solo requiere algunos cambios de bits y máscaras, pero el resultado es que un par de contenedores comparte 8 líneas de caché.

Otra forma posible de acelerar su código en este caso es SSE4, especialmente en modo x64. Obtendrá 16 registros x 128 bits, y puede optimizar la lectura (MOVNTDQA) para limitar la contaminación del caché. No estoy seguro de si eso ayudará mucho con la velocidad de lectura, espero que el recabador Core2 capte esto. Leer números enteros secuenciales es el tipo de acceso más simple posible, cualquier selector previo debería optimizar eso.

+0

Esto intenta mantener cada cola de salida siempre asignada a la misma bandeja de caché. Cada contenedor de caché siempre tiene la misma cantidad de transmisiones, lo que minimiza el desalojo. Las direcciones aleatorias pueden mapear fácilmente 9+ secuencias en la misma ubicación y provocar desalojos. Complejo y dependiente de la CPU, ¡pero lógico! Gracias. – SPWorley

1

Es posible que desee explorar el mapa de los archivos en la memoria. De esta forma, el kernel puede encargarse de la administración de la memoria por usted. El kernel generalmente sabe mejor cómo manejar las cachés de página. Esto es especialmente cierto si su aplicación necesita ejecutarse en más de una plataforma, ya que las diferentes Oses manejan la administración de memoria de diferentes maneras.

Existen marcos como ACE (http://www.cs.wustl.edu/~schmidt/ACE.html) o Boost (http://www.boost.org) que le permiten escribir código que hace la asignación de memoria de una manera independiente de la plataforma.

2

Aquí están algunas ideas si realmente desesperáis ...

Se podría considerar la actualización de hardware. Para aplicaciones de transmisión similares a la suya, descubrí que obtuve un gran impulso al cambiar a un procesador i7. Además, los procesadores AMD supuestamente son mejores que Core 2 para trabajos con memoria (aunque no los he usado recientemente).

Otra solución que puede considerar es hacer el procesamiento en una tarjeta gráfica usando un lenguaje como CUDA. Las tarjetas gráficas están sintonizadas para tener un ancho de banda de memoria muy alto y para realizar cálculos matemáticos rápidos en coma flotante. Espere pasar de 5x a 20x el tiempo de desarrollo para el código CUDA en relación con una implementación directa de C no optimizada.

3

¿Tiene la opción de escribir sus flujos de salida como una única secuencia con metadatos en línea para identificar cada 'porción'? Si tuviera que leer un "fragmento", ejecute su función umbral, entonces, en lugar de escribirlo en un flujo de salida en particular, simplemente escribiría a qué flujo pertenecía (1 byte) seguido de los datos originales, lo haría seriamente. reduce tu paliza

No recomendaría esto, excepto por el hecho de que ha dicho que debe procesar estos datos muchas veces. En cada ejecución sucesiva, lee su flujo de entrada para obtener el número de bin (1 byte) y luego hace lo que necesite para ese bin en los próximos 8 bytes.

En cuanto al comportamiento de caché de este mecanismo, ya que solo se desliza a través de dos flujos de datos y, en todos menos el primer caso, escribe tantos datos como está leyendo, el hardware le dará toda la ayuda posiblemente podría esperar hasta en cuanto a la captación previa, optimización de la línea de caché, etc.

Si tuviera que agregar ese byte extra cada vez que procesara sus datos, su peor caso de caché es el caso promedio. Si puede permitirse el golpe de almacenamiento, parece una victoria para mí.

1

La respuesta real para situaciones como esta es codificar varios enfoques y cronometrarlos. Que obviamente has hecho. Todas las personas como yo podemos hacer es sugerir otros enfoques para probar.

Por ejemplo: incluso en ausencia de memoria caché (las secuencias de salida se asignan a las mismas líneas de caché), si está escribiendo tamaños de tamaño, con tamaño = 1 < < 19 y sizeof (int) = 4, 32- bits, es decir, si está escribiendo 8MB de datos, en realidad está leyendo 8MB y luego escribiendo 8MB. Porque si sus datos están en la memoria ordinaria WB (WriteBack) en un procesador x86, para escribir en una línea primero debe leer la copia anterior de la línea, aunque vaya a tirar los datos leídos.

Puede eliminar este tráfico de lectura de RFO innecesarios mediante (a) el uso de la memoria de WC (probablemente una dificultad para configurar) o (b) utilizando almacenes de transmisión SSE, también conocidos como tiendas NT (no temporales). MOVNT * - MOVNTQ, MOVNTPS, etc. (Hay también un MOVNTDQA transmisión de carga, aunque más doloroso para su uso.)

me gusta más este trabajo que acabo de encontrar buscando en Google http://blogs.fau.de/hager/2008/09/04/a-case-for-the-non-temporal-store/

Ahora: MOVNT * aplica a WB memoria, pero funciona como la memoria WC, utilizando una pequeña cantidad de búferes de escritura de escritura. El número real varía según el modelo de procesador: solo había 4 en el primer chip Intel para tenerlos, P6 (también conocido como Pentium Pro). Ooof ... el 4K WCC de Bulldozer (Write Combining Cache) básicamente proporciona 64 búferes de combinación de escritura, por http://semiaccurate.com/forums/showthread.php?t=6145&page=40, aunque solo hay 4 búferes de WC clásicos. Pero http://www.intel.com/content/dam/doc/manual/64-ia-32-architectures-optimization-manual.pdf dice que algunos procesos tienen 6 búferes de WC, y algunos 8. De todos modos ... hay algunos, pero no tantos. Por lo general, no 64.

Pero aquí hay algo que podría intentar: implementar escribir combinándose.

a) escriba en un solo conjunto de 64 (# flujos) buffers, cada uno de tamaño 64B (tamaño de línea de caché), - o tal vez 128 o 256B. Deje que estos búferes estén en la memoria ordinaria de WB. Puede acceder a ellos con tiendas normales, aunque si puede usar MOVNT *, genial.

Cuando uno de estos búferes se llene, cópielo como una ráfaga en el lugar de la memoria donde se supone que debe pasar la secuencia. Uso de tiendas de transmisión MOVNT *.

Esto va a terminar haciendo * N bytes almacenados en las memorias intermedias temporales, golpeando la caché L1 * 64 * 64 bytes leídos para llenar los buffers temporales * N bytes leídos desde los buffers temporales, golpeando la caché L1. * N bytes escritos a través de tiendas de transmisión, básicamente yendo directamente a la memoria.

es decir, n bytes acierto de caché leer + n bytes de aciertos de caché de escritura + n bytes de caché de

frente N bytes de error de caché leer + N bytes leídos caché de escritura.

Reducir los N bytes de lectura de falta de memoria caché puede representar más que una sobrecarga adicional.

Cuestiones relacionadas