2009-04-02 22 views
5

J'ai une application qui parcourt 250 Mo de données, en appliquant une fonction de seuil neural-net simple et rapide aux blocs de données (qui ne sont que 2 mots de 32 bits chacun). Basé sur le résultat du calcul (très simple), le morceau est poussé de façon imprévisible dans l'un des 64 casiers. Il y a donc un grand flux dans et 64 flux plus courts (de longueur variable).Utilisation efficace de la bande passante mémoire pour le streaming

Ceci est répété plusieurs fois avec différentes fonctions de détection.

Le calcul est limité par la bande passante mémoire. Je peux le dire parce qu'il n'y a pas de changement de vitesse, même si j'utilise une fonction discriminante qui est beaucoup plus computationnelle. Quel est le meilleur moyen de structurer les écritures des nouveaux flux pour optimiser ma bande passante mémoire?

Je pense surtout que la compréhension de l'utilisation du cache et de la taille de la ligne de cache peut jouer un grand rôle dans ce domaine. Imaginez le pire des cas où j'ai mes 64 flux de sortie et par malchance, beaucoup mappent sur la même ligne de cache. Ensuite, lorsque j'écris les 64 bits de données suivants dans un flux, le processeur doit vider une ligne de cache périmée dans la mémoire principale et charger dans la ligne de cache appropriée. Chacun d'entre eux utilise 64 octets de bande passante ... donc mon application limitée en bande passante peut gaspiller 95% de la bande passante mémoire (dans ce cas hypothétique, cependant).

Il est difficile d'essayer même de mesurer l'effet, donc la conception des façons de le contourner est encore plus vague. Ou suis-je même à la poursuite d'un goulot d'étranglement que le matériel optimise d'une manière ou d'une autre? J'utilise des processeurs Core II x86 si cela fait une différence.

Editer: Voici un exemple de code. Il circule dans un tableau et copie ses éléments dans différents tableaux de sortie choisis de manière pseudo-aléatoire. L'exécution du même programme avec un nombre différent de bacs de destination donne différents runtimes, même si la même quantité de calcul et de mémoire lectures et écritures ont été faites:

2 flux de sortie: 13 secondes
8 flux de sortie: 13 secondes
32 flux de sortie: 19 secs
128 streams de sortie: 29 secondes
512 streams de sortie: 47 secondes

La différence entre l'utilisation de 512 par rapport à 2 flux de sortie est 4X, (probablement ??) causée par les frais généraux d'expulsion de ligne de cache.

#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 .. peut-être s'il y avait du code? –

+0

