Unitys Mono-Problem: Warum Ihr C#-Code langsamer läuft als erwartet
(marekfiser.com)- Die von Unity verwendete Mono-Laufzeit zeigt im Vergleich zu modernem .NET eine deutlich geringere Ausführungsgeschwindigkeit; beim selben C#-Code wurden Unterschiede von bis zu 15x beobachtet
- In realem Spielecode wurde 100 Sekunden für die Unity-Ausführung auf Mono-Basis gemessen, während derselbe Code unter .NET 38 Sekunden benötigte, was sich stark auf die Effizienz von Debugging und Tests auswirkt
- Selbst im Release-Modus bleibt der Unterschied bestehen: Mono benötigt 30 Sekunden, .NET 12 Sekunden, also mehr als 2,5x selbst in einer optimierten Umgebung
- Ursachen sind unter anderem ineffiziente JIT-Kompilierung und fehlgeschlagenes Inlining in Mono sowie übermäßige Speicherkopien, im Gegensatz zu den modernen CoreCLR-JIT-Optimierungen von .NET
- Sobald Unity die .NET-Modernisierung auf CoreCLR-Basis abschließt, sind sowohl in Spielen als auch im Editor große Leistungssteigerungen möglich; das dürfte auf eine Beseitigung einer versteckten Performance-Steuer für alle Unity-Projekte hinauslaufen
Hintergrund zur Nutzung von Mono in Unity
- Unity verwendet seit 2006 das Mono-Framework zur Ausführung von C#-Code
- Damals war Mono die einzige plattformübergreifende .NET-Implementierung und als Open Source von Unity anpassbar
- Ab 2014 stellte Microsoft .NET Core als Open Source bereit und veröffentlichte 2016 .NET Core 1.0
- Danach entwickelte sich das .NET-Ökosystem mit Roslyn-Compiler, neuem JIT und Performance-Verbesserungen rasant weiter
- 2018 erklärten Unity-Ingenieure, dass sie an einem CoreCLR-Port arbeiteten, mit erwarteten 2- bis 10-fachen Performance-Gewinnen gegenüber Mono
- Doch selbst Ende 2025 ist spielseitige Ausführung auf CoreCLR-Basis weiterhin nicht möglich
Performance-Lücke zwischen Mono und .NET
- Der Simulationscode eines Unity-Projekts wurde außerhalb von Unity direkt unter .NET ausgeführt und verglichen
- Unity/Mono-Umgebung: 100 Sekunden, .NET-Umgebung: 38 Sekunden (im Debug-Modus)
- Im Release-Modus bleibt der Unterschied mit 30 Sekunden für Mono und 12 Sekunden für .NET bestehen
- .NET zeigt zudem starke Multithreading-Optimierung, etwa bei der Generierung einer 4K×4K-Karte in unter 3 Sekunden
- Hauptursache ist die ineffiziente Codegenerierung von Mono; selbst in einfachen Schleifen treten 15-fache Geschwindigkeitsunterschiede auf
Assembly-Vergleich: Mono vs. .NET
- Vergleich der erzeugten x64-Assemblies mit identischem Testcode
- Der .NET-JIT verschiebt schleifeninvariante Ausdrücke aus der Schleife heraus (Hoisting) und arbeitet nur mit einem Minimum an Registeroperationen
- Mono wiederholt Speicherkopien mit Dutzenden von
mov-Befehlen und verliert durch ineffizientes Inlining zusätzlich an Leistung
- Laufzeit einer Schleife mit
int.MaxValueWiederholungen- .NET: 750ms, Mono: 11.500ms, Unity Editor (Debug): 67.000ms
- Mono wiederholt innerhalb der Schleife unnötige Speicherbewegungen und Vergleichsoperationen
Bedeutung der CoreCLR-Einführung
- CoreCLR bietet moderne Funktionen wie aktuellen JIT, Span<T>-API, SIMD-Optimierungen und Unterstützung für Hardware-Instruktionen
- Diese Funktionen eröffnen Potenzial für zusätzliche Performance-Gewinne von mehr als 2x
- Unitys Burst-Compiler erzeugt zwar auf LLVM-Basis nativen Code, bringt aber Einschränkungen bei C#-Features mit sich
- Der moderne JIT von CoreCLR kann Burst-ähnliche Performance liefern und hat dabei weniger Sprachbeschränkungen
- CoreCLR unterstützt AOT (Ahead-of-Time-Kompilierung), was schnellere Startzeiten und Unterstützung für Plattformen mit JIT-Einschränkungen (z. B. iOS) ermöglichen kann
- Unity erklärt jedoch weiterhin, an IL2CPP festhalten zu wollen
Fazit: Unity braucht eine Modernisierung von .NET
- Mono zeigt gegenüber modernem .NET eine 1,5- bis über 3-fach langsamere Ausführungsleistung und wirkt damit als versteckter Kostenfaktor für alle Unity-Projekte
- Erwartete Effekte bei einer CoreCLR-Einführung
- Bessere Laufzeitleistung, schnellere iterative Builds, verbesserte GC, Wegfall von Domain Reloads, größerer Anteil an Managed Code
- Die Roadmap von Unity 6.x enthält zwar .NET Modernization, diese ist aber erst nach 2026 geplant
- Wenn die CoreCLR-Unterstützung fertig ist, könnte sie sowohl Unity-Entwicklern als auch Spielern eine spürbare Leistungsrevolution bringen
- Aktuell bleiben die Grenzen von Mono ein Performance-Flaschenhals für das gesamte Unity-Ökosystem
13 Kommentare
Es ist schön, mal wieder einen Artikel über Unity zu sehen.
Ich habe ihn mit großem Interesse gelesen.
Ah … Offenbar basiert Mono immer noch auf dem Legacy-.NET-Framework …
Es ist zwar kein Spiel, aber ich migriere gerade eine etwa 100.000 Zeilen große Finanzanwendung auf Basis von .NET 4.8 + LINQ to SQL + WinForms nach .NET 10 + Entity Framework, und ich merke deutlich, dass sie viel schneller geworden ist. Rechenaufgaben, die früher 10 Sekunden dauerten, sind teilweise auf 3 Sekunden gesunken!
Falls dies erfolgreich eingeführt wird, dürfte sich die Optimierung der unzähligen Indie-Games vermutlich verbessern ...
Es wäre schön, wenn auch NuGet-Kompatibilität hinzugefügt würde (oder liegt das daran, dass ich mich mit Unity nicht gut auskenne?)
Es wird zwar nicht offiziell unterstützt, aber es gibt ein Open-Source-Projekt namens NuGetForUnity.
Theoretisch sollten auf .NET Standard 2.0 ausgerichtete NuGet-Pakete auch in einer Unity-Umgebung geladen und verwendet werden können … aber offenbar gibt es dabei doch einige Unannehmlichkeiten.
https://learn.microsoft.com/ko-kr/dotnet/…
Ein weiterer Grund, warum Mono unbedingt mit CoreCLR modernisiert werden muss, ist meiner Meinung nach, dass Unity weder besonders die Möglichkeiten noch den Willen haben dürfte, in Performance-Verbesserungen für Mono zu investieren. Ich denke, es ist richtig, die Altlasten aus der Zeit des .NET Frameworks so schnell wie möglich aufzuräumen. :-D
Und ich fände es gut, wenn auch berücksichtigt würde, dass ab .NET 10 das Problem, das sie früher mit IL2CPP lösen wollten, zwar in eine andere Richtung entwickelt, aber dennoch präzise adressiert wird (
Native AOT).Natürlich bleibt die Einschränkung, dass dabei kein zwischendurch bearbeitbarer C++-Code erzeugt wird, aber letztlich hat sich die Erzeugung nativer Binärdateien ohne Just-in-Time von .NET 8 ausgehend bis .NET 10 weiter deutlich ausgereift.
Deshalb halte ich es für keine gute Entscheidung von Unity, die Modernisierung hin zu CoreCLR weiter aufzuschieben. Oder vielleicht wäre sogar ein vollständiger Wechsel zu einer ganz anderen Sprache oder Grundlage die sinnvollere Option!
Dem stimme ich ebenfalls voll und ganz zu...
Stimmt schon, aber ich verstehe nicht so recht, warum man unbedingt die Editor-Performance vergleichen muss ... Man hätte zumindest einen Debug-Build zum Vergleich heranziehen können, oder? Oder auch nicht — hätte das die Aussagekraft am Ende noch weiter geschwächt? Andererseits wirken sowohl IL2CPP als auch Mono gleichermaßen wie veraltete Technologien.
Bei großen Projekten beeinträchtigt die Editor-Performance die Developer Experience erheblich, daher ist auch die Editor-Performance wichtig. Das Starten des Editors ist langsam, der Asset-Import ist langsam, und auch die Schleife aus Debugging/Tests ist langsam ...
Ah ... natürlich ist auch das wichtig. Als ich den Text zum ersten Mal gelesen habe, wirkte es auf mich so, als hätte der Autor eher über die grundlegendere Ausführungsgeschwindigkeit des Codes sprechen wollen. Dass Unity, wie du sagst, auch beim Editor langsam ist, der Import langsam ist und die gesamte Testschleife insgesamt langsam ist, stimmt natürlich ebenfalls ...
Hacker-News-Kommentare
Im Artikel gab es ein paar Stellen, die so wirkten, als hätte ihn jemand mit wenig Unity-Entwicklungserfahrung geschrieben.
Kurz gesagt: Unity-Entwickler freuen sich bei diesem Update weniger auf Performance-Gewinne als auf den Zugang zu modernen Sprachfeatures. Und es ist üblich, GC zur Laufzeit zu minimieren oder mit nicht verwaltetem Speicher und DOTS daran vorbeizuarbeiten.
IL2CPP ist letztlich nur ein Codegenerator geringer Qualität, der .NET-IL nach C++ übersetzt und sich auf optimierende Compiler verlässt.
Das sieht man auch in Unitys Blog über die internen Strukturen von IL2CPP.
Burst/HPC# folgt ebenfalls Trends wie ECS oder SoA, bleibt in der Performance aber hinter gut geschriebenem C++ oder CoreCLR-C# zurück.
Außerdem sind diese Technologien geschlossen und Unity-spezifisch, man kann sie außerhalb davon nicht nutzen. Unity vermarktet sie immer mit Benchmarks gegen das langsame Mono.
Am Ende blieb Unity nichts anderes übrig, als CoreCLR zu akzeptieren, und dann wird man feststellen, dass normales C# schneller ist als der bisherige komplexe Code.
Wir verwenden IL2CPP nicht, weil es mit Dingen wie dynamischem DLL-Laden zur Laufzeit, Reflection und Struct-Packing mit FieldOffset nicht kompatibel ist.
Modder können Funktionen per IL-Injection erweitern, was die Entwicklung am Ende beschleunigt.
Burst und HPC# bevorzuge ich wegen ihrer Komplexität und Einschränkungen nicht. Gerade der Performance-Unterschied zwischen Mono und .NET ist frustrierend.
Auch Editor-Profiling war nützlich, weil es Leistungsverbesserungen in ähnlichen Verhältnissen wie der echte Build zeigte. Statt des ungenauen Standard-Profilers von Unity nutze ich allerdings ein selbst gebautes Tracing-System.
GC bleibt weiterhin ein Problem. String-Verarbeitung und UI erzeugen in jedem Frame Garbage. Mit CoreCLR bekäme man bessere APIs und einen bewegenden GC, was Probleme mit Speicherfragmentierung reduzieren könnte.
Der Asset Store ist großartig, aber die Engine selbst wirkt unausgereift.
Das Mono-basierte Scripting ist strukturell kompliziert auf CoreCLR zu migrieren.
Wenn Unity den Core wirklich verbessern will, müsste es wie Blender 3.x den gesamten Editor neu designen.
Im Moment fühlt es sich wie eine UI von 1999 an.
Unzählige Plugins und Tools bleiben im Stadium „0.x-preview“ stehen und funktionieren 5 bis 10 Jahre später entweder nicht mehr oder gehen zwischen neueren Assets unter.
Deshalb nutze ich inzwischen nur noch Versionen ab 1.0. Sonst hängt man an verlassenen Plugins und muss am Ende doch alles erneut portieren.
Das ist schlecht für Unity, für Entwickler und für Nutzer.
Intern fehlt durch gescheiterte eigene Spieleentwicklung das Gespür für echte Spieleproduktion.
Es werden einfach angeforderte Features ergänzt, aber es gibt keine konsistente Vision.
Wenn Performance entscheidend ist, sollte man Vulkan direkt aufrufen, und wenn Portabilität wichtig ist, WebGPU verwenden.
Da die Implementierungen je nach Browser unterschiedlich sind, entsteht Overhead, aber das ließe sich lösen, wenn WebGPU auf OS-/Treiber-Ebene bereitgestellt würde.
Godot dagegen bietet einfache und sinnvolle Bausteine, mit denen man frei bauen kann, was man möchte.
Im Asset Store ist es ähnlich: Wegen Versionskompatibilitätsproblemen ist Wartung schwierig, und die meisten werden zu vernachlässigten Assets.
Wenn Unity nützliche Assets aufkauft, werden sie weder ordentlich integriert noch bleiben konkurrierende Assets bestehen.
Unreal Engine bietet solche Funktionen dagegen eher auf Engine-integriertem Niveau.
Es gibt auch keine Pläne von Unity, für IL2CPP einen besseren GC einzuführen.
Wenn ein CoreCLR-basierter Editor kommt, könnte der Editor sogar schneller sein als ein Build.
Zugehörige Diskussion: Unity CoreCLR und .NET-Modernisierung
Wenn inkrementeller GC gut funktioniert, sind auch Stotterprobleme nicht so groß.
Da C# an sich inzwischen sehr schnell geworden ist, muss Unity diese Umstellung unbedingt mit voller Kraft priorisieren.
Unser Team brauchte ein paar Monate, um von .NET Framework 4.7.2 auf .NET 6 zu wechseln, und spätere Upgrades auf LTS-Versionen waren dann in wenigen Stunden erledigt.
Es gibt ständig Verzögerungen, und Führungskräfte gehen.
Als Alternative empfehle ich die .NET-10-basierte Stride-Engine. Dort gibt es keinen Boundary-Overhead wie bei Unity.
Godot ist Open Source, aber die C#-Unterstützung ist instabil, und wenn Web-Builds nicht funktionieren, ist es für Game Jams ungeeignet.
Man braucht eine echte Sandbox-Lösung mit GPU-Unterstützung.
Prioritäten ändern sich ständig, Anforderungen werden angepasst, und dadurch wird Arbeit immer wiederholt.
Die Entwickler sind nach wie vor hervorragend, aber es fehlt an konsequenter Durchsetzung.
Solche groß angelegten Rewrites sind auch aus Sicht eines CEO eine Entscheidung mit hohem Risiko.
Das Ergebnis war eine deutliche Performance-Steigerung, und durch die geringere Engine-Abhängigkeit wurde auch die Wartung des Codes einfacher.
Indem wir Unity-Konzepte nur dort exponierten, wo es nötig war, und die Grenzen per Tests erzwangen, haben wir den Wert dieser logischen Trennung klar gespürt.
Mit Root-Zugriff und in Kombination mit Netzwerktopologien wie WireGuard oder Tailscale wäre das auch als tragbarer Server perfekt.
Mit dem neuen GC in .NET 10 würden auch Ruckler in Spielen fast verschwinden.
Derzeit streame ich Spiele mit Sunlight + Moonlight vom Haupt-PC und spiele sie auf dem Handy.
Dank des OLED-Displays mit hoher Bildwiederholrate hält sich auch der Batterieverbrauch in Grenzen.
Es ist zwar nicht das .NET SDK, aber die Mono-Laufzeit ist in die App eingebettet, sodass es sich praktisch ähnlich anfühlt.
Der Cross-Platform-Vorteil von Mono ist ohnehin verschwunden, daher verstehe ich nicht, warum man komplizierte Hacks wie IL2CPP weiter pflegt.
Über Jahre haben sich nicht standardisierte Änderungen angesammelt, und außer einem Großunternehmen könnte das kaum jemand neu optimieren.
Für ein Projekt dieser Größenordnung hätte ich gedacht, ein bis zwei Jahre müssten ausreichen.