Comment nous avons doublé la vitesse de travail avec Float en Mono


Mon ami Aras a récemment écrit le même ray tracer dans différents langages, y compris C ++, C # et le compilateur Unity Burst. Bien sûr, il est naturel de s'attendre à ce que C # soit plus lent que C ++, mais il m'a semblé intéressant que Mono soit tellement plus lent que .NET Core.

Ses indicateurs publiés étaient médiocres:

  • C # (.NET Core): Mac 17,5 Mray / s,
  • C # (Unity, Mono): Mac 4.6 Mray / s,
  • C # (Unity, IL2CPP): Mac 17.1 Mray / s

J'ai décidé de voir ce qui se passait et de documenter les endroits qui pourraient être améliorés.

Grâce à cette référence et à l'étude de ce problème, nous avons trouvé trois domaines dans lesquels une amélioration est possible:

  • Tout d'abord, vous devez améliorer les paramètres Mono par défaut, car les utilisateurs ne configurent généralement pas leurs paramètres
  • Deuxièmement, nous devons introduire activement le monde au backend de l'optimisation de code LLVM en Mono
  • Troisièmement, nous avons amélioré le réglage de certains paramètres mono.

Le point de référence de ce test était les résultats du traceur de rayons exécuté sur ma machine, et puisque j'ai un matériel différent, nous ne pouvons pas comparer les chiffres.

Les résultats sur mon iMac domestique pour Mono et .NET Core étaient les suivants:

Environnement de travailRésultats, MRay / sec
.NET Core 2.1.4, dotnet run débogage dotnet run3,6
dotnet run -c Release .NET Core 2.1.4 build build dotnet run -c Release21,7
Vanilla Mono, mono Maths.exe6,6
Vanilla Mono avec LLVM et float3215,5

Au cours de l'étude de ce problème, nous avons trouvé quelques problèmes, après la correction desquels les résultats suivants ont été obtenus:

Environnement de travailRésultats, MRay / sec
Mono avec LLVM et float3215,5
Mono avancé avec LLVM, float32 et fixe en ligne29,6

La vue d'ensemble:


En appliquant simplement LLVM et float32, vous pouvez augmenter les performances du code à virgule flottante de près de 2,3 fois. Et après le réglage, que nous avons ajouté à Mono à la suite de ces expériences, vous pouvez augmenter la productivité de 4,4 fois par rapport au Mono standard - ces paramètres dans les futures versions de Mono deviendront les paramètres par défaut.

Dans cet article, je vais expliquer nos résultats.

Flottant 32 bits et 64 bits


