Design: Dokumentieren Sie, warum NaN-Bits nicht vollständig deterministisch sind

Erstellt am 22. März 2016  ·  15Kommentare  ·  Quelle: WebAssembly/design

(Ich befürworte derzeit nicht; deshalb treffen wir eine informierte Entscheidung und sammeln Material für eine Begründung.)

Betrachtet man Nondeterminism.md , fallen die NaN-Bits-Teile auf. Natürlich können Threads um die Wette laufen, Ressourcen erschöpft sein und Funktionen hinzugefügt werden; das sind Konsequenzen des Gesamtdesigns. Aber NaN-Bits? Könnten VMs etwas cleverer sein und sich darum kümmern? Es gibt zwei Probleme:

Wenn eine Operation mehr als einen NaN-Operanden erhält, welche propagiert sie?

IEEE 754 lässt dies unspezifiziert, aber zumindest x86, ARM und Power wählen alle den "ersten" Operanden, und es ist eine vernünftige Wahl.

Das Festlegen der Auswahl auf Wasm-Ebene würde jedoch bedeuten, dass wasm-Implementierungen Gleitkomma-Additionen und -Multiplikationen nicht umwandeln können, was manchmal eine nützliche Optimierung auf Pre-VEX x86 ist, wo die Befehle eine ihrer Eingaben überlagern. Außerdem müsste jeder, der eine LLVM-basierte VM verwendet, ihr beibringen, dass Addieren und Multiplizieren auf wasm nicht kommutativ sind.

Man könnte vernünftigerweise argumentieren, dass dies keine Show-Stopper sind, daher ist dieses Problem theoretisch behebbar, wenn ein starker Wunsch besteht.

Wenn eine Operation eine NaN erzeugt und keine NaN-Operanden hat, was ist dann das Vorzeichenbit?

x86 verwendet 1, ARM verwendet 0.

Der einfachste Weg, dies zu beheben, besteht darin, nach jeder Gleitkommaoperation zu kanonisieren. Dies ist machbar, obwohl eine Schwierigkeit darin besteht, dass sowohl 0 als auch 1 mögliche gültige Werte sind, wenn ein NaN-Operand weitergegeben werden muss. Es würde also nicht ausreichen, nur auf ein NaN-Ergebnis zu prüfen und zu kanonisieren; man müsste auf ein NaN-Ergebnis und das Fehlen von NaN-Operanden prüfen und erst dann kanonisieren.

Das Kanonisieren nach _jeder_ Operation wäre sehr teuer; eine andere Möglichkeit besteht darin, nur an "Escape" -Punkten der Berechnungen zu kanonisieren (der kanonisierende Pfad müsste dann im Wesentlichen einen ganzen Berechnungsstrom wiedergeben, um die korrekte NaN-Ausgabe zu bestimmen). Dies ist eine Verbesserung, verursacht aber wahrscheinlich immer noch einen erheblichen Mehraufwand.

Eine andere mögliche Implementierung wäre, die ungültige Ausnahme zu demaskieren, jedes Mal, wenn eine NaN generiert wird, einen Trap zu nehmen und dann die Kanonisierung und Rückgabe durchzuführen. Zu den Nachteilen gehören die Verwendung eines nicht standardmäßigen CPU-Modus und die Tatsache, dass es sehr langsam ist, wenn viele Invaliden generiert werden.

Leider haben diese Ansätze erhebliche Nachteile. Wenn keine anderen Ideen auftauchen oder ein sehr starker Wunsch besteht, scheint dies schwer zu beheben.


Eine andere Sache, die zu beachten ist, ist, dass NaN-Bits schwer versehentlich zu beobachten sind, daher ist dies im Allgemeinen kein großes Problem bei der Portabilität.

Hat noch jemand Gedanken hinzuzufügen?

clarification floating point

Hilfreichster Kommentar

Ich möchte die Auswirkungen auf die Leistung quantifizieren, bevor ich eine Entscheidung treffe. Ich denke, unsere Toolchains sind im Moment noch zu unausgereift, um gute Performance-Messungen zu machen (hier stehen längere Pole im Weg). Anders ausgedrückt: Ich möchte den „Tod durch tausend Schnitte“ vermeiden.

Alle 15 Kommentare

Zu beachten ist auch, dass NaN-Bits schwer versehentlich zu beobachten sind

Gibt es eine Möglichkeit, die Beobachtung oder zumindest das Vorzeichenbit unmöglich zu machen?

IMO sollten die NaN-Nichtdeterminismen in Ruhe gelassen werden, da sich Software so selten um sie kümmert. Was ist der Fall, wenn Leistung geopfert wird, um sie deterministisch zu machen?

Hier ist eine Liste der Möglichkeiten, wie man NaN-Bits beobachten kann:

  • reinterpret Konvertierung
  • store in den linearen Speicher und laden die Bits mit einer anderen Interpretation (oder lassen Sie die Bits extern beobachten)
  • ein Argument an ein call einer importierten Funktion übergeben oder einen Wert von einer exportierten Funktion zurückgeben
  • copysign das Vorzeichenbit auf ein Nicht-NaN

VMs könnten vor jedem dieser Codes kanonisierenden Code einfügen. das ist die oben diskutierte Idee "an Fluchtpunkten kanonisieren". store ist auf heißen Pfaden sehr verbreitet, daher wäre dies wahrscheinlich immer noch ziemlich teuer.

@qwertie Ich hier besprochen

Zu den Vorteilen von

store ist auf heißen Pfaden sehr verbreitet, daher wäre dies wahrscheinlich immer noch ziemlich teuer.

Könnten wir für diesen Fall einfach immer den Vorzeichenwert angeben? Also einfach 1 machen, wenn es in Mem platziert wird?

@wanderer Das ist im Wesentlichen die Sie in diesem

Ich sollte auch hinzufügen, dass ich keine der in dieser Ausgabe erwähnten Optionen einem Benchmarking unterzogen habe; jedes Benchmarking, das jeder hier hinzufügen kann, wäre willkommen.

Ich würde mich stark zum aktuellen Niveau des Nicht-Determinismus neigen, der die Leistung begünstigt.
Ich war eigentlich etwas überrascht, dass wasm die Mantissen-Bits des NaN rigoros definiert.
Dies mag zwar für heutige Prozessoren ausreichen, aber wer weiß, welche zukünftigen Prozessoren kommen werden.

Wenn das wasm aus einer Hochsprache generiert wird, könnte dieser Übersetzer eine Option zur Verfügung stellen, um die Ebene der FP-Semantik zu steuern, ähnlich wie zum Beispiel -ffast-math. Der Übersetzer könnte dann jedes zusätzliche Wasm-Fixup einfügen, das benötigt wird, um Nans in ein gewünschtes Format zu zwingen. Zu diesem Zweck könnten wir isNan- oder normalizeNan-Operatoren bereitstellen, obwohl ich das jetzt nicht befürworte.

Kurz gesagt, das "Reparieren" wird am besten einem übergeordneten Tool überlassen, IMO.

+1 @mbodart. Bearbeiten : oh schau, es gibt jetzt tatsächlich ein +1-Ding :). Übrigens, ich persönlich denke, wasm = Weltherrschaft und daher werden zukünftige Prozessoren es nicht antagonisieren.

Ich möchte die Auswirkungen auf die Leistung quantifizieren, bevor ich eine Entscheidung treffe. Ich denke, unsere Toolchains sind im Moment noch zu unausgereift, um gute Performance-Messungen zu machen (hier stehen längere Pole im Weg). Anders ausgedrückt: Ich möchte den „Tod durch tausend Schnitte“ vermeiden.

Ich stimme @jfbastien zu. Performance-Effekte müssen zuerst quantifiziert werden, bevor an der Semantik herumgebastelt wird.

Um es klar zu sagen, ich plädiere derzeit nicht für eine Änderung hier; Ich sammle Begründungsmaterial. Der Nichtdeterminismus des NaN-Bits ragt heraus und möchte eine Erklärung. Und soweit mir bekannt ist, hat auch niemand die Leistungseffekte quantifiziert, wenn diese Bits nicht deterministisch gemacht werden.

ARMv8 propagiert nicht immer den ersten Operanden, wenn beide Operanden NaN sind. Die Regel lautet:

  • Wenn ein Operand ein ruhiges NaN ist und der andere ein signalisierendes NaN ist, propagiere das signalisierende NaN.
  • Andernfalls den ersten Operanden weitergeben.

Während ich die Spezifikation lese, gilt dies sowohl für den Aarch32- als auch für den Aarch64-Modus von ARMv8.

Dieses Verhalten unterscheidet sich von SSE, das in beiden Fällen den ersten Operanden propagiert.

Beide Architekturen wandeln eine sNaN in eine qNaN um, indem sie das Quiet-Bit setzen, bevor sie es weitergeben.

@stoklund Guter Ort! Ich habe übersehen, dass ARM das erste NaN nicht auswählt, wenn das zweite NaN signalisiert. Das würde die Strategie, die ich oben für den mehrfachen NaN-Fall dargelegt habe, verkomplizieren, also können wir dies in der Begründung erwähnen.

Ich habe jetzt #973 erstellt, um einen spezifischen Text vorzuschlagen, der das Obige zusammenfasst.

War diese Seite hilfreich?
0 / 5 - 0 Bewertungen

Verwandte Themen

artem-v-shamsutdinov picture artem-v-shamsutdinov  ·  6Kommentare

aaabbbcccddd00001111 picture aaabbbcccddd00001111  ·  3Kommentare

void4 picture void4  ·  5Kommentare

dpw picture dpw  ·  3Kommentare

cretz picture cretz  ·  5Kommentare