La situation générale
Une application extrêmement gourmande en bande passante, en utilisation du CPU et en utilisation du GPU doit transférer environ 10 à 15 Go par seconde d'un GPU à un autre. Elle utilise l'API DX11 pour accéder au GPU, de sorte que le chargement vers le GPU ne peut se faire qu'avec des tampons qui nécessitent un mappage pour chaque chargement. Le chargement se fait par tranches de 25 Mo à la fois, et 16 threads écrivent simultanément des tampons vers des tampons mappés. Il n'y a pas grand-chose que l'on puisse faire à ce sujet. Le niveau de concurrence réel des écritures devrait être plus faible, si ce n'est pour le bogue suivant.
Il s'agit d'une station de travail musclée dotée de trois GPU Pascal, d'un processeur Haswell haut de gamme et d'une mémoire vive à quatre canaux. Il n'y a pas grand-chose à améliorer sur le plan matériel. Il fonctionne avec une édition de bureau de Windows 10.
Le problème réel
Une fois que j'ai dépassé ~50% de charge CPU, quelque chose dans MmPageFault()
(à l'intérieur du noyau Windows, appelé lors de l'accès à la mémoire qui a été mappée dans votre espace d'adressage, mais qui n'a pas encore été validée par le système d'exploitation) se casse horriblement, et les 50 % restants de la charge du CPU sont gaspillés sur un spin-lock à l'intérieur de l'espace d'adressage. MmPageFault()
. L'unité centrale est utilisée à 100 % et les performances de l'application se dégradent complètement.
Je dois supposer que cela est dû à l'immense quantité de mémoire qui doit être allouée au processus chaque seconde et qui est également complètement désaffectée du processus chaque fois que le tampon DX11 est désaffecté. En conséquence, il s'agit en fait de milliers d'appels à MmPageFault()
par seconde, se produisant séquentiellement comme memcpy()
écrit séquentiellement dans le tampon. Pour chaque page non engagée rencontrée.
Lorsque la charge du processeur dépasse 50 %, le spin-lock optimiste du noyau Windows qui protège la gestion des pages se dégrade complètement en termes de performances.
Considérations
Le tampon est alloué par le pilote DX11. Rien ne peut être modifié dans la stratégie d'allocation. L'utilisation d'une API mémoire différente et surtout la réutilisation ne sont pas possibles.
Les appels à l'API DX11 (mappage/démappage des tampons) se font à partir d'un seul thread. Les opérations de copie proprement dites peuvent se dérouler en multithread sur plus de threads qu'il n'y a de processeurs virtuels dans le système.
Il n'est pas possible de réduire les besoins en bande passante de la mémoire. Il s'agit d'une application en temps réel. En fait, la limite absolue est actuellement la bande passante PCIe 3.0 16x du GPU principal. Si je le pouvais, j'aurais déjà besoin de pousser plus loin.
Il n'est pas possible d'éviter les copies multithread, car il existe des files d'attente producteur-consommateur indépendantes qui ne peuvent pas être fusionnées de manière triviale.
La dégradation des performances du spin-lock semble être si rare (parce que le cas d'utilisation le pousse si loin) que sur Google, vous ne trouverez pas un seul résultat pour le nom de la fonction spin-lock.
La mise à niveau vers une API qui donne plus de contrôle sur les mappings (Vulkan) est en cours, mais ce n'est pas une solution à court terme. Le passage à un meilleur noyau d'OS n'est actuellement pas une option pour la même raison.
Réduire la charge du CPU ne fonctionne pas non plus ; il y a trop de travail à faire en dehors de la copie de la mémoire tampon (généralement triviale et peu coûteuse).
La question
Que peut-on faire ?
J'ai besoin de réduire considérablement le nombre de pages par défaut individuelles. Je connais l'adresse et la taille du tampon qui a été mappé dans mon processus, et je sais également que la mémoire n'a pas encore été engagée.
Comment puis-je m'assurer que la mémoire est engagée avec le moins de transactions possible ?
Des drapeaux exotiques pour DX11 qui empêcheraient la désallocation des tampons après le démappage, des API Windows pour forcer le commit en une seule transaction, à peu près tout est bienvenu.
L'état actuel
// In the processing threads
{
DX11DeferredContext->Map(..., &buffer)
std::memcpy(buffer, source, size);
DX11DeferredContext->Unmap(...);
}