Aras utilise des nombres à virgule flottante 32 bits pour la partie principale des calculs (tapez float en C # ou System.Single dans .NET). Dans Mono, nous avons fait une erreur il y a longtemps - tous les calculs en virgule flottante 32 bits ont été effectués en 64 bits et les données étaient toujours stockées dans des zones 32 bits.

Aujourd'hui, ma mémoire n'est pas aussi nette qu'auparavant, et je ne me souviens pas exactement pourquoi nous avons pris une telle décision.

Je ne peux que supposer qu'il a été influencé par les tendances et les idées de l'époque.

Puis une aura positive a plané autour de l'informatique flottante avec une précision accrue. Par exemple, les processeurs Intel x87 ont utilisé une précision de 80 bits pour les calculs en virgule flottante, même lorsque les opérandes étaient doubles, ce qui a fourni aux utilisateurs des résultats plus précis.

À cette époque, l'idée était également pertinente que dans l'un de mes projets précédents - Feuilles de calcul Gnumeric - les fonctions statistiques aient été mises en œuvre plus efficacement que dans Excel. Par conséquent, de nombreuses communautés sont bien conscientes de l'idée que des résultats plus précis avec une précision accrue peuvent être utilisés.

Aux étapes initiales du développement Mono, la plupart des opérations mathématiques effectuées sur toutes les plates-formes ne pouvaient recevoir que le double en entrée. Des versions 32 bits ont été ajoutées à C99, Posix et ISO, mais à cette époque, elles n'étaient pas largement disponibles pour l'ensemble de l'industrie (par exemple, sinf est la version flottante de sin , fabsf est la version de fabs , etc.).

Bref, le début des années 2000 a été une période d'optimisme.

Les applications ont payé un lourd tribut pour l'augmentation du temps de calcul, mais Mono était principalement utilisé pour les applications Linux de bureau servant des pages HTTP et certains processus de serveur, donc la vitesse à virgule flottante n'était pas le problème que nous rencontrions quotidiennement. Il n'est devenu visible que dans certains référentiels scientifiques, et en 2003, ils ont rarement été développés sur .NET.

Aujourd'hui, les jeux, les applications 3D, le traitement d'image, la réalité virtuelle, la réalité augmentée et l'apprentissage automatique ont fait des opérations en virgule flottante un type de données plus courant. Le problème ne vient pas seul et il n'y a pas d'exceptions. Float n'était plus le type de données convivial utilisé dans le code à quelques endroits. Ils se sont transformés en avalanche, d'où il n'y a nulle part où se cacher. Ils sont nombreux et leur propagation ne peut être stoppée.

Indicateur d'espace de travail float32


Par conséquent, il y a quelques années, nous avons décidé d'ajouter la prise en charge de l'exécution d'opérations flottantes 32 bits à l'aide d'opérations 32 bits, comme dans tous les autres cas. Nous avons appelé cette fonctionnalité de l'espace de travail «float32». En Mono, il est activé en ajoutant l'option --O=float32 dans l'environnement de travail, et dans les applications Xamarin, ce paramètre est modifié dans les paramètres du projet.

Ce nouveau drapeau a été bien reçu par nos utilisateurs mobiles, car les appareils mobiles ne sont toujours pas trop puissants et il est préférable de traiter les données plus rapidement que d'avoir une précision accrue. Nous avons recommandé aux utilisateurs mobiles d'activer le compilateur d'optimisation LLVM et l'indicateur float32 en même temps.

Bien que ce drapeau soit implémenté depuis plusieurs années, nous ne l'avons pas fait par défaut pour éviter les mauvaises surprises des utilisateurs. Cependant, nous avons commencé à rencontrer des cas où des surprises surviennent en raison d'un comportement 64 bits standard, voir ce rapport de bogue soumis par l'utilisateur Unity .

Nous allons maintenant utiliser Mono float32 , les progrès peuvent être suivis ici: https://github.com/mono/mono/issues/6985 .

En attendant, je suis revenu sur le projet de mon ami Aras. Il a utilisé les nouvelles API ajoutées à .NET Core. Bien que .NET Core ait toujours effectué des opérations flottantes 32 bits en tant que flottants 32 bits, l'API System.Math effectue toujours des conversions de float en double dans le processus. Par exemple, si vous devez calculer la fonction sinus pour une valeur flottante, alors la seule option est d'appeler Math.Sin (double) , et vous devrez convertir de flottant en double.

Pour résoudre ce problème, un nouveau type de System.MathF été ajouté à .NET Core qui contient des opérations mathématiques avec virgule flottante simple précision, et maintenant nous venons de déplacer ce [System.MathF] en Mono .

La transition de 64 bits à 32 bits float améliore considérablement les performances, comme le montre ce tableau:

Environnement de travail et optionsMrays / seconde
Mono avec System.Math6,6
Mono avec System.Math et -O=float328.1
Mono avec System.MathF6.5
Mono avec System.MathF et -O=float328.2

Autrement dit, l'utilisation de float32 dans ce test améliore vraiment les performances et MathF a peu d'effet.

Configuration de LLVM


Au cours de cette recherche, nous avons constaté que bien que le compilateur Fast JIT Mono ait float32 support float32 , nous n'avons pas ajouté ce support au backend LLVM. Cela signifiait que Mono avec LLVM effectuait toujours des conversions coûteuses du flottant au double.

Par conséquent, Zoltan a ajouté la float32 charge de float32 au moteur de génération de code LLVM.

Il a ensuite remarqué que notre inliner utilise les mêmes heuristiques pour Fast JIT que celles utilisées pour LLVM. Lorsque vous travaillez avec Fast JIT, il est nécessaire de trouver un équilibre entre la vitesse JIT et la vitesse d'exécution.Par conséquent, nous avons limité la quantité de code intégré afin de réduire la quantité de travail du moteur JIT.

Mais si vous décidez d'utiliser LLVM en Mono, vous vous efforcez d'obtenir le code le plus rapidement possible, nous avons donc modifié les paramètres en conséquence. Aujourd'hui, ce paramètre peut être modifié à l'aide de la MONO_INLINELIMIT environnement MONO_INLINELIMIT , mais en fait, il doit être écrit aux valeurs par défaut.

Voici les résultats avec les paramètres LLVM modifiés:

Environnement de travail et optionsMrays / secondes
Mono avec System.Math --llvm -O=float3216,0
Mono avec System.Math --llvm -O=float32 , heuristique constante29,1
Mono avec System.MathF --llvm -O=float32 , heuristique constante29,6

Prochaines étapes


Peu d'efforts ont été nécessaires pour apporter toutes ces améliorations. Ces changements ont été menés par des discussions périodiques à Slack. J'ai même réussi à faire quelques heures un soir pour porter System.MathF vers Mono.

Le code Aras ray tracer est devenu un sujet d'étude idéal car il était autosuffisant, c'était une vraie application, et non une référence synthétique. Nous voulons trouver d'autres logiciels similaires qui peuvent être utilisés pour étudier le code binaire que nous générons, et nous assurer que nous transmettons à LLVM les meilleures données pour l'exécution optimale de son travail.

Nous envisageons également de mettre à jour notre LLVM et d'utiliser les nouvelles optimisations ajoutées.

Note séparée


La précision supplémentaire a de beaux effets secondaires. Par exemple, en lisant les demandes de pool du moteur Godot, j'ai vu qu'il y avait une discussion active sur l'opportunité de rendre la précision des opérations en virgule flottante personnalisable au moment de la compilation ( https://github.com/godotengine/godot/pull/17134 ).

J'ai demandé à Juan pourquoi cela pouvait être nécessaire pour quelqu'un, car je pensais que les opérations en virgule flottante 32 bits étaient assez suffisantes pour les jeux.

Juan a expliqué que dans le cas général, les flotteurs fonctionnent très bien, mais si vous vous éloignez du centre, disons, éloignez-vous de 100 kilomètres du centre du jeu, une erreur de calcul commence à s'accumuler, ce qui peut conduire à des problèmes graphiques intéressants. Vous pouvez utiliser différentes stratégies pour réduire l'impact de ce problème, et l'une d'entre elles consiste à travailler avec une précision accrue, pour laquelle vous devez payer pour les performances.

Peu de temps après notre conversation, dans mon fil Twitter, j'ai vu un post démontrant ce problème: http://pharr.org/matt/blog/2018/03/02/rendering-in-camera-space.html

Le problème est illustré dans les images ci-dessous. Nous voyons ici un modèle de voiture de sport de l'ensemble pbrt-v3-scènes ** . La caméra et la scène sont proches de l'origine, et tout semble parfait.


** (Auteur de Yasutoshi Mori .)

Ensuite, nous déplaçons la caméra et la scène de 200 000 unités à xx, yy et zz depuis l'origine. On peut voir que le modèle de la machine est devenu assez fragmenté; cela est uniquement dû à un manque de précision dans les nombres à virgule flottante.


Si l'on avance encore 5 × 5 × 5 fois, à 1 million d'unités de l'origine, le modèle commence à se désintégrer; la machine se transforme en une approximation de voxel extrêmement grossière d'elle-même, à la fois intéressante et terrifiante. (Keanu a posé la question: Minecraft est-il si cubique simplement parce que tout est rendu très loin de l'origine?)


** (Je m'excuse auprès de Yasutoshi Mori pour ce que nous avons fait avec son beau modèle.)

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


All Articles