Ich entwickle ein plattformübergreifendes Spiel, das mit einem Lockstep-Modell über ein Netzwerk spielt. Als kurze Übersicht bedeutet dies, dass nur Eingaben übermittelt werden und alle Spiellogik auf dem Computer jedes Kunden simuliert wird. Daher sind Konsistenz und Determinismus sehr wichtig.
Ich kompiliere die Windows-Version auf MinGW32, die GCC 4.8.1 verwendet, und auf Linux kompiliere ich mit GCC 4.8.2.
Was mir in letzter Zeit auffiel, war, dass, wenn meine Linux-Version mit meiner Windows-Version verbunden war, das Programm sofort auseinandergehen oder de-synchronisieren würde, obwohl der gleiche Code auf beiden Maschinen kompiliert wurde! Es stellte sich heraus, dass das Linux-Build über 64 Bit kompiliert wurde, während die Windows-Version 32 Bit aufwies.
Nachdem ich eine Linux 32 Bit Version kompiliert hatte, war ich dankbar, dass das Problem gelöst war. Jedoch brachte es mich dazu, über Gleitkommadeterminismus nachzudenken und zu forschen.
Das habe ich gesammelt:
Ein Programm wird im Allgemeinen konsistent sein, wenn es:
ist
- lief auf der gleichen Architektur
- wurde mit dem gleichen Compiler
kompiliert
Wenn ich also davon ausginge, einen PC-Markt anzusprechen, dass jeder einen x86-Prozessor hat, dann löst das die Anforderung eins. Die zweite Voraussetzung scheint jedoch etwas albern zu sein.
MinGW, GCC und Clang (Windows, Linux, Mac) sind alle verschiedene Compiler, die auf GCC basieren / damit kompatibel sind. Bedeutet das, dass es unmöglich ist, einen plattformübergreifenden Determinismus zu erreichen? oder ist es nur auf Visual C ++ vs GCC anwendbar?
Beeinflussen die Optimierungsflags -O1 oder -O2 diesen Determinismus? Wäre es sicherer, sie wegzulassen?
Am Ende habe ich drei Fragen zu stellen:
- 1) Ist plattformübergreifender Determinismus möglich, wenn MinGW, GCC und Clang für Compiler verwendet werden?
- 2) Welche Flags sollten über diese Compiler hinweg gesetzt werden, um die bestmögliche Konsistenz zwischen Betriebssystemen / CPUs zu gewährleisten?
- 3) Fließkomma-Genauigkeit ist für mich nicht so wichtig - wichtig ist, dass sie konsistent sind. Gibt es eine Methode, um Fließkommazahlen auf eine niedrigere Genauigkeit (wie 3-4 Dezimalstellen) zu reduzieren, um sicherzustellen, dass die kleinen Rundungsfehler über Systeme hinweg nicht mehr existieren? (Jede Implementierung, die ich bisher versucht habe zu schreiben, ist fehlgeschlagen)
Bearbeiten: Ich habe einige plattformübergreifende Experimente durchgeführt.
Unter Verwendung von Gleitpunkten für Geschwindigkeit und Position habe ich einen Linux-Intel-Laptop und einen Windows-AMD-Desktopcomputer für bis zu 15 Dezimalstellen der Gleitkommawerte synchronisiert. Beide Systeme sind jedoch x86_64. Der Test war jedoch einfach - es wurden nur Objekte über ein Netzwerk bewegt, um einen sichtbaren Fehler zu finden.
Wäre es sinnvoll anzunehmen, dass die gleichen Ergebnisse gelten würden, wenn ein x86-Computer eine Verbindung zu einem x86_64-Computer herstellen würde? (32 Bit gegenüber 64 Bit Betriebssystem)
Cross-Plattform- und Cross-Compiler-Konsistenz ist natürlich möglich. Mit genügend Wissen und Zeit ist alles möglich! Aber es könnte sehr schwer oder sehr zeitaufwendig oder in der Tat unpraktisch sein.
Hier sind die Probleme, die ich vorhersehen kann, in keiner bestimmten Reihenfolge:
Denken Sie daran, dass sogar ein extrem kleiner Fehler von plus / minus 1/10 ^ 15 explodieren kann, um signifikant zu werden (Sie multiplizieren diese Zahl mit dieser Fehlergrenze mit einer Milliarde, und jetzt haben Sie ein Plus- oder-minus 0,000001 Fehler, der signifikant sein kann.) Diese Fehler können sich im Laufe der Zeit über viele Frames hinweg ansammeln, bis Sie eine desynchronisierte Simulation haben. Oder sie können sich manifestieren, wenn Sie Werte vergleichen (auch wenn Sie naiv mit "Epsilonen" in Fließkomma-Vergleichen arbeiten, kann das nicht helfen, sondern nur die Manifestation verschieben oder verzögern.)
Das obige Problem ist nicht einzigartig für verteilte deterministische Simulationen (wie Ihre.) Die Berührung mit dem Thema " numerische Stabilität ", das ein schwieriges und oft vernachlässigtes Thema ist.
Unterschiedliche Compiler-Optimierungsschalter und verschiedene Gleitkomma-Verhaltensermittlungsschalter können dazu führen, dass der Compiler leicht unterschiedliche Sequenzen von CPU-Anweisungen für dieselben Anweisungen generiert. Offensichtlich müssen diese bei den Compilations die gleichen sein und die gleichen genauen Compiler verwenden, oder der generierte Code muss genau verglichen und verifiziert werden.
32-Bit- und 64-Bit-Programme (Anmerkung: Ich sage Programme und nicht CPUs) werden wahrscheinlich ein etwas anderes Fließkomma-Verhalten aufweisen. Standardmäßig können 32-Bit-Programme nicht auf etwas weiter fortgeschrittenes als den x87-Befehlssatz von der CPU (kein SSE, SSE2, AVX usw.) zurückgreifen, es sei denn, Sie geben dies in der Compilerbefehlszeile an Ihr Code.) Auf der anderen Seite wird ein 64-Bit-Programm garantiert auf einer CPU mit SSE2-Unterstützung laufen, so dass der Compiler diese Anweisungen standardmäßig verwendet (wieder, außer vom Benutzer überschrieben.) Während x87 und SSE2 Float-Datentypen und Operationen auf ihnen sind ähnlich, sie sind - AFAIK - nicht identisch. Dies führt zu Inkonsistenzen in der Simulation, wenn ein Programm einen Befehlssatz und ein anderes Programm einen anderen verwendet.
Der x87-Befehlssatz enthält ein "Steuerwort" -Register, das Flags enthält, die einige Aspekte von Gleitkommaoperationen steuern (z. B. genaues Rundungsverhalten usw.). Dies ist eine Laufzeit-Sache, und Ihr Programm kann das eine Reihe von Berechnungen, dann ändern Sie dieses Register und danach genau die gleichen Berechnungen und erhalten ein anderes Ergebnis. Offensichtlich muss dieses Register auf den verschiedenen Maschinen überprüft und gehandhabt werden und identisch gehalten werden. Es ist möglich, dass der Compiler (oder die Bibliotheken, die Sie in Ihrem Programm verwenden) einen Code generiert, der diese Flags zur Laufzeit inkonsistent über die Programme hinweg ändert.
Auch im Fall des x87-Befehls haben Intel und AMD die Dinge historisch etwas anders umgesetzt. Zum Beispiel könnte die CPU eines Anbieters intern einige Berechnungen durchführen, die mehr Bits verwenden (und daher zu einem genaueren Ergebnis gelangen) als die anderen, was bedeutet, dass, wenn Sie zufällig auf zwei verschiedenen CPUs (beide x86) von zwei verschiedenen Herstellern laufen Ergebnisse einfacher Berechnungen könnten nicht identisch sein. Ich weiß nicht, wie und unter welchen Umständen diese Berechnungen mit höherer Genauigkeit möglich sind und ob sie unter normalen Betriebsbedingungen stattfinden oder ob Sie speziell danach fragen müssen, aber ich weiß, dass diese Diskrepanzen existieren.
Zufallszahlen, die konsistent und deterministisch über Programme hinweg erzeugt werden, haben nichts mit Fließkomma-Konsistenz zu tun. Es ist wichtig und Quelle von vielen Fehlern, aber am Ende sind es nur ein paar mehr Bits des Zustands, die Sie synchronisiert halten müssen.
Und hier sind ein paar Techniken, die helfen könnten:
Einige Projekte verwenden " Festkomma " - Zahlen und Festkommaarithmetik, um Rundungsfehler und allgemeine Unvorhersagbarkeit von Gleitkommazahlen zu vermeiden. Weitere Informationen und externe Links finden Sie im Wikipedia-Artikel .
In einem meiner eigenen Projekte habe ich während der Entwicklung in allen Instanzen des Spiels den gesamten relevanten Status (einschließlich einer großen Anzahl von Gleitkommazahlen) gehasht und den Hash-Wert für jeden Frame über das Netzwerk gesendet um sicherzustellen, dass ein einzelnes Bit dieses Status auf verschiedenen Computern nicht unterschiedlich war. Das half auch beim Debugging, wo ich nicht darauf vertrauen musste, wann und wo Inkonsistenzen existierten (was mir sowieso nicht sagen würde, wo sie herkamen), sobald ein Teil des Spielzustands auf einer Maschine divergierte von den anderen und genau wissen, was es war (wenn die Hash-Prüfung fehlgeschlagen wäre, würde ich die Simulation stoppen und den gesamten Zustand vergleichen.) Diese Funktion wurde in dieser Codebase von Anfang an implementiert und wurde nur während des Entwicklungsprozesses verwendet, um beim Debuggen zu helfen (da es Leistungs- und Speicherkosten hatte).
Update (als Antwort auf den ersten Kommentar unten): Wie ich in Punkt 1 gesagt habe und andere in anderen Antworten gesagt haben, garantiert das nichts. Wenn Sie das tun, können Sie möglicherweise die Wahrscheinlichkeit und Häufigkeit einer auftretenden Inkonsistenz verringern, aber die Wahrscheinlichkeit wird nicht null. Wenn Sie nicht sorgfältig und systematisch analysieren, was in Ihrem Code vor sich geht und welche möglichen Fehlerquellen auftreten, können Sie immer noch auf Fehler stoßen, egal wie sehr Sie Ihre Zahlen "abrunden".
Wenn Sie beispielsweise zwei Zahlen haben (z. B. als Ergebnisse von zwei Berechnungen, die identische Ergebnisse liefern sollen), die 1,111999999 und 1,111500001 sind, und sie auf drei Dezimalstellen runden, werden sie zu 1,111 bzw. 1,112. Der Unterschied der ursprünglichen Zahlen war nur 2E-9, aber jetzt ist es 1E-3 geworden. In der Tat haben Sie Ihren Fehler 500'000 mal erhöht. Und sie sind immer noch nicht gleich mit der Rundung. Sie haben das Problem verschärft.
Wahr ist, das passiert nicht viel, und die Beispiele, die ich gab, sind zwei unglückliche Zahlen, um in diese Situation zu kommen, aber es ist immer noch möglich, sich mit diesen Arten von Zahlen zu finden. Und wenn Sie das tun, sind Sie in Schwierigkeiten. Die einzige sichere Lösung, auch wenn Sie Festkomma-Arithmetik oder was auch immer verwenden, ist eine gründliche und systematische mathematische Analyse aller Ihrer möglichen Problembereiche und beweisen, dass sie über alle Programme hinweg konsistent bleiben.
Kurz gesagt, für uns Normalsterbliche, müssen Sie einen wasserdichten Weg haben, um die Situation zu überwachen und genau zu finden, wann und wie die geringsten Unstimmigkeiten auftreten, um das Problem im Nachhinein lösen zu können (statt sich zu verlassen) auf deine Augen, um Probleme in der Spielanimation oder Objektbewegung oder physischem Verhalten zu sehen.)sin()
von einer Bibliothek oder von einem Compiler intrinsisch kommen und sich beim Runden unterscheiden. Sicher, das ist nur ein Bit, aber das ist schon nicht mehr synchron. Und dieser Ein-Bit-Fehler kann sich im Laufe der Zeit addieren, so dass selbst ein ungenauer Vergleich nicht ausreicht. Neben Ihren Bedenken bezüglich Determinismus habe ich noch eine Bemerkung: Wenn Sie Bedenken hinsichtlich der Berechnungskonsistenz in einem verteilten System haben, haben Sie möglicherweise ein Designproblem.
Sie könnten Ihre Anwendung als eine Reihe von Knoten betrachten, von denen jeder für seine eigenen Berechnungen verantwortlich ist. Wenn Informationen über einen anderen Knoten benötigt werden, sollte er von diesem Knoten an Sie gesendet werden.
1.) Im Prinzip ist Cross-Plattform, OS, Hardware-Kompatibilität möglich, aber in der Praxis ist es ein Schmerz.
Im Allgemeinen hängen Ihre Ergebnisse davon ab, welches Betriebssystem Sie verwenden, welchen Compiler und welche Hardware Sie verwenden. Ändern Sie eine davon und Ihre Ergebnisse könnten sich ändern. Sie müssen alle Änderungen testen. Ich benutze Qt Creator und qmake (cmake ist wahrscheinlich besser, aber qmake funktioniert für mich) und teste meinen Code in MSVC unter Windows, GCC unter Linux und MinGW-w64 unter Windows. Ich teste sowohl 32-Bit als auch 64-Bit. Dies muss geschehen, wenn sich der Code ändert.
2.) und 3.)
In Bezug auf Gleitkommazahl verwenden einige Compiler x87 anstelle von SSE im 32-Bit-Modus. Sehen Sie dies als ein Beispiel für die Konsequenzen, wenn das passiert. Warum beginnt ein Programm zum Programmieren von Zahlen viel langsamer, wenn es in NaNs aufgeteilt wird? Alle 64-Bit-Systeme haben SSE, also glaube ich, dass SSE / AVX in 64-Bit verwendet wird Im 32-Bit-Modus müssen Sie möglicherweise SSE mit etwas wie -mfpmath=sse and -msse2
erzwingen.
Aber wenn Sie eine kompatiblere Version von GCC unter Windows wollen, dann würde ich MingGW-w64 für 32-Bit (aka MinGW-w32) oder MinGW-w64 in 64bit verwenden. Das ist nicht dasselbe wie MinGW (aka mingw32). Die Projekte sind auseinander gegangen. MinGW ist abhängig von MSVCRT
(der MSVC C-Laufzeitbibliothek) und MinGW-w64 nicht. Das Qt-Projekt hat eine ziemlich gute Beschreibung von MinGW-w64 und Installation. Ссылка
Sie sollten auch einen CPU-Dispatcher in Erwägung ziehen cpu Dispatcher für Visual Studio für AVX und SSE .