La fonction non reconnue ralentit le programme 5 fois

Ralentissement de Windows, partie 3: arrêt du processus



L'auteur est engagé dans l'optimisation des performances de Chrome chez Google - env. trans.

À l'été 2017, j'ai eu des problèmes avec les performances de Windows. L'arrêt du processus a été lent, sérialisé et a bloqué la file d'attente d'entrée du système, ce qui a provoqué plusieurs blocages du curseur de la souris lors de l'assemblage de Chrome. La raison principale était qu'à la fin des processus, Windows passait beaucoup de temps à rechercher des objets GDI, tout en maintenant la section critique de l'utilisateur global du système32. J'en ai parlé dans l'article "Processeur 24 cœurs, mais je ne peux pas déplacer le curseur" .

Microsoft a corrigé le bogue et je suis retourné à mon entreprise, mais il s'est avéré que le bogue était de retour. Il y avait des plaintes concernant la lenteur du fonctionnement des tests LLVM, avec des blocages d'entrée fréquents.

Mais en fait, le bug n'est pas revenu. La raison en était un changement dans notre code.

Numéro 2017


Chaque processus Windows contient plusieurs descripteurs d'objet GDI standard. Pour les processus qui ne font rien avec les graphiques, ces descripteurs sont généralement NULL. À la fin du processus, Windows appelle certaines fonctions pour ces descripteurs, même si elles sont NULL. Peu importait - les fonctionnalités fonctionnaient rapidement - jusqu'à la sortie de Windows 10 Anniversary Edition, dans laquelle certaines modifications de sécurité rendaient ces fonctionnalités lentes . Pendant le fonctionnement, ils détenaient le même verrou que celui utilisé pour les événements d'entrée. Lorsqu'un grand nombre de processus se terminent en même temps, chacun fait plusieurs appels à la fonction lente qui détient ce verrou critique, ce qui conduit finalement à bloquer l'entrée de l'utilisateur et à bloquer le curseur.

Le correctif de Microsoft ne devait pas appeler ces fonctions pour les processus sans objets GDI. Je ne connais pas les détails, mais je pense que le patch Microsoft était quelque chose comme ça:

+ if (IsGUIProcess())
+ NtGdiCloseProcess();
– NtGdiCloseProcess();


Autrement dit, ignorez simplement le nettoyage GDI si le processus n'est pas un processus GUI / GDI.

Étant donné que les compilateurs et autres processus que nous créons et terminons rapidement n'utilisaient pas d'objets GDI, ce correctif s'est avéré suffisant pour corriger le gel de l'interface utilisateur.

Numéro 2018


Il s'est avéré que les processus sont très facilement attribués à certains objets GDI standard. Si votre processus charge gdi32.dll, vous recevrez automatiquement des objets GDI (DC, surfaces, régions, pinceaux, polices, etc.), que vous en ayez besoin ou non (notez que ces objets GDI standard ne sont pas affichés dans le Gestionnaire des tâches parmi les objets GDI du processus).

Mais cela ne devrait pas être un problème. Je veux dire, pourquoi le compilateur chargerait-il gdi32.dll? Eh bien, il s'est avéré que si vous chargez user32.dll, shell32.dll, ole32.dll ou de nombreuses autres DLL, vous obtiendrez automatiquement en plus gdi32.dll (avec les objets GDI standard susmentionnés). Et il est très facile de télécharger accidentellement une de ces bibliothèques.

Tests LLVM lors du chargement de chaque processus appelé CommandLineToArgvW (shell32.dll), et parfois appelé SHGetKnownFolderPath (également shell32.dll) Ces appels étaient suffisants pour extraire gdi32.dll et générer ces objets GDI standard effrayants. Étant donné que la suite de tests LLVM génère autant de processus, elle finit par sérialiser à la fin des processus, provoquant d'énormes retards et gels d'entrée, bien pires que ceux de 2017.

Mais cette fois, nous connaissions le principal problème du blocage, nous avons donc immédiatement su quoi faire.

