2017-07-21 1 views
20

Situation généraleComment faire pour accélérer la mémoire allouée en C++?

Une application qui est extrêmement intensive à la fois la bande passante, l'utilisation du processeur et l'utilisation du GPU doit transférer sur les 10-15GB par seconde d'un GPU à l'autre. Il utilise l'API DX11 pour accéder au GPU, donc le téléchargement vers le GPU ne peut se faire qu'avec des tampons nécessitant un mappage pour chaque téléchargement. Le téléchargement se fait par blocs de 25 Mo à la fois, et 16 threads écrivent des tampons simultanément dans les buffers mappés. Il n'y a pas grand-chose à faire à propos de tout cela. Le niveau de concurrence réel des écritures devrait être inférieur, s'il n'y avait pas le bogue suivant.

C'est un poste de travail costaud avec 3 GPU Pascal, un processeur Haswell haut de gamme et une RAM à quatre canaux. Pas beaucoup peut être amélioré sur le matériel. Il est en cours d'exécution d'une édition de bureau de Windows 10.

Le problème réel

Une fois que je passe ~ 50% de charge CPU, quelque chose dans MmPageFault() (à l'intérieur du noyau Windows, appelé lors de l'accès mémoire qui a été cartographiée dans votre l'espace d'adressage, mais n'a pas encore été commis par le système d'exploitation) se brise horriblement, et la charge de CPU 50% restante est gaspillée sur un spin-lock à l'intérieur MmPageFault(). Le processeur est utilisé à 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émappée du processus chaque fois que le tampon DX11 est démappé. En conséquence, il s'agit en fait des milliers d'appels à MmPageFault() par seconde, se produisant séquentiellement que memcpy() écrit séquentiellement dans le tampon. Pour chaque page non validée rencontrée.

Si la charge du processeur dépasse 50%, le spin-lock optimiste du noyau Windows protégeant la gestion des pages dégrade complètement les performances.

Considérations

La mémoire tampon est allouée par le conducteur DX11. Rien ne peut être modifié au sujet de la stratégie d'allocation. L'utilisation d'une API mémoire différente et surtout 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 réelles sont potentiellement multi-threadées sur plus de threads que de processeurs virtuels dans le 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 dure est actuellement la bande passante PCIe 3.0 16x du GPU primaire. Si je pouvais, je devrais déjà 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 de spin-lock semble être si rare (parce que le cas d'utilisation 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 mappages (Vulkan) est en cours, mais elle ne convient pas comme solution à court terme. Passer à un meilleur noyau OS n'est actuellement pas une option pour la même raison.

La réduction de la charge du processeur ne fonctionne pas non plus; il y a trop de travail qui doit être fait autre que la copie du tampon (généralement triviale et peu coûteuse).

La question

Que peut-on faire?

Je dois réduire de manière significative le nombre d'erreurs de page individuelles. Je connais l'adresse et la taille du buffer qui a été mappé dans mon processus, et je sais aussi 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?

Drapeaux exotiques pour DX11 qui empêcheraient la désaffectation des tampons après le démappage, 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(...); 
} 
+1

il semble que vous êtes à environ 400 M pour tous les 16 threads tous ensemble. Relativement bas. Pouvez-vous vérifier que vous ne dépassez pas cela dans votre application? Quelle est la consommation de mémoire pick là-bas? Je me demande si vous avez une fuite de mémoire. – Serge

+0

La consommation de pointe est d'environ 7-8 Go, mais c'est normal, étant donné que l'ensemble du pipeline de traitement a besoin de> 1s de mémoire tampon pour compenser toutes sortes de goulots d'étranglement. Oui, c'est "seulement" 400Mo, 25 fois par seconde. Et cela fonctionne très bien, jusqu'à ce que la charge du processeur de base dépasse 50% et que les performances de la verrou de rotation passent soudainement de 0 à 40-50% de l'utilisation totale du processeur. Affecte également d'autres processus sur le système en même temps. – Ext3h

+1

1. Quelle est votre mémoire physique? pouvez-vous tuer tous les autres processus actifs? 2. devinez # 2 puisque vous voyez le seuil de 50%, vous pourriez entrer dans quelques problèmes avec hyperthreading. Combien de cœurs physiques avez-vous? 8? Pouvez-vous désactiver l'hyperthreading? Essayez d'exécuter autant de threads qu'il y a de processeurs physiques dans votre cas sur une machine propre. – Serge

Répondre

11

solution actuelle, le code de pseudo 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() oblige le noyau à sauvegarder la plage d'adresses spécifiée avec RAM immédiatement. L'appel à la fonction complémentaire VirtualUnlock() est facultatif, il se produit implicitement (et sans coût supplémentaire) lorsque la plage d'adresses est démappée du processus. (Si appelé explicitement, il en coûte environ 1/3 du coût de blocage.)

Pour VirtualLock() travailler du tout, SetProcessWorkingSetSize() doit être appelée d'abord, comme la somme de toutes les régions de mémoire verrouillée par VirtualLock() ne peut pas dépasser la taille d'ensemble de travail minimum configurée pour le processus. La définition de la taille de jeu de travail «minimale» à quelque chose de plus élevé que l'empreinte mémoire de base de votre processus n'a pas d'effets secondaires, sauf si votre système est potentiellement échangeable, votre processus ne consommera pas plus de RAM que la taille réelle.


Juste l'utilisation de VirtualLock(), bien que dans des threads individuels et en utilisant des contextes DX11 différés pour Map/Unmap appels, ne diminuent instantanément la peine de performance à partir de 40-50% à 15% un peu plus acceptable.

Rejeter l'utilisation d'un contexte différé, et exclusivement le déclenchement des deux tous les défauts doux, ainsi que la désallocation correspondante lorsque sur un l'annulation du mappage seul thread, a donné le coup de pouce de la performance nécessaire. Le coût total de ce spin-lock est maintenant de < 1% de l'utilisation totale du processeur.


Résumé?

Lorsque vous attendez des erreurs logicielles sur Windows, essayez de les conserver toutes dans le même thread. Effectuer un parallèle memcpy lui-même est sans problème, dans certaines situations, même nécessaire d'utiliser pleinement la bande passante de la mémoire. Cependant, c'est seulement si la mémoire est déjà engagée dans la RAM. VirtualLock() est le moyen le plus efficace pour assurer cela.

(Si vous travaillez avec une API comme DirectX qui mappe la mémoire dans votre processus, vous ne rencontrerez probablement pas de mémoire non validée fréquemment Si vous travaillez simplement avec C++ standard new ou malloc votre mémoire est regroupée et recyclée dans votre processus Quoi qu'il en soit, les failles si légères sont rares.)

Assurez-vous d'éviter toute forme d'erreur de page simultanée lorsque vous travaillez avec Windows.