Dans la continuité de la réponse de @Mike_Dunlavey :
Tout d'abord, obtenez un profil temporel, en utilisant votre outil préféré : VTune ou PTU ou OProf.
Ensuite, obtenez un profil de manque de cache. Les ratés du cache L1, ou les ratés du cache L2, ou ...
C'est-à-dire que le premier profil associe un "temps passé" à chaque compteur de programme. Le second associe une valeur de "nombre d'échecs de cache" à chaque compteur de programme.
Note : Je "réduis" souvent les données, en les additionnant par fonction, ou (si j'ai la technologie) par boucle. Ou par tranches de, disons, 64 octets. Comparer les compteurs de programmes individuels n'est souvent pas utile, parce que les compteurs de performance sont flous - l'endroit où vous voyez un manque de cache être rapporté est souvent plusieurs instructions différentes de l'endroit où il s'est réellement produit.
OK, maintenant, faites un graphique de ces deux profils pour les comparer. Voici quelques graphiques que je trouve utiles :
Graphiques "Iceberg" : L'axe X représente le PC, l'axe Y positif représente le temps, l'axe Y négatif représente les échecs de cache. Cherchez des endroits qui vont à la fois vers le haut et vers le bas.
(Les graphiques "entrelacés" sont également utiles : même idée, l'axe X est le PC, le temps et les manques dans la mémoire cache sont représentés sur l'axe Y, mais avec des lignes verticales étroites de différentes couleurs, généralement rouge et bleu. Les endroits où l'on dépense beaucoup de temps et de manques de cache auront des lignes rouges et bleues finement entrelacées, ressemblant presque à du violet. Cela s'étend aux manques de cache L2 et L3, tous sur le même graphique. Au fait, vous voudrez probablement "normaliser" les chiffres, soit en % du temps total ou des ratés du cache, soit, encore mieux, en % du point de données maximum du temps ou des ratés du cache. Si vous vous trompez dans l'échelle, vous ne verrez rien).
Graphiques XY pour chaque case d'échantillonnage (PC, ou fonction, ou boucle, ou...) tracer un point dont dont la coordonnée X est le temps normalisé, et dont la coordonnée Y est les manques normalisés dans le cache . Si vous obtenez un grand nombre de points de données dans le coin supérieur droit - un pourcentage élevé de temps ET un pourcentage élevé de ratés du cache - c'est une preuve intéressante. Ou, oubliez le nombre de points - si la somme de tous les pourcentages dans le coin supérieur est grande...
Notez, malheureusement, que vous devez souvent effectuer ces analyses vous-même. Aux dernières nouvelles, VTune ne le fait pas pour vous. J'ai utilisé gnuplot et Excel. (Attention : Excel meurt au-delà de 64 mille points de données).
Plus de conseils :
Si votre pointeur intelligent est inlined, vous risquez d'avoir des comptes partout. Dans un monde idéal, vous seriez en mesure de retracer les PC jusqu'à la ligne de code source d'origine. Dans ce cas, vous voudrez peut-être différer un peu la réduction : regardez tous les PC individuels ; remontez-les jusqu'aux lignes de code source ; puis remontez-les jusqu'à la fonction d'origine. De nombreux compilateurs, par exemple GCC, ont des options de table de symboles qui vous permettent de faire cela.
D'ailleurs, je soupçonne que votre problème n'est PAS dû au pointeur intelligent qui cause des problèmes de cache. A moins que vous ne fassiez du smart_ptr<int> partout. Si vous faites des smart_ptr<Obj>, et que sizeof(Obj) + est supérieur à, disons, 4*sizeof(Obj*) (et si le smart_ptr lui-même n'est pas énorme), alors ce n'est pas si grave.
Il est plus probable que ce soit le niveau supplémentaire d'indirection du pointeur intelligent qui cause votre problème.
Par coïncidence, je discutais au déjeuner avec un type qui avait un pointeur intelligent compté en référence qui utilisait un handle, c'est-à-dire un niveau d'indirection, quelque chose du genre
template<typename T> class refcntptr {
refcnt_handle<T> handle;
public:
refcntptr(T*obj) {
this->handle = new refcnt_handle<T>();
this->handle->ptr = obj;
this->handle->count = 1;
}
};
template<typename T> class refcnt_handle {
T* ptr;
int count;
friend refcnt_ptr<T>;
};
(Je ne le coderais pas de cette façon, mais cela sert à l'exposition).
La double indirection ce->handle->ptr peut être un gros problème de performance. Ou même une triple indirection, this->handle->ptr->field. Au minimum, sur une machine avec des hits de cache L1 de 5 cycles, chaque this->handle->ptr->field prendrait 10 cycles. Et serait beaucoup plus difficile à chevaucher qu'une simple poursuite de pointeur. Mais, pire encore, si chacun d'entre eux est un manque de cache L1, même s'il n'y a que 20 cycles vers la L2... eh bien, il est beaucoup plus difficile de cacher 2*20=40 cycles de latence de manque de cache, qu'un seul manque de cache L1.
En général, il est conseillé d'éviter les niveaux d'indirection dans les pointeurs intelligents. Au lieu de pointer vers un handle, vers lequel tous les pointeurs intelligents pointent, et qui lui-même pointe vers l'objet, vous pouvez rendre le pointeur intelligent plus grand en le faisant pointer vers l'objet ainsi que vers le handle. (Qui n'est alors plus ce qu'on appelle communément un handle, mais plutôt un objet d'information).
Par exemple
template<typename T> class refcntptr {
refcnt_info<T> info;
T* ptr;
public:
refcntptr(T*obj) {
this->ptr = obj;
this->info = new refcnt_handle<T>();
this->info->count = 1;
}
};
template<typename T> class refcnt_info {
T* ptr; // perhaps not necessary, but useful.
int count;
friend refcnt_ptr<T>;
};
Quoi qu'il en soit, un profil temporel est votre meilleur ami.
Oh, oui - le matériel Intel EMON peut également vous dire combien de cycles vous avez attendu sur un PC. Cela permet de distinguer un grand nombre de ratés de L1 d'un petit nombre de ratés de L2.