Tout d'abord, nous nous sommes débarrassés de l'appel de CommandLineToArgvW , en analysant manuellement la ligne de commande . Après cela, la suite de tests LLVM a rarement appelé une fonction à partir d'une DLL problématique. Mais nous savions à l'avance que cela n'affecterait en rien les performances. La raison en était que même l'appel conditionnel restant était suffisant pour toujours tirer shell32.dll, qui à son tour tirait gdi32.dll, qui crée des objets GDI standard.

Le deuxième correctif était le chargement retardé de shell32.dll . Un chargement retardé signifie que la bibliothèque se charge à la demande - lorsque la fonction est appelée - au lieu de se charger au démarrage du processus. Cela signifiait que shell32.dll et gdi32.dll se chargeraient rarement et pas toujours.

Après cela, la suite de tests LLVM a commencé à fonctionner cinq fois plus rapidement - en une minute au lieu de cinq. Et plus aucune souris ne se bloque sur les machines de développement, pour que les employés puissent travailler normalement lors de l'exécution des tests. C'est une accélération folle pour un changement aussi modeste, et l'auteur des correctifs était si reconnaissant pour mon enquête qu'il m'a proposé une prime d'entreprise .

Parfois, les plus petits changements ont les plus grandes conséquences. Vous avez juste besoin de savoir où composer le «zéro» .

Chemin d'exécution non accepté


Il convient de répéter que nous avons prêté attention au code qui ne s'est pas exécuté - et cela a été un changement clé. Si vous disposez d'un outil de ligne de commande qui n'accède pas à gdi32.dll, l'ajout de code avec un appel de fonction conditionnelle ralentira le processus plusieurs fois si gdi32.dll est chargé. Dans l'exemple ci-dessous, CommandLineToArgvW n'est jamais appelé, mais même une simple présence dans le code (sans délai d'appel) affecte négativement les performances:

 int main(int argc, char* argv[]) { if (argc < 0) { CommandLineToArgvW(nullptr, nullptr); // shell32.dll, pulls in gdi32.dll } } 

Alors oui, la suppression d'un appel de fonction, même si le code n'est jamais exécuté, peut être suffisante pour améliorer considérablement les performances dans certains cas.

Reproduction de pathologie


Lorsque j'ai enquêté sur l'erreur initiale, j'ai écrit un programme ( ProcessCreateTests ) qui a créé 1000 processus puis les a tous tués en parallèle. Cela a reproduit le gel, et lorsque Microsoft a corrigé l'erreur, j'ai utilisé un programme de test pour vérifier le correctif: voir la vidéo . Après la réincarnation du bogue, j'ai changé mon programme en ajoutant l'option -user32 , qui charge user32.dll pour chacun des milliers de processus de test. Comme prévu, le temps d'exécution de tous les processus de test augmente considérablement avec cette option, et il est facile de détecter les blocages du curseur de la souris. Le temps de création de processus augmente également avec l'option -user32, mais il n'y a aucune suspension de curseur pendant la création de processus. Vous pouvez utiliser ce programme et voir à quel point le problème peut être grave. Voici quelques résultats typiques de mon ordinateur portable à quatre cœurs / huit fils après une semaine de disponibilité. L'option -user32 augmente le temps pour tout, mais le verrouillage UserCrit sur les processus se termine de manière particulièrement dramatique:

> ProcessCreatetests.exe
Process creation took 2.448 s (2.448 ms per process).
Lock blocked for 0.008 s total, maximum was 0.001 s.

Process destruction took 0.801 s (0.801 ms per process).
Lock blocked for 0.004 s total, maximum was 0.001 s.

> ProcessCreatetests.exe -user32
Testing with 1000 descendant processes with user32.dll loaded.
Process creation took 3.154 s (3.154 ms per process).
Lock blocked for 0.032 s total, maximum was 0.007 s.

Process destruction took 2.240 s (2.240 ms per process).
Lock blocked for 1.991 s total, maximum was 0.864 s.


Creuser plus profondément juste pour le plaisir


J'ai pensé à certaines méthodes ETW qui peuvent être utilisées pour étudier le problème plus en détail et j'ai déjà commencé à les écrire. Mais je suis tombé sur un comportement aussi inexplicable, que j'ai décidé de consacrer un article séparé. Qu'il suffise de dire que dans ce cas, Windows se comporte encore plus étrangement.

Autres articles de la série:


Littérature


Source: https://habr.com/ru/post/fr432076/


All Articles