Comme écrit, ce code ne compilera pas (point-virgule manquant, que j'ai ajouté), mais je me méfie de tout exemple qui a été édité pour publication. –

Répondre

4

Lorsque vous écrivez dans les 64 bacs de sortie, vous utilisez de nombreux emplacements de mémoire différents. Si les bacs sont remplis essentiellement au hasard, cela signifie que vous aurez parfois deux bacs partageant la même ligne de cache. Pas un gros problème; le cache Core 2 L1 est associatif à 8 voies. Cela signifie que vous ne rencontrerez un problème qu'avec la 9ème ligne de cache. Avec seulement 65 références de mémoire en direct à tout moment (1 lecture/64 écriture), l'associatif à 8 voies est OK.

Le cache L2 est apparemment associatif à 12 voies (total de 3/6MB, donc 12 n'est pas ce nombre bizarre). Donc même si vous avez des collisions en L1, il y a de fortes chances que vous n'ayez toujours pas de mémoire principale. Cependant, si vous n'aimez pas cela, réorganisez les bacs en mémoire. Au lieu de strocher séquentiellement chaque corbeille, intercalez-les. Pour le bac 0, stockez les blocs 0-15 aux décalages 0-63, mais stockez les blocs 16-31 au décalage 8192-8255. Pour le bac 1, stockez les blocs 0-15 aux décalages 64-127, etcetera. Cela ne prend que quelques décalages et masques, mais le résultat est qu'une paire de bacs partage 8 lignes de cache.

Un autre moyen possible d'accélérer votre code dans ce cas est SSE4, en particulier en mode x64. Vous auriez 16 registres x 128 bits, et vous pouvez optimiser la lecture (MOVNTDQA) pour limiter la pollution du cache. Je ne suis pas sûr si cela aidera beaucoup avec la vitesse de lecture, cependant - je m'attendrais à ce que le prefetcher Core2 l'attrape. La lecture d'entiers séquentiels est le type d'accès le plus simple possible, tout préfecteur doit optimiser cela.

+0

Donc, cela essaie de garder chaque file d'attente de sortie toujours mappée sur le même cache. Chaque corbeille de cache a toujours un nombre égal de flux, ce qui minimise l'expulsion. Les adresses aléatoires pourraient facilement faire correspondre plus de 9 flux à la même case et provoquer des expulsions. Complexe et dépendant du processeur, mais logique! Merci. – SPWorley

1

Vous pouvez explorer pour mapper les fichiers en mémoire. De cette façon, le noyau peut prendre soin de la gestion de la mémoire pour vous. Le noyau sait généralement comment gérer les caches de page. Cela est particulièrement vrai si votre application doit être exécutée sur plusieurs plates-formes, car les différentes Oses gèrent la gestion de la mémoire de différentes manières.

Il existe des structures telles que ACE (http://www.cs.wustl.edu/~schmidt/ACE.html) ou Boost (http://www.boost.org) qui vous permettent d'écrire du code qui effectue le mappage de la mémoire d'une manière indépendante de la plate-forme.

2

Voici quelques idées si vous avez vraiment désespéré ...

Vous pourriez envisager de matériel la mise à niveau. Pour les applications de streaming un peu similaire à la vôtre, j'ai trouvé que j'ai eu un gros coup de pouce de vitesse en changeant pour un processeur i7. De plus, les processeurs AMD sont censés être meilleurs que Core 2 pour le travail lié à la mémoire (même si je ne les ai pas utilisés récemment).

Une autre solution que vous pourriez envisager est de faire le traitement sur une carte graphique en utilisant un langage comme CUDA. Les cartes graphiques sont réglées pour avoir une bande passante mémoire très élevée et pour effectuer des calculs rapides en virgule flottante. Attendez-vous à passer de 5 à 20 fois le temps de développement du code CUDA par rapport à une implémentation C non optimisée.

3

Avez-vous la possibilité d'écrire vos flux de sortie sous la forme d'un flux unique avec des métadonnées intégrées pour identifier chaque 'segment'? Si vous deviez lire un 'morceau', exécutez votre fonction de seuil, puis au lieu de l'écrire dans un flux de sortie particulier, vous écririez simplement à quel flux il appartenait (1 octet) suivi des données d'origine, vous seriez sérieux Réduis ton galop.

Je ne le suggérerais pas, sauf que vous avez dit que vous devez traiter ces données plusieurs fois. À chaque exécution successive, vous lisez votre flux d'entrée pour obtenir le numéro de la corbeille (1 octet), puis vous faites tout ce que vous devez faire pour cette corbeille sur les 8 octets suivants. En ce qui concerne le comportement de cache de ce mécanisme, puisque vous ne faites que glisser à travers deux flux de données et, dans tous les cas sauf le premier, écrire autant de données que vous lisez, le matériel vous donnera toute l'aide vous pouvez espérer jusqu'à la préchargement, l'optimisation de la ligne de cache, etc.

Si vous deviez ajouter cet octet supplémentaire à chaque fois que vous traitez vos données, le pire des cas est le cas moyen. Si vous pouvez vous permettre le stockage, ça me semble être une victoire.

1

La vraie réponse dans de telles situations est de coder plusieurs approches et de les chronométrer. Ce que vous avez évidemment fait. Tout le monde comme moi peut faire est de suggérer d'autres approches à essayer. Par exemple: même en l'absence de mémoire cache (vos flux de sortie correspondent aux mêmes lignes de cache), si vous écrivez size ints, avec size = 1 < < 19 et sizeof (int) = 4, 32- bits - c'est-à-dire si vous écrivez 8 Mo de données, vous lisez en réalité 8 Mo, puis vous écrivez 8 Mo. Parce que si vos données sont dans la mémoire WB (WriteBack) ordinaire sur un processeur x86, pour écrire sur une ligne, vous devez d'abord lire l'ancienne copie de la ligne - même si vous allez jeter les données lues.

Vous pouvez éliminer ce trafic de lecture RFO inutile en (a) utilisant la mémoire WC (probablement difficile à configurer) ou (b) en utilisant des magasins de streaming SSE, alias NT (Non-Temporal) Stores. MOVNT * - MOVNTQ, MOVNTPS, etc. (Il y a aussi une charge de streaming MOVNTDQA, bien plus pénible à utiliser.)

Je aime bien cet article, je viens de trouver par googling http://blogs.fau.de/hager/2008/09/04/a-case-for-the-non-temporal-store/

maintenant: MOVNT * appliquer à WB mémoire mais fonctionne comme la mémoire WC, en utilisant un petit nombre de tampons d'écriture. Le nombre réel varie selon le modèle de processeur: il n'y en avait que 4 sur la première puce Intel pour les avoir, P6 (aka Pentium Pro). Ooof ... Le WCC 4K (Write Combining Cache) de Bulldozer fournit essentiellement 64 tampons de combinaison d'écriture, par http://semiaccurate.com/forums/showthread.php?t=6145&page=40, bien qu'il n'y ait que 4 tampons WC classiques. Mais http://www.intel.com/content/dam/doc/manual/64-ia-32-architectures-optimization-manual.pdf dit que certains processos ont 6 tampons WC, et certains 8. En tout cas ... il y en a quelques-uns, mais pas beaucoup. Habituellement pas 64.

Mais voici quelque chose que vous pourriez essayer: mettre en œuvre écrire vous-même. A) écrire dans un seul jeu de 64 tampons (flux), chacun de taille 64B (taille de ligne de cache), ou peut-être 128 ou 256B. Laissez ces tampons être dans la mémoire WB ordinaire. Vous pouvez y accéder avec des magasins ordinaires, bien que si vous pouvez utiliser MOVNT *, génial.

Lorsque l'un de ces tampons est plein, copiez-le en tant que salve à l'endroit de la mémoire où le flux est supposé aller. Utiliser les magasins de streaming MOVNT *.

Cela finira par faire * N octets stockés dans les tampons temporaires, frapper le cache L1 * 64 * 64 octets lus pour remplir les tampons temporaires * N octets lus dans les tampons temporaires, frapper le cache L1. * N octets écrits via des magasins de streaming - essentiellement aller directement à la mémoire.

i.e. N octets dans le cache lu + N octets dans le cache d'écriture + N octets de cache manquant

par rapport à N octets de cache manquant lu + N octets lus d'écriture de cache.

La réduction des N octets de la mémoire cache manquante peut être inférieure à la surcharge supplémentaire.