Comment hâter de commettre la mémoire allouée en C ++?

La situation générale

Une application extrêmement intensive en termes de bande passante, d’utilisation du processeur et d’utilisation du processeur graphique doit transférer environ 10-15 Go par seconde d’un GPU à un autre. Il utilise l’API DX11 pour accéder au GPU. Le téléchargement sur le GPU ne peut donc s’effectuer qu’avec des tampons nécessitant un mappage pour chaque téléchargement. Le téléchargement se produit par blocs de 25 Mo à la fois et 16 threads écrivent des tampons sur les tampons mappés simultanément. Il n’y a pas grand chose à faire à ce sujet. Le niveau de concomitance réel des écritures devrait être inférieur, si ce n’était le bogue suivant.

C’est une station de travail robuste avec 3 GPU Pascal, un processeur Haswell haut de gamme et une mémoire vive à quatre canaux. Pas beaucoup peut être amélioré sur le matériel. Il exécute une édition de bureau de Windows 10.

Le problème réel

Une fois que j’ai passé ~ 50% de la charge du processeur, quelque chose dans MmPageFault() (à l’intérieur du kernel Windows, appelé lors de l’access à la mémoire mappée dans votre espace d’adressage mais pas encore MmPageFault() et les 50% restants La charge du processeur est gaspillée sur un spin-lock dans MmPageFault() . Le processeur est utilisé à 100% et les performances de l’application se dégradent complètement.

Je suppose 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éconnectée du processus à chaque fois que le tampon DX11 est déconnecté. En conséquence, il s’agit en réalité de milliers d’appels à MmPageFault() par seconde, qui se produisent séquentiellement lorsque memcpy() écrit de manière séquentielle dans le tampon. Pour chaque page unique non validée rencontrée.

Une fois la charge du processeur supérieure à 50%, le locking optimisé du kernel Windows protégeant la gestion des pages dégrade complètement les performances.

Considérations

Le tampon est alloué par le pilote DX11. Rien ne peut être modifié sur la stratégie d’allocation. L’utilisation d’une API de mémoire différente et en particulier la réutilisation n’est pas possible.

Les appels à l’API DX11 (mappage / démappage des tampons) se font tous à partir d’un seul thread. Les opérations de copie proprement dites peuvent se dérouler sous plusieurs threads sur plus de threads que de processeurs virtuels du système.

La réduction des besoins en bande passante mémoire n’est pas possible. C’est une application en temps réel. En fait, la limite ssortingcte est actuellement la bande passante PCIe 3.0 16x du GPU principal. Si je pouvais, je devrais déjà pousser plus loin.

Eviter les copies multithreads n’est pas possible, car il existe des files d’attente indépendantes producteur-consommateur qui ne peuvent être fusionnées sortingvialement.

La dégradation des performances de locking du spin semble être si rare (car 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 de blocage de spin.

La mise à niveau vers une API qui donne plus de contrôle sur les mappages (Vulkan) est en cours, mais elle ne convient pas comme solution à court terme. Passer à un meilleur kernel de système d’exploitation n’est actuellement pas une option pour la même raison.

Réduire la charge du processeur ne fonctionne pas non plus; il y a trop de travail à faire, autre que la copie tampon (généralement sortingviale et peu coûteuse).

La question

Ce qui peut être fait?

Je dois réduire considérablement le nombre de pages 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é validée.

Comment puis-je m’assurer que la mémoire est validée avec le moins de transactions possible?

Les drapeaux exotiques pour DX11 qui empêcheraient la désaffectation des tampons après la désinstallation, les API Windows pour forcer la validation en une seule transaction, à peu près tout est le bienvenu.

L’état actuel

 // In the processing threads { DX11DeferredContext->Map(..., &buffer) std::memcpy(buffer, source, size); DX11DeferredContext->Unmap(...); } 

Solution de contournement actuelle, pseudo-code simplifié:

 // During startup { SetProcessWorkingSetSize(GetCurrentProcess(), 2*1024*1024*1024, -1); } // In the DX11 render loop thread { DX11context->Map(..., &resource) VirtualLock(resource.pData, resource.size); notify(); wait(); DX11context->Unmap(...); } // In the processing threads { wait(); std::memcpy(buffer, source, size); signal(); } 

VirtualLock() force le kernel à sauvegarder immédiatement la plage d’adresses spécifiée avec la RAM. L’appel à la fonction VirtualUnlock() complémentaire est facultatif, il se produit implicitement (et sans coût supplémentaire) lorsque la plage d’adresses n’est pas cartographiée à partir du processus. (Si appelé explicitement, il en coûte environ 1/3 du coût de locking.)

Pour que VirtualLock() fonctionne, SetProcessWorkingSetSize() doit être appelé en premier, car la sum de toutes les régions de mémoire verrouillées par VirtualLock() ne peut pas dépasser la taille minimale définie pour le processus. Définir la taille de travail minimum à quelque chose de plus élevé que l’empreinte mémoire de base de votre processus n’a d’effets secondaires que si votre système échange réellement, votre processus ne consumra toujours pas plus de RAM que la taille réelle.


Seule l’utilisation de VirtualLock() , bien que dans des threads individuels et en utilisant des contextes DX11 différés pour les appels Map / Unmap , a permis de réduire instantanément la pénalité de performance de 40 à 50% à un peu plus acceptable de 15%.

L’abandon de l’utilisation d’un contexte différé et le déclenchement exclusif de toutes les erreurs logicielles , ainsi que la désaffectation correspondante lors de la suppression d’un mappage sur un seul thread, ont donné l’effet de performance nécessaire. Le coût total de ce locking est désormais inférieur à 1% de l’utilisation totale du processeur.


Résumé?

Lorsque vous vous attendez à des erreurs légères sous Windows, essayez de les conserver toutes dans le même thread. Effectuer un memcpy parallèle est sans problème, dans certaines situations, même nécessaire pour utiliser pleinement la bande passante mémoire. Cependant, c’est seulement si la mémoire est déjà validée pour la RAM. VirtualLock() est le moyen le plus efficace de garantir cela.

(Sauf si vous travaillez avec une API comme DirectX qui mappe la mémoire dans votre processus, vous ne rencontrerez probablement pas fréquemment de mémoire non validée. les défauts sont rares.)

Veillez simplement à éviter toute forme de défaut de page simultané lorsque vous travaillez avec Windows.