Wie wir die Geschwindigkeit der Arbeit mit Float in Mono verdoppelt haben


Mein Freund Aras hat kürzlich denselben Ray Tracer in verschiedenen Sprachen geschrieben, einschließlich C ++, C # und dem Unity Burst-Compiler. Natürlich ist zu erwarten, dass C # langsamer als C ++ ist, aber es schien mir interessant, dass Mono so langsamer als .NET Core ist.

Seine veröffentlichten Indikatoren waren schlecht:

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

Ich beschloss zu sehen, was geschah, und Orte zu dokumentieren, die verbessert werden könnten.

Als Ergebnis dieses Benchmarks und der Untersuchung dieses Problems haben wir drei Bereiche gefunden, in denen Verbesserungen möglich sind:

  • Zunächst müssen Sie die Standardeinstellungen für Mono verbessern, da Benutzer ihre Einstellungen normalerweise nicht konfigurieren
  • Zweitens müssen wir die Welt aktiv in das Backend der LLVM-Codeoptimierung in Mono einführen
  • Drittens haben wir die Abstimmung einiger Mono-Parameter verbessert.

Der Bezugspunkt dieses Tests waren die Ergebnisse des Raytracers , der auf meinem Computer ausgeführt wurde, und da ich unterschiedliche Hardware habe, können wir die Zahlen nicht vergleichen.

Die Ergebnisse auf meinem iMac für Mono und .NET Core zu Hause waren wie folgt:

ArbeitsumgebungErgebnisse, MRay / Sek
.NET Core 2.1.4, dotnet run Debug Build3.6
.NET Core 2.1.4 Release Build dotnet run -c Release21.7
Vanille Mono, mono Maths.exe6.6
Vanilla Mono mit LLVM und float3215.5

Bei der Untersuchung dieses Problems haben wir einige Probleme festgestellt, nach deren Korrektur die folgenden Ergebnisse erzielt wurden:

ArbeitsumgebungErgebnisse, MRay / Sek
Mono mit LLVM und float3215.5
Advanced Mono mit LLVM, float32 und fester Inline29.6

Das große Ganze:


Durch einfaches Anwenden von LLVM und float32 können Sie die Leistung von Gleitkomma-Code um fast das 2,3-fache steigern. Und nach dem Tuning, das wir als Ergebnis dieser Experimente zu Mono hinzugefügt haben, können Sie die Produktivität im Vergleich zum Standard-Mono um das 4,4-fache steigern - diese Parameter werden in zukünftigen Versionen von Mono zu den Standardparametern.

In diesem Artikel werde ich unsere Ergebnisse erläutern.

32-Bit- und 64-Bit-Float


Aras verwendet 32-Bit-Gleitkommazahlen für den Hauptteil der Berechnungen (geben Sie float in C # oder System.Single in .NET ein). In Mono haben wir vor langer Zeit einen Fehler gemacht: Alle 32-Bit-Gleitkommaberechnungen wurden als 64-Bit-Berechnungen durchgeführt, und die Daten wurden weiterhin in 32-Bit-Bereichen gespeichert.

Heute ist mein Gedächtnis nicht mehr so ​​scharf wie zuvor und ich kann mich nicht genau erinnern, warum wir eine solche Entscheidung getroffen haben.

Ich kann nur vermuten, dass es von Trends und Ideen dieser Zeit beeinflusst wurde.

Dann schwebte eine positive Aura mit erhöhter Genauigkeit um Float Computing. Beispielsweise verwendeten Intel x87-Prozessoren eine 80-Bit-Genauigkeit für Gleitkommaberechnungen, selbst wenn die Operanden doppelt waren, was den Benutzern genauere Ergebnisse lieferte.

Zu dieser Zeit war auch die Idee relevant, dass in einem meiner vorherigen Projekte - Gnumeric Spreadsheets - statistische Funktionen effizienter implementiert wurden als in Excel. Daher sind sich viele Gemeinden der Idee bewusst, dass genauere Ergebnisse mit höherer Genauigkeit verwendet werden können.

In der Anfangsphase der Mono-Entwicklung konnten die meisten mathematischen Operationen, die auf allen Plattformen ausgeführt wurden, am Eingang nur das Doppelte erhalten. 32-Bit-Versionen wurden zu C99, Posix und ISO hinzugefügt, aber damals waren sie nicht für die gesamte Branche verfügbar (z. B. sinf ist die Float-Version von sin , fabsf ist die Version von fabs usw.).

Kurz gesagt, die frühen 2000er Jahre waren eine Zeit des Optimismus.

Anwendungen zahlten einen hohen Preis für die Verlängerung der Rechenzeit, aber Mono wurde hauptsächlich für Desktop-Linux-Anwendungen verwendet, die HTTP-Seiten und einige Serverprozesse bedienen. Daher war die Gleitkomma-Geschwindigkeit nicht das Problem, auf das wir täglich stießen. Dies machte sich nur in einigen wissenschaftlichen Benchmarks bemerkbar, und 2003 wurden sie selten in .NET entwickelt.

Heutzutage haben Spiele, 3D-Anwendungen, Bildverarbeitung, VR, AR und maschinelles Lernen Gleitkommaoperationen zu einer häufigeren Art von Daten gemacht. Das Problem kommt nicht alleine und es gibt keine Ausnahmen. Float war nicht mehr der benutzerfreundliche Datentyp, der nur an wenigen Stellen im Code verwendet wurde. Sie verwandelten sich in eine Lawine, vor der man sich nirgendwo verstecken kann. Es gibt viele von ihnen und ihre Ausbreitung kann nicht gestoppt werden.

Arbeitsbereich-Flag float32


Aus diesem Grund haben wir vor einigen Jahren beschlossen, wie in allen anderen Fällen Unterstützung für die Ausführung von 32-Bit-Float-Operationen mit 32-Bit-Operationen hinzuzufügen. Wir haben diese Funktion des Arbeitsbereichs "float32" genannt. In Mono wird es durch Hinzufügen der Option --O=float32 in der Arbeitsumgebung --O=float32 , und in Xamarin-Anwendungen wird dieser Parameter in den Projekteinstellungen geändert.

Diese neue Flagge wurde von unseren mobilen Benutzern gut aufgenommen, da mobile Geräte im Grunde immer noch nicht zu leistungsfähig sind und es vorzuziehen ist, Daten schneller zu verarbeiten als eine höhere Genauigkeit. Wir empfehlen mobilen Benutzern, den LLVM-Optimierungscompiler und das float32-Flag gleichzeitig zu aktivieren.

Obwohl dieses Flag seit mehreren Jahren implementiert ist, haben wir es nicht zum Standard-Flag gemacht, um unangenehme Überraschungen für Benutzer zu vermeiden. Es kam jedoch zu Fällen, in denen aufgrund des 64-Bit-Standardverhaltens Überraschungen auftreten. Weitere Informationen finden Sie in diesem vom Unity-Benutzer eingereichten Fehlerbericht .

Jetzt verwenden wir float32 Mono float32 . Der Fortschritt kann hier verfolgt werden: https://github.com/mono/mono/issues/6985 .

In der Zwischenzeit kehrte ich zum Projekt meines Freundes Aras zurück. Er verwendete die neuen APIs, die zu .NET Core hinzugefügt wurden. Obwohl .NET Core immer 32-Bit-Float-Operationen als 32-Bit-Floats ausgeführt hat, führt die System.Math API dabei immer noch Konvertierungen von float zu double durch. Wenn Sie beispielsweise die Sinusfunktion für einen Float-Wert berechnen müssen, können Sie nur Math.Sin (double) aufrufen und müssen von float in double konvertieren.

Um dies zu beheben, wurde .NET Core ein neuer Typ von System.MathF hinzugefügt, der mathematische Operationen mit Gleitkommazahlen mit einfacher Genauigkeit enthält. Jetzt haben wir dieses [System.MathF] nach Mono [System.MathF] .

Der Übergang vom 64-Bit- zum 32-Bit-Float verbessert die Leistung erheblich. Dies geht aus dieser Tabelle hervor:

Arbeitsumgebung und OptionenMrays / Sekunde
Mono mit System.Math6.6
Mono mit System.Math und -O=float328.1
Mono mit System.MathF6.5
Mono mit System.MathF und -O=float328.2

Das heißt, die Verwendung von float32 in diesem Test verbessert die Leistung erheblich, und MathF hat nur geringe Auswirkungen.

LLVM-Setup


Bei dieser Untersuchung haben wir festgestellt, dass der Fast JIT Mono-Compiler zwar float32 Unterstützung bietet, diese Unterstützung jedoch nicht zum LLVM-Backend float32 hat. Dies bedeutete, dass Mono mit LLVM immer noch kostspielige Konvertierungen von Float zu Double durchführte.

Daher fügte Zoltan der LLVM-Codegenerierungs-Engine float32 Unterstützung hinzu.

Dann bemerkte er, dass unser Inliner für Fast JIT die gleichen Heuristiken verwendet wie für LLVM. Bei der Arbeit mit Fast JIT muss ein Gleichgewicht zwischen JIT-Geschwindigkeit und Ausführungsgeschwindigkeit hergestellt werden. Daher haben wir die Menge an eingebettetem Code begrenzt, um den Arbeitsaufwand der JIT-Engine zu verringern.

Wenn Sie sich jedoch für die Verwendung von LLVM in Mono entscheiden, bemühen Sie sich so schnell wie möglich um den Code. Daher haben wir die Einstellungen entsprechend geändert. Heute kann dieser Parameter mithilfe der Umgebungsvariablen MONO_INLINELIMIT geändert werden. Tatsächlich muss er jedoch in die Standardwerte geschrieben werden.

Hier sind die Ergebnisse mit den geänderten LLVM-Einstellungen:

Arbeitsumgebung und OptionenMrays / Sekunden
Mono mit System.Math --llvm -O=float3216.0
Mono mit System.Math --llvm -O=float32 , konstante Heuristik29.1
Mono mit System.MathF --llvm -O=float32 , konstante Heuristik29.6

Nächste Schritte


Es waren nur geringe Anstrengungen erforderlich, um all diese Verbesserungen vorzunehmen. Diese Änderungen wurden von regelmäßigen Diskussionen bei Slack angeführt. Ich habe es sogar geschafft, eines Abends ein paar Stunden Zeit zu haben, um System.MathF nach Mono zu portieren.

Der Aras Ray Tracer Code ist zu einem idealen Studienfach geworden, da er autark war, eine echte Anwendung und kein synthetischer Benchmark. Wir möchten eine andere ähnliche Software finden, mit der der von uns generierte Binärcode untersucht werden kann, und sicherstellen, dass wir LLVM die besten Daten für die optimale Ausführung seiner Arbeit übergeben.

Wir erwägen auch, unser LLVM zu aktualisieren und die neu hinzugefügten Optimierungen zu verwenden.

Separate Notiz


Extra Präzision hat schöne Nebenwirkungen. Beim Lesen der Poolanforderungen der Godot-Engine habe ich beispielsweise festgestellt, dass derzeit aktiv diskutiert wird, ob die Genauigkeit von Gleitkommaoperationen zur Kompilierungszeit anpassbar gemacht werden soll ( https://github.com/godotengine/godot/pull/17134 ).

Ich fragte Juan, warum dies für jemanden notwendig sein könnte, weil ich glaubte, dass 32-Bit-Gleitkommaoperationen für Spiele völlig ausreichend sind.

Juan erklärte, dass Floats im Allgemeinen hervorragend funktionieren. Wenn Sie sich jedoch von der Mitte wegbewegen, beispielsweise 100 Kilometer von der Spielmitte entfernt, sammelt sich ein Berechnungsfehler an, der zu interessanten Grafikfehlern führen kann. Sie können verschiedene Strategien verwenden, um die Auswirkungen dieses Problems zu verringern. Eine davon ist die Arbeit mit erhöhter Genauigkeit, für die Sie für die Leistung bezahlen müssen.

Kurz nach unserem Gespräch sah ich in meinem Twitter-Feed einen Beitrag, der dieses Problem demonstrierte: http://pharr.org/matt/blog/2018/03/02/rendering-in-camera-space.html

Das Problem ist in den folgenden Bildern dargestellt. Hier sehen wir ein Sportwagenmodell aus dem Paket pbrt-v3-scene ** . Sowohl die Kamera als auch die Szene befinden sich in der Nähe des Ursprungs und alles sieht gut aus.


** (Autor von Yasutoshi Mori .)

Dann bewegen wir die Kamera und die Szene 200.000 Einheiten bei xx, yy und zz vom Ursprung. Es ist ersichtlich, dass das Modell der Maschine ziemlich fragmentiert ist; Dies ist ausschließlich auf die mangelnde Genauigkeit der Gleitkommazahlen zurückzuführen.


Wenn wir uns noch 5 × 5 × 5 Mal weiter bewegen, 1 Million Einheiten vom Ursprung entfernt, beginnt sich das Modell aufzulösen. Die Maschine verwandelt sich in eine extrem grobe Voxel-Annäherung an sich selbst, sowohl interessant als auch erschreckend. (Keanu stellte die Frage: Ist Minecraft so kubisch, nur weil alles sehr weit vom Ursprung entfernt ist?)


** (Ich entschuldige mich bei Yasutoshi Mori für das, was wir mit seinem schönen Modell gemacht haben.)

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


All Articles