2015-12-25 2 views
3

Je cours le code C++ de quelqu'un pour faire l'analyse comparative d'un jeu de données. Le problème que j'ai est que souvent je reçois un timing pour la première exécution, et ces nombres changent massivement (c'est-à-dire 28 secondes à 10 secondes) si je cours encore le même code. Je suppose que cela se produit en raison de la mise en cache automatique du processeur. Y at-il un moyen de vider le cache, ou d'éviter ces fluctuations d'une manière ou d'une autre?Vider le cache pour éviter les fluctions d'analyse comparative

+1

Parlez-vous du cache CPU ou du cache disque du système d'exploitation? Un délai supplémentaire de 18 secondes semble beaucoup trop long pour remplir la mémoire cache du processeur à partir de la RAM, des fautes de page + et des échecs TLB. Même les lectures aléatoires dispersées devraient pouvoir remplir toutes les lignes de cache du cache L3 d'un grand Xeon en * bien * moins d'une seconde. –

+2

@PeterCordes D'après mes calculs, si vous faisiez des lectures dispersées dépendantes en série et prenait 100 ns chacune, même en ignorant TLB et d'autres coûts, vous ne regarderiez que 2,8 Mo de lignes de cache en 18 secondes. le domaine de la faisabilité. – BeeOnRope

Répondre

3

Pas un qui fonctionne "pour tout, partout". La plupart des processeurs ont des instructions spéciales pour vider le cache, mais ce sont souvent des instructions privilégiées, donc cela doit être fait depuis le noyau du système d'exploitation, pas avec le code en mode utilisateur. Et bien sûr, il s'agit d'instructions complètement différentes pour chaque architecture de processeur.

Tous les processeurs x86 actuels ont une instruction clflush, qui vide une ligne de cache, mais pour ce faire, vous devez avoir l'adresse de la donnée (ou du code) que vous voulez vider. Ce qui est bien pour les structures de données petites et simples, pas si bon si vous avez un arbre binaire qui est partout. Et bien sûr, pas du tout portable.

Dans la plupart des environnements, lire et écrire un grand bloc de données alternatives, par ex. quelque chose comme:

// Global variables. 
const size_t bigger_than_cachesize = 10 * 1024 * 1024; 
long *p = new long[bigger_than_cachesize]; 
... 
// When you want to "flush" cache. 
for(int i = 0; i < bigger_than_cachesize; i++) 
{ 
    p[i] = rand(); 
} 

L'utilisation rand sera beaucoup plus lent que le remplissage avec quelque chose de constant/connu. Mais le compilateur ne peut pas optimiser l'appel, ce qui signifie (presque) que le code restera. Ce qui précède ne videra pas les caches d'instructions, ce qui est beaucoup plus difficile à faire. En gros, vous devez exécuter une partie (assez grande) d'un autre morceau de code pour le faire de manière fiable. Cependant, les caches d'instructions tendent à avoir moins d'effet sur la performance globale du benchmark (le cache d'instructions est EXTRÊMEMENT important pour la perforamnce du processeur moderne, ce n'est pas ce que je dis, mais dans le sens où le code est généralement assez petit dans le cache, et l'indice de référence passe plusieurs fois sur le même code, il est donc plus lent de la première itération)

Autres idées

une autre façon de simuler le comportement « non-cache » est allouer une nouvelle zone pour chaque étape de l'analyse comparative - en d'autres termes, ne pas libérer la mémoire jusqu'à la fin du test ou utiliser un tableau contenant les données, et produire des résultats, de sorte que chaque exécution dispose de son propre ensemble de données. De plus, il est courant de mesurer réellement les performances des "hot-runs" d'un benchmark, pas la première "cold run" où les caches sont vides. Cela ne dépend évidemment de ce que vous en train d'essayer d'atteindre ...

1

Voici mon approche de base:

  1. Allouer une zone de mémoire 2x la taille de la LLC, si vous pouvez déterminer la taille de LLC dynamique (ou vous le savez statiquement), ou si vous ne le faites pas, un multiple raisonnable de la plus grande taille LLC sur la plate-forme d'intérêt .
  2. memset la zone de mémoire à une valeur non nulle: 1 fera très bien. "Coule" le pointeur quelque part pour que le compilateur ne puisse pas optimiser les choses au-dessus ou au-dessous (écrire dans un volatile global fonctionne presque 100% du temps).Lire des index aléatoires dans la région jusqu'à ce que vous ayez touché chaque ligne de cache en moyenne 10 fois (accumulez les valeurs lues en une somme que vous évidez de la même manière que (3)).

Voici quelques explications sur les raisons pour lesquelles cela fonctionne généralement et pourquoi moins peut ne pas fonctionner - les détails sont axés sur x86, mais des problèmes similaires s'appliqueront à de nombreuses autres architectures.

  • Vous voulez absolument écrire à la mémoire allouée (étape 2) avant de commencer votre boucle principale de rinçage en lecture seule, car sinon vous pourriez être la lecture à plusieurs reprises de la même petite zero-mapped page retourné par le système d'exploitation à satisfaire votre allocation de mémoire.
  • Vous souhaitez utiliser une région considérablement plus grande que la taille LLC, car les niveaux de cache externes sont généralement adressés physiquement, mais vous pouvez uniquement allouer et accéder aux adresses virtuelles. Si vous allouez juste une région de taille LLC, vous n'obtiendrez généralement pas une couverture complète de tous les chemins de chaque ensemble de cache: certains ensembles seront surreprésentés (et seront donc entièrement vidés), tandis que d'autres seront sous-représentés et ainsi toutes les valeurs existantes peuvent même être vidées en accédant à cette région de la mémoire. Une surallocation de 2x rend très probable que presque tous les ensembles ont une représentation suffisante.
  • Vous voulez éviter que l'optimiseur ne fasse des choses intelligentes, par exemple en notant que la mémoire n'échappe jamais à la fonction et en éliminant toutes vos lectures et écritures.
  • Vous voulez itérativement au hasard au hasard de la région de la mémoire, plutôt que de la parcourir linéairement: certaines conceptions comme la LLC sur un Intel récent détectent quand un pattern "streaming" est présent, et passent de LRU à MRU puisque LRU est sur la politique de remplacement la plus mauvaise possible pour une telle charge. L'effet est que, peu importe le nombre de fois que vous diffusez de la mémoire, certaines «anciennes» lignes d'avant vos efforts peuvent rester dans le cache. L'accès aléatoire à la mémoire annule ce comportement. Vous voulez accéder à plus que la quantité de mémoire LLC pour (a) la même raison que vous allouez plus de taille LLC (accès virtuel vs mise en cache physique) et (b) parce que l'accès aléatoire nécessite plus d'accès avant d'avoir un haut (c) les caches ne sont généralement que des pseudo-LRU, donc vous avez besoin de plus que le nombre d'accès que vous attendez sous LRU exact pour vider chaque ligne.

Même si ce n'est pas infaillible. D'autres optimisations matérielles ou comportements de mise en cache non considérés ci-dessus peuvent entraîner l'échec de cette approche. Vous pourriez obtenir très malchanceux avec l'allocation de page fournie par le système d'exploitation et ne pas être en mesure d'atteindre toutes les pages (vous pouvez largement atténuer cela en utilisant des pages de 2 Mo). Je recommande fortement de tester si votre technique de flush est adéquate: une méthode consiste à mesurer le nombre de caches d'antémémoire en utilisant les compteurs de performance du processeur pendant l'exécution de votre test et de vérifier si le nombre est correct. Notez que cela laisse tous les niveaux de l'antémémoire avec des lignes en état E (exclusif) ou S (partagé), et non l'état M (modifié). Cela signifie que ces lignes n'ont pas besoin d'être expulsées vers d'autres niveaux de cache lorsqu'elles sont remplacées par des accès dans votre benchmark: elles peuvent simplement être supprimées. L'approche décrite dans le other answer laissera la plupart/toutes les lignes dans l'état M, de sorte que vous disposerez initialement d'une ligne de trafic d'expulsion pour chaque ligne à laquelle vous accédez dans votre cas-test. Vous pouvez obtenir le même comportement avec ma recette ci-dessus en changeant l'étape 4 pour écrire plutôt que lire.De ce point de vue, aucune approche n'est intrinsèquement «meilleure» que l'autre: dans le monde réel, les niveaux de cache auront un mélange de lignes modifiées et non modifiées, alors que ces approches quittent la mémoire cache aux deux extrêmes: le continuum. En principe, vous pouvez faire un benchmark avec les états tout-M et non-M, et voir si cela compte beaucoup: si c'est le cas, vous pouvez essayer d'évaluer ce que l'état réel du cache sera habituellement une réplique.


Rappelez-vous que les tailles de LLC sont en croissance presque chaque génération de CPU (la plupart du temps parce que nombre de base augmentent), de sorte que vous voulez laisser un peu de place pour la croissance si cela doit être à l'épreuve. Je viens de jeter cela là-bas comme si c'était "facile", mais en réalité peut être très difficile en fonction de votre problème exact.