Question

J'ai une application qui ruisselle par 250 Mo de données, l'application d'une simple et rapide fonction de seuil réseau neuronal aux blocs de données (qui sont seulement 2 mots de 32 bits chacun). En fonction du résultat de la (très simple) Compute, le morceau est poussé dans un imprévisiblement de 64 bacs. Il est donc un grand cours d'eau et 64 cours d'eau plus courte (longueur variable) sur.

Cette opération est répétée plusieurs fois avec différentes fonctions de détection.

Le calcul est la bande passante mémoire limitée. Je peux dire cela parce qu'il n'y a pas de changement de vitesse, même si j'utilise une fonction discriminante qui est beaucoup plus de calculs.

Quelle est la meilleure façon de structurer les écritures des nouveaux flux pour optimiser ma bande passante mémoire? Je pense en particulier que l'utilisation du cache la compréhension et 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 la malchance, beaucoup carte à la même ligne de cache. Puis, quand j'écrire les 64 bits suivants de données dans un flux, l'unité centrale a pour rincer une ligne d'antémémoire vicié à la mémoire principale, et charger dans la bonne ligne de cache. Chacun de ces 64 utilise la bande passante de BYTES ... donc ma bande passante application limitée peut être perdre 95% de la bande passante de la mémoire (dans ce pire cas hypothétique, cependant).

Il est difficile d'essayer même de mesurer l'effet, si la conception des moyens autour d'elle est encore plus vague. Ou suis-je même poursuivais un goulot d'étranglement fantôme qui en quelque sorte le matériel optimise mieux que moi?

J'utilise de base II processeurs x86 si cela fait une différence.

Edit: Voici quelques exemples de code. Ce flux à travers un réseau et des copies de ses éléments à différents tableaux de sortie pseudo-aléatoire choisi. L'exécution du même programme avec un nombre différent de bacs de destination donne différents runtimes, même si les lectures et les écritures ont été fait la même quantité de calcul et de mémoire:

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

La différence entre l'utilisation de 512 par rapport aux deux flux de sortie est 4X, (probablement ??) provoquée par surcharge 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);
}
Était-ce utile?

La solution

Comme vous écrivez aux 64 bacs de sortie, vous utiliserez différents emplacements de mémoire. Si les bacs sont remplis essentiellement au hasard, cela signifie que vous aurez parfois deux bacs qui partagent Couls la même ligne de cache. Pas un gros problème; le cache L1 est Core 2 à 8 voies associative. Cela signifie que vous obtiendrez seulement un problème avec la 9e ligne de cache. Avec seulement 65 références de mémoire en direct à tout moment (1 lecture / écriture 64), 8 voies associative est OK.

Le cache L2 est apparemment 12 voies associative (total 3 / 6MB, donc 12 est un nombre qui bizarre). Donc, même si vous avez des collisions en L1, les chances sont assez bonnes, vous n'êtes toujours pas frapper la mémoire principale.

Cependant, si vous ne l'aimez pas, réarranger les bacs en mémoire. Au lieu de stroing chaque cellule séquentielle, les entrelacer. Pour bin 0, des morceaux de magasin 0-15 à 0-63, mais les compensations des morceaux de magasin 16-31 à l'offset 8192-8255. Pour bac 1, des morceaux de magasin 0-15 à compensation 64-127, etcetera. Cela ne prend que quelques décalages de bits et des masques, mais le résultat est qu'une paire de réceptacles 8 lignes de cache.

Une autre façon possible d'accélérer votre code dans ce cas est SSE4, en particulier en mode 64 bits. Vous obtiendriez 16 registres x 128 bits, et vous pouvez optimiser la lecture (MOVNTDQA) afin de limiter la pollution de cache. Je ne sais pas si cela va aider beaucoup à la vitesse de lecture, bien que - j'attends le prefetcher Core2 pour attraper cela. La lecture des nombres entiers séquentiels est le type le plus simple accès possible, tout prefetcher devrait optimiser cela.

Autres conseils

Avez-vous la possibilité d'écrire vos flux de sortie en un seul flux de métadonnées en ligne pour identifier chaque « morceau »? Si vous deviez lire un « gros morceau, » exécuter votre fonction de seuil sur, puis au lieu d'écrire à un flux de sortie particulier vous suffit d'écrire quel flux appartenait à (1 octet), suivi par les données d'origine, vous devriez sérieusement réduire votre raclée.

Je conseillerais à l'exception du fait que vous avez dit que vous avez à traiter ces données plusieurs fois. Sur chaque course successive, vous lisez votre flux d'entrée pour obtenir le numéro de casier (1 octet), puis faites tout ce que vous devez faire pour ce bac sur les 8 octets suivants.

En ce qui concerne le comportement en tant que cache de ce mécanisme, puisque vous ne glissons par deux flux de données et, dans l'ensemble, mais le premier cas, l'écriture autant de données que vous lisez, le matériel vous donnera toute l'aide pourrait espérer aussi loin que le préchargement, l'optimisation de la ligne de cache, etc.

Si vous deviez ajouter cet octet supplémentaire à chaque fois que vous avez traité vos données, votre pire comportement du cache de cas est le cas moyen. Si vous pouvez vous permettre le coup de stockage, il semble comme une victoire pour moi.

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 semblables à la vôtre, je l'ai trouvé que je suis un coup de pouce grande vitesse en changeant à un processeur Core i7. De plus, les processeurs AMD sont censés être mieux que Core 2 pour le travail lié mémoire (bien que je ne les ai pas utilisé moi-même récemment).

Une autre solution possible consiste à faire le traitement sur une carte graphique en utilisant un langage comme CUDA. Les cartes graphiques sont réglés pour avoir une très grande bande passante mémoire et de faire des mathématiques à virgule flottante rapide. Attendez-vous à passer 5 à 20 fois le temps de développement pour le code CUDA par rapport à une implémentation C non optimisée simple.

Vous pouvez explorer pour cartographier les fichiers dans la mémoire. De cette façon, le noyau peut prendre en charge la gestion de la mémoire pour vous. Le noyau sait généralement mieux comment gérer les caches de page. Cela est particulièrement vrai si votre application doit fonctionner sur plusieurs plates-formes, comme les différentes Oses gérer la gestion de la mémoire de différentes manières.

Il existe des cadres comme ACE ( http: //www.cs.wustl. edu / ~ schmidt / ACE.html ) ou Boost ( http://www.boost.org ) qui vous permettent d'écrire du code qui fait la cartographie de la mémoire dans une plate-forme de manière indépendante.

La vraie réponse à des situations comme celle-ci est à coder plusieurs approches et les temps. Que vous avez évidemment fait. Tous les gens comme moi peuvent faire est de proposer d'autres approches pour essayer.

Par exemple: même en l'absence de raclée de cache (votre flux de sortie de cartographie aux mêmes lignes de cache), si vous taille écrivez ints, avec la taille = 1 << 19 et sizeof (int) = 4, 32 bits - à savoir si vous écrivez 8MB de données, vous lisez réellement 8MB puis écrire 8MB. Parce que si vos données sont en mémoire ordinaire WB (writeback) sur un processeur x86, d'écrire sur une ligne que vous devez d'abord lire l'ancienne copie de la ligne -. Même si vous allez jeter les données lues loin

Vous pouvez éliminer ce trafic inutile RFO lu par (a) en utilisant la mémoire WC (probablement une douleur à mettre en place) ou (b) en utilisant les magasins streaming SSE, alias NT (non-temporelle) Stores. MOVNT * - (. Il y a aussi une charge de streaming MOVNTDQA, bien que plus pénible à utiliser) MOVNTQ, MOVNTPS, etc.

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/

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

Mais voici quelque chose que vous pouvez essayer: mettre en œuvre écrire vous-même combinaison.

a) écrire dans un ensemble unique de 64 tampons (de #streams), chacun de taille 64B (taille de ligne de mémoire cache), - ou peut-être 128 ou 256B. Que ces tampons soient en mémoire WB ordinaire. Vous pouvez y accéder avec les magasins ordinaires, mais si vous pouvez utiliser MOVNT *, grand.

Lorsque l'un de ces tampons est plein, copiez comme un éclat à la place dans la mémoire où le flux est vraiment censé aller. En utilisant MOVNT * magasins de diffusion en continu.

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 par les magasins streaming -. va essentiellement 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.

Réduire les N octets de lecture de défaut de cache peut moe que compenser pour les frais généraux supplémentaires.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top