Rust: Gleitkomma-Ganzzahl-Casts können undefiniertes Verhalten verursachen

Erstellt am 31. Okt. 2013  ·  234Kommentare  ·  Quelle: rust-lang/rust

Stand Stand 18.04.2020

Wir beabsichtigen, das Sättigungs-Float-Casts-Verhalten für as stabilisieren und haben unsichere Bibliotheksfunktionen stabilisiert, die das vorherige Verhalten behandeln. Siehe # 71269 für die neueste Diskussion zu diesem Stabilisierungsprozess.

Stand Stand 05.11.2018

Im Compiler wurde ein Flag implementiert, -Zsaturating-float-casts , das bewirkt, dass alle Float-Integer-Casts ein "sättigendes" Verhalten aufweisen, bei dem es außerhalb der Grenzen an die nächste Grenze geklemmt wird. Ein Aufruf zum Benchmarking dieser Änderung wurde vor einiger Zeit veröffentlicht. Obwohl die Ergebnisse in vielen Projekten positiv sind, sind sie für einige Projekte ziemlich negativ und zeigen, dass wir hier noch nicht fertig sind.

In den nächsten Schritten wird herausgefunden, wie die Leistung für diese Fälle wiederhergestellt werden kann:

  • Eine Möglichkeit besteht darin, das heutige Cast-Verhalten von as (in einigen Fällen UB) zu übernehmen und unsafe -Funktionen für die relevanten Typen und dergleichen hinzuzufügen.
  • Eine andere Möglichkeit besteht darin, darauf zu warten, dass LLVM ein freeze -Konzept hinzufügt, was bedeutet, dass wir ein Müllbitmuster erhalten, aber es ist zumindest nicht UB
  • Eine andere Möglichkeit besteht darin, Casts über Inline-Assembly in LLVM IR zu implementieren, da der aktuelle Codegen nicht stark optimiert ist.

Alter Status

UPDATE (von @nikomatsakis): Nach vielen Diskussionen haben wir die Grundlagen eines Plans zur Lösung dieses Problems. Wir brauchen jedoch Hilfe bei der Untersuchung der Auswirkungen auf die Leistung und der Erarbeitung der endgültigen Details!


ORIGINAL AUSGABE FOLGT:

Wenn der Wert nicht in ty2 passt, sind die Ergebnisse undefiniert.

1.04E+17 as u8
A-LLVM C-bug I-unsound 💥 P-medium T-lang

Hilfreichster Kommentar

Ich habe einige Arbeiten zur Implementierung von Intrinsics für die Sättigung von Float-Int-Casts in LLVM begonnen: https://reviews.llvm.org/D54749

Wenn dies irgendwohin führt, bietet es eine relativ kostengünstige Möglichkeit, die Sättigungssemantik zu erhalten.

Alle 234 Kommentare

Nominierung

akzeptiert für P-hoch, gleiche Argumentation wie # 10183

Ich denke nicht, dass dies auf Sprachebene rückwärts inkompatibel ist. Code, der in Ordnung war, funktioniert nicht mehr. Nominierung.

Wechsel zu P-hoch, gleiche Argumentation wie # 10183

Wie schlagen wir vor, dies und # 10185 zu lösen? Da das Definieren des Verhaltens vom dynamischen Wert der übertragenen Zahl abhängt, scheint die einzige Lösung darin zu bestehen, dynamische Prüfungen einzufügen. Wir scheinen uns einig zu sein, dass wir dies nicht für einen arithmetischen Überlauf tun möchten. Sind wir glücklich, dies für einen gegossenen Überlauf zu tun?

Wir könnten LLVM eine Eigenart hinzufügen, die eine "sichere Konvertierung" durchführt. @zwarich kann andere Ideen haben.

AFAIK Die einzige Lösung im Moment ist die Verwendung der zielspezifischen Eigenschaften. Das macht JavaScriptCore, zumindest laut jemandem, den ich gefragt habe.

Oh, das ist dann einfach genug.

ping @pnkfelix Wird dies durch die neuen Überlaufprüfungen abgedeckt?

Diese Casts werden von rustc nicht mit Debug-Assertions überprüft.

Ich bin gerne damit fertig, aber ich brauche eine konkrete Lösung. Ich persönlich denke, dass dies zusammen mit einer überlaufenden Ganzzahlarithmetik überprüft werden sollte, da es sich um ein sehr ähnliches Problem handelt. Es macht mir aber nichts aus, was wir tun.

Beachten Sie, dass dieses Problem derzeit einen ICE verursacht, wenn es in bestimmten konstanten Ausdrücken verwendet wird.

Dies ermöglicht die Verletzung der Speichersicherheit bei sicherem Rost, Beispiel aus diesem Forumsbeitrag :

Undefs, was? Undefs machen Spaß. Sie neigen dazu, sich zu verbreiten. Nach ein paar Minuten des Streits ..

#[inline(never)]
pub fn f(ary: &[u8; 5]) -> &[u8] {
    let idx = 1e100f64 as usize;
    &ary[idx..]
}

fn main() {
    println!("{}", f(&[1; 5])[0xdeadbeef]);
}

Segfaults auf meinem System (spätestens abends) mit -O.

Markierung mit I-unsound angesichts der Verletzung der Speichersicherheit bei sicherem Rost.

@bluss , das segfualt für mich nicht, gibt nur einen Assertionsfehler. Untagging, da ich derjenige war, der es hinzugefügt hat

Seufz, ich habe das -O vergessen und neu markiert.

Nominierung für P-High. Anscheinend war dies irgendwann P-hoch, wurde aber mit der Zeit niedriger. Dies scheint für die Richtigkeit ziemlich wichtig zu sein.

BEARBEITEN: reagierte nicht auf Triage-Kommentar und fügte das Etikett manuell hinzu.

Es scheint, als ob der Präzedenzfall für das Überlaufmaterial (z. B. zum Verschieben) darin besteht, sich nur auf ein bestimmtes Verhalten zu einigen. Java scheint das Ergebnis modulo des Bereichs zu erzeugen, was nicht unangemessen erscheint; Ich bin mir nicht sicher, welche Art von LLVM-Code wir dafür benötigen würden.

Laut https://docs.oracle.com/javase/specs/jls/se7/html/jls-5.html#jls -5.1.3 garantiert Java auch, dass NaN Werte 0 und Unendlichkeiten zugeordnet werden auf die minimal / maximal darstellbare ganze Zahl. Darüber hinaus ist die Java-Regel für die Konvertierung komplexer als nur das Umbrechen. Sie kann eine Kombination aus Sättigung (für die Konvertierung in int oder long ) und Umbruch (für die Konvertierung in kleinere Integraltypen) sein , wenn benötigt). Das Replizieren des gesamten Konvertierungsalgorithmus von Java ist sicherlich möglich, würde jedoch für jede Besetzung eine angemessene Anzahl von Operationen erfordern. Insbesondere um sicherzustellen, dass das Ergebnis einer fpto[us]i -Operation in LLVM kein undefiniertes Verhalten aufweist, wäre eine Bereichsprüfung erforderlich.

Als Alternative würde ich vorschlagen, dass float-> int-Casts garantiert nur dann gültig sind, wenn die Kürzung des ursprünglichen Werts als Wert des Zieltyps (oder vielleicht als [iu]size ?) Und bis dargestellt werden kann Aussagen zu Debug-Builds haben, die eine Panik auslösen, wenn der Wert nicht korrekt dargestellt wurde.

Die Hauptvorteile des Java-Ansatzes bestehen darin, dass die Konvertierungsfunktion vollständig ist. Dies bedeutet jedoch auch, dass sich unerwartetes Verhalten einschleichen könnte: Es würde undefiniertes Verhalten verhindern, aber es wäre leicht zu täuschen, nicht zu prüfen, ob die Besetzung tatsächlich Sinn ergibt (Dies gilt leider auch für die anderen Darsteller: besorgt :).

Der andere Ansatz entspricht dem derzeit für arithmetische Operationen verwendeten: einfache und effiziente Implementierung in der Version, Panik, die durch die Bereichsprüfung beim Debuggen ausgelöst wird. Leider würde dies im Gegensatz zu anderen as Casts eine solche Konvertierung überprüfen lassen, was für den Benutzer überraschend sein kann (obwohl hier möglicherweise die Analogie zu arithmetischen Operationen hilfreich sein kann). Dies würde auch einen Teil des Codes beschädigen, aber AFAICT sollte nur für Code auftreten, der derzeit auf undefiniertem Verhalten beruht (dh es würde das undefinierte Verhalten "Lassen Sie uns eine ganze Zahl zurückgeben, es ist Ihnen offensichtlich egal, welcher" mit einer Panik ersetzen).

Das Problem ist nicht "Lass uns eine ganze Zahl zurückgeben, es ist dir offensichtlich egal, welche", sondern dass es ein Undef verursacht, das kein zufälliger Wert ist, sondern ein nasaler Dämonenwert, und LLVM darf annehmen, dass das Undef niemals auftritt Optimierungen ermöglichen, die schreckliche Fehler machen. Wenn es ein zufälliger Wert wäre, aber entscheidend nicht undef, dann würde dies ausreichen, um die Soliditätsprobleme zu beheben. Wir müssen nicht definieren, wie nicht darstellbare Werte dargestellt werden, wir müssen nur undef verhindern.

Besprochen im @ rust-lang / compiler-Meeting. Die konsequenteste Vorgehensweise bleibt:

  1. Wenn Überlaufprüfungen aktiviert sind, suchen Sie nach illegalen Würfen und Panik.
  2. Andernfalls benötigen wir ein Fallback-Verhalten. Es sollte etwas sein, das minimale (idealerweise null) Laufzeitkosten für gültige Werte hat, aber das genaue Verhalten ist nicht so wichtig, solange es nicht LLVM undef ist.

Das Hauptproblem ist, dass wir einen konkreten Vorschlag für Option 2 benötigen.

Triage: P-Medium

@nikomatsakis Ist as jemals in Panik bei Debug-Builds? Wenn dies nicht der Fall ist, erscheint es aus Gründen der Konsistenz und Vorhersehbarkeit vorzuziehen, dies so zu halten. (Ich denke, es sollte haben, genau wie Arithmetik, aber das ist eine separate und vergangene Debatte.)

Andernfalls benötigen wir ein Fallback-Verhalten. Es sollte etwas sein, das minimale (idealerweise null) Laufzeitkosten für gültige Werte hat, aber das genaue Verhalten ist nicht so wichtig, solange es nicht LLVM undef ist.

Konkreter Vorschlag: Ziffern und Exponenten als u64 und Bitshift-Ziffern nach Exponenten extrahieren.

fn f64_as_u64(f: f64) -> u64 {
    let (mantissa, exponent, _sign) = f.integer_decode();
    mantissa >> ((-exponent) & 63)
}

Ja, es sind keine Nullkosten , aber es ist etwas optimierbar (wäre besser, wenn wir inline markieren) und zumindest deterministisch. Ein zukünftiger MIR-Pass, der einen float-> int-Cast erweitert, könnte wahrscheinlich analysieren, ob der Float garantiert in Ordnung ist, um diese schwere Konvertierung zu werfen und zu überspringen.

Verfügt LLVM nicht über Plattform-Eigenschaften für die Konvertierungsfunktionen?

EDIT : @zwarich sagte (vor langer Zeit):

AFAIK Die einzige Lösung im Moment ist die Verwendung der zielspezifischen Eigenschaften. Das macht JavaScriptCore, zumindest laut jemandem, den ich gefragt habe.

Warum überhaupt in Panik geraten? AFAIK, @glaebhoerl ist korrekt, as soll abschneiden / erweitern, _nicht_ die Operanden überprüfen.

Am Samstag, den 05. März 2016 um 03:47:55 Uhr -0800 schrieb Gábor Lehel:

@nikomatsakis Ist as jemals in Panik bei Debug-Builds? Wenn dies nicht der Fall ist, erscheint es aus Gründen der Konsistenz und Vorhersehbarkeit vorzuziehen, dies so zu halten. (Ich denke, es sollte haben, genau wie Arithmetik, aber das ist eine separate und vergangene Debatte.)

Wahr. Ich finde das überzeugend.

Am Mittwoch, den 09. März 2016 um 02:31:05 Uhr -0800 schrieb Eduard-Mihai Burtescu:

Verfügt LLVM nicht über Plattform-Eigenschaften für die Konvertierungsfunktionen?

EDIT :

AFAIK Die einzige Lösung im Moment ist die Verwendung der zielspezifischen Eigenschaften. Das macht JavaScriptCore, zumindest laut jemandem, den ich gefragt habe.

Warum überhaupt in Panik geraten? AFAIK, @glaebhoerl ist korrekt, as soll abschneiden / erweitern, _nicht_ die Operanden überprüfen.

Ja, ich glaube ich habe mich vorher geirrt. as ist die "ungeprüfte Kürzung"
Betreiber, zum Guten oder Schlechten, und es scheint am besten, konsequent zu bleiben
mit dieser Philosophie. Die Verwendung zielspezifischer Intrinsics kann perfekt sein
feine Lösung?

@ Nikomatsakis : Es scheint, dass das Verhalten noch nicht definiert wurde? Können Sie diesbezüglich ein Update zur Planung geben?

Bin gerade mit viel kleineren Zahlen darauf gestoßen

    let x: f64 = -1.0;
    x as u8

Ergebnisse in 0, 16 usw. Abhängig von den Optimierungen hatte ich gehofft, dass es als 255 definiert wird, damit ich nicht x as i16 as u8 schreiben muss.

@gmorenz Hast du !0u8 ausprobiert?

In einem Kontext, der keinen Sinn ergeben würde, erhielt ich den f64 aus einer Transformation von Daten, die über das Netzwerk gesendet wurden, mit einem Bereich von [-255, 255]. Ich hatte gehofft, dass es sich gut einwickeln würde (genau so, wie <i32> as u8 einwickelt).

Hier ist ein aktueller LLVM-Vorschlag, "undef zu töten" http://lists.llvm.org/pipermail/llvm-dev/2016-October/106182.html , obwohl ich kaum genug weiß, um zu wissen, ob sich dies automatisch lösen lässt oder nicht dieses Problem.

Sie ersetzen undef durch Gift, wobei die Semantik etwas anders ist. Int -> float wirft kein definiertes Verhalten.

Wir sollten wahrscheinlich eine explizite Weise bieten eine sättigende Besetzung zu tun? Ich wollte gerade genau dieses Verhalten.

Anscheinend sollte dies unter https://github.com/rust-lang/rust/issues/10184#issuecomment -139858153 als I-Absturz markiert werden.

Wir hatten heute eine Frage dazu in #rust-beginners , jemand ist in freier Wildbahn darauf gestoßen.

Das Buch, das ich mit @jimblandy schreibe, _Programming Rust_, erwähnt diesen Fehler.

Es sind verschiedene Arten von Abgüssen zulässig.

  • Zahlen können von jedem der eingebauten numerischen Typen in einen anderen umgewandelt werden.

    (...)

    Zum jetzigen Zeitpunkt kann das Umwandeln eines großen Gleitkommawertes in einen ganzzahligen Typ, der zu klein ist, um ihn darzustellen, zu undefiniertem Verhalten führen. Dies kann auch in sicherem Rost zu Abstürzen führen. Es ist ein Fehler im Compiler, github.com/rust-lang/rust/issues/10184 .

Unsere Frist für dieses Kapitel ist der 19. Mai. Ich würde diesen letzten Absatz gerne löschen, aber ich denke, wir sollten hier zuerst einen Plan haben.

Anscheinend verwendet der aktuelle JavaScriptCore einen interessanten Hack auf x86. Sie verwenden die Anweisung CVTTSD2SI und greifen dann auf haariges C ++ zurück, wenn der Wert außerhalb des Bereichs liegt. Da Werte außerhalb des Bereichs derzeit explodieren, wäre die Verwendung dieser Anweisung (ohne Fallback!) Eine Verbesserung gegenüber dem, was wir jetzt haben, wenn auch nur für eine Architektur.

Ehrlich gesagt denke ich, wir sollten numerische Casts mit as ablehnen und stattdessen From und TryFrom oder so etwas wie die Conv-Kiste verwenden.

Vielleicht ja, aber das scheint mir orthogonal zu sein.

OK, ich habe gerade das ganze Gespräch noch einmal gelesen. Ich denke, es besteht Einigkeit darüber, dass diese Operation nicht in Panik geraten sollte (aus Gründen der allgemeinen Übereinstimmung mit as ). Es gibt zwei Hauptkandidaten für das Verhalten:

  • Eine Art definiertes Ergebnis
  • Ein undefinierter Wert (nicht undefiniertes Verhalten)

    • Pro: Auf diese Weise können wir nur die plattformspezifischen Eigenschaften verwenden, die auf jeder Plattform verfügbar sind.

    • Con: Es ist eine Portabilitätsgefahr. Im Allgemeinen habe ich das Gefühl, dass wir nicht sehr oft undefinierte Ergebnisse verwendet haben, zumindest in der Sprache (ich bin sicher, dass wir dies in den Bibliotheken an verschiedenen Stellen tun).

Mir ist nicht klar, ob es einen klaren Präzedenzfall dafür gibt, wie das Ergebnis im ersten Fall aussehen sollte?

Nachdem ich das ausgeschrieben habe, würde ich es vorziehen, ein deterministisches Ergebnis beizubehalten. Ich denke, jeder Ort, an dem wir die Linie des Determinismus halten können, ist ein Gewinn. Ich bin mir allerdings nicht sicher, was das Ergebnis sein soll.

Ich mag Sättigung, weil ich sie verstehen kann und sie nützlich erscheint, aber sie scheint irgendwie nicht mit der Art und Weise übereinzustimmen, wie u64 as u32 Kürzungen vornimmt. Vielleicht macht also eine Art Ergebnis, das auf Kürzung basiert, Sinn, was was @ oli-obk vorgeschlagen hat - ich verstehe nicht ganz, was dieser Code tun soll. =)

Mein Code gibt den korrekten Wert für Dinge im Bereich 0..2 ^ 64 und deterministische, aber falsche Werte für alles andere an.

Floats werden durch einen Mantisse-Exponenten dargestellt, z. B. 1.0 ist (2 << 52) ^ -52 und da Bitverschiebungen und Exponenten in Binärform dasselbe sind, können wir die Verschiebung einfach umkehren (also die Negation des Exponenten und der Rechten Verschiebung).

+1 für Determinismus.

Ich sehe zwei Semantiken, die für den Menschen sinnvoll sind, und ich denke, wir sollten diejenige auswählen, die für Werte im Bereich schneller ist

  • Sättigung (Werte außerhalb des Bereichs werden zu IntType::max_value() / min_value() )
  • Modulo (Werte außerhalb des Bereichs werden so behandelt, als würden sie zuerst in einen Bigint konvertiert und dann abgeschnitten)

In der folgenden Tabelle sollen beide Optionen vollständig angegeben werden. T ist ein beliebiger Maschinentyp. Tmin und Tmax sind T::min_value() und T::max_value() . RTZ (v) bedeutet, den mathematischen Wert von v und Round Toward Zero zu nehmen, um eine mathematische Ganzzahl zu erhalten.

v | v as T (Sättigung) | v as T (Modulo)
---- | ---- | ----
im Bereich (Tmin <= v <= Tmax) | RTZ (v) | RTZ (v)
negative Null | 0 | 0
NaN | 0 | 0
Unendlichkeit | Tmax | 0
-Infinity | Tmin | 0
v> Tmax | Tmax | RTZ (v) abgeschnitten, um zu T zu passen
v <Tmin | Tmin | RTZ (v) abgeschnitten, um zu T zu passen

Der ECMAScript-Standard spezifiziert die Operationen ToInt32 , ToUint32, ToInt16, ToUint16, ToInt8, ToUint8, und meine Absicht mit der obigen Option "Modulo" ist es, diese Operationen in jedem Fall abzugleichen.

ECMAScript spezifiziert auch ToInt8Clamp, das in keinem der oben genannten Fälle übereinstimmt: Es rundet "auf nicht auf "auf Null runden".

Der Vorschlag von @ oli-obk ist ein dritter Weg, der in Betracht gezogen werden sollte, wenn die Berechnung für Werte im Bereich schneller ist.

@ oli-obk Was ist mit vorzeichenbehafteten Ganzzahltypen?

Einen weiteren Vorschlag in die Mischung einfließen lassen: Markieren Sie u128-Casts als unsicher und zwingen Sie die Leute, explizit eine Art der Handhabung zu wählen. u128 ist derzeit ziemlich selten.

@Manishearth Ich würde auf ähnliche Semantik-Ganzzahlen → Floats als Floats → Ganzzahlen hoffen. Da beide UB-voll sind und wir float → integer nicht mehr unsicher machen können, sollten wir wahrscheinlich vermeiden, auch integer → float unsicher zu machen.

Für Float → Integer-Sättigung ist AFAICT schneller (was zu einer Folge von and , Test + Sprung Float-Vergleich und Sprung, alles für 0,66 oder 0,5 2-3 Zyklen auf modernen Bögen). Es könnte mich persönlich nicht weniger interessieren, für welches genaue Verhalten wir uns entscheiden, solange die Werte im Bereich so schnell sind, wie sie nur sein könnten.

Wäre es nicht sinnvoll, wenn es sich wie ein Überlauf verhält? In einem Debug-Build würde es also in Panik geraten, wenn Sie eine Besetzung mit undefiniertem Verhalten durchführen. Dann könnten Sie Methoden zum Festlegen des Casting-Verhaltens haben, wie 1.04E+17.saturating_cast::<u8>() , unsafe { 1.04E+17.unsafe_cast::<u8>() } und möglicherweise andere.

Oh, ich dachte, das Problem sei nur für u128, und wir können das in beide Richtungen unsicher machen.

@cryze UB sollte auch im Release-Modus im sicheren Code nicht existieren. Das Überlaufmaterial ist immer noch definiertes Verhalten.

Das heißt, Panik beim Debuggen undbei Veröffentlichung wäre toll.

Dies betrifft:

  • f32 -> u8, u16, u32, u64, u128, usize ( -1f32 as _ für alle, f32::MAX as _ für alle außer u128)
  • f32 -> i8, i16, i32, i64, i128, isize ( f32::MAX as _ für alle)
  • f64 -> alle Ints ( f64::MAX as _ für alle)

f32::INFINITY as u128 ist auch UB

@CryZe

Wäre es nicht sinnvoll, wenn es sich wie ein Überlauf verhält? In einem Debug-Build würde es also in Panik geraten, wenn Sie eine Besetzung mit undefiniertem Verhalten durchführen.

Dies ist, was ich anfangs dachte, aber ich wurde daran erinnert, dass as Conversions derzeit nie in Panik geraten (wir führen keine Überlaufprüfung mit as , egal ob gut oder schlecht). Das Analogste ist also, "etwas Definiertes zu tun".

FWIW, das "kill undef" -Ding würde in der Tat eine Möglichkeit bieten, die Speichersicherheit zu beheben, aber das Ergebnis nicht deterministisch zu lassen. Eine der Schlüsselkomponenten ist:

3) Erstellen Sie eine neue Anweisung '% y = freeze% x', die die Weitergabe von stoppt
Gift. Wenn die Eingabe Gift ist, gibt sie eine beliebige, aber feste
Wert. (wie altes undef, aber jede Verwendung bekommt den gleichen Wert), sonst ist es
gibt nur seinen Eingabewert zurück.

Der Grund, warum Undefs heutzutage verwendet werden können, um die Speichersicherheit zu verletzen, besteht darin, dass sie Werte zwischen Verwendungen auf magische Weise ändern können: insbesondere zwischen einer Grenzüberprüfung und einer nachfolgenden Zeigerarithmetik. Wenn rustc nach jedem gefährlichen Wurf ein Einfrieren hinzufügt, erhalten Sie nur einen unbekannten, aber ansonsten gut erzogenen Wert. In Bezug auf die Leistung ist das Einfrieren hier grundsätzlich kostenlos, da natürlich die Maschinenanweisung, die der Besetzung entspricht, einen einzelnen Wert erzeugt, keinen schwankenden; Selbst wenn der Optimierer aus irgendeinem Grund Lust hat, die Cast-Anweisung zu duplizieren, sollte dies sicher sein, da das Ergebnis für Eingaben außerhalb des Bereichs für eine bestimmte Architektur normalerweise deterministisch ist.

... aber nicht deterministisch über Architekturen hinweg, wenn sich jemand wunderte. x86 gibt 0x80000000 für alle fehlerhaften Eingaben zurück; ARM sättigt für Eingaben außerhalb des Bereichs und gibt (wenn ich diesen Pseudocode richtig lese) 0 für NaN zurück. Wenn das Ziel also darin besteht, ein deterministisches und plattformunabhängiges Ergebnis zu erzielen, reicht es nicht aus, nur das fp-to-int-Intrinsic der Plattform zu verwenden. Zumindest auf ARM müssen Sie auch das Statusregister auf eine Ausnahme überprüfen. Dies kann einen gewissen Overhead für sich haben und verhindert sicherlich die Autovektorisierung in dem unwahrscheinlichen Fall, dass die Verwendung des Intrinsic noch nicht geschehen ist. Alternativ könnten Sie mit regulären Vergleichsoperationen explizit auf Werte im Bereich testen und dann einen regulären Float-to-Int verwenden. Das klingt im Optimierer viel besser…

as Conversions geraten derzeit nie in Panik

Irgendwann haben wir + in Panik geändert (im Debug-Modus). Ich wäre nicht schockiert, wenn ich in Fällen, die zuvor UB waren, as Panik sehen würde.

Wenn es uns wichtig ist, zu überprüfen (was wir sollten), sollten wir entweder as verwerfen (gibt es einen Anwendungsfall, in dem dies die einzig gute Option ist?) Oder zumindest davon abraten, sie zu verwenden, und Leute zu Dingen wie bewegen Stattdessen TryFrom und TryInto , was wir sagten, dass wir vorhatten, es zurück zu tun, als beschlossen wurde, as unverändert zu lassen. Ich bin nicht der Meinung, dass sich die zur Diskussion stehenden Fälle abstrakt qualitativ von den Fällen unterscheiden, as bereits definiert ist, keine Überprüfungen durchzuführen. Der Unterschied besteht nur darin, dass in der Praxis die Implementierung für diese Fälle derzeit unvollständig ist und UB hat. Eine Welt , wo man kann nicht verlassen as Kontrollen zu tun (weil für die meisten Arten, tut es nicht), und man kann sich nicht darauf verlassen nicht (weil für einige Arten, es würde) in Panik zu geraten, und Es ist nicht konsistent und wir haben es immer noch nicht abgelehnt. Es scheint mir das Schlimmste von allen zu sein.

Ich denke also, dass @jorendorff an dieser Stelle was mir als der beste Plan erscheint :

  • as hat ein deterministisches Verhalten;
  • Wir werden ein Verhalten auswählen, das auf einer Kombination aus Sinn und Effizienz basiert

Er zählte drei Möglichkeiten auf. Ich denke, die verbleibende Arbeit besteht darin, diese Möglichkeiten zu untersuchen - oder zumindest eine davon zu untersuchen. Das heißt, implementieren Sie es tatsächlich und versuchen Sie, ein Gefühl dafür zu bekommen, wie "langsam" oder "schnell" es ist.

Gibt es da draußen jemanden, der sich motiviert fühlt, einen Stich zu machen? Ich werde dies als E-help-wanted markieren, in der Hoffnung, jemanden anzulocken. (@ oli-obk?)

Äh, ich zahle lieber keinen Preis für plattformübergreifende Konsistenz: / Es ist Garbage-In, es ist mir egal, welcher Müll rausgeht (eine Debug-Behauptung wäre jedoch super hilfreich).

Derzeit sind alle Rundungs- / Kürzungsfunktionen in Rust sehr langsam (Funktionsaufrufe mit akribisch präzisen Implementierungen), daher ist as mein letzter Ausweg für schnelles Float-Runden.

Wenn Sie as mehr als nur cvttss2si verdienen möchten, fügen Sie bitte auch eine stabile Alternative hinzu, die genau das ist.

@pornel das ist nicht nur UB der theoretischen Art, wo Sachen in Ordnung sind, wenn man ignoriert, dass es ub ist, es hat reale Auswirkungen. Ich habe # 41799 aus einem realen Codebeispiel extrahiert.

@ est31 Ich stimme zu, dass es falsch ist, es als UB zu freeze als Lösung für UB vorgeschlagen wurde. AFAIK, die es zu einem definierten deterministischen Wert macht, kann man einfach nicht sagen, welcher. Dieses Verhalten ist gut für mich.

Es wäre also in Ordnung, wenn z. B. u128::MAX as f32 deterministisch 17.5 auf x86 und 999.0 auf x86-64 und -555 auf ARM produzieren würde.

freeze würde keinen definierten, deterministischen, nicht spezifizierten Wert erzeugen. Das Ergebnis ist immer noch "ein beliebiges Bitmuster, das der Compiler mag", und es ist nur für alle Verwendungen derselben Operation konsistent. Dies mag die UB-produzierenden Beispiele umgehen, die die Leute oben gesammelt haben, aber es würde dies nicht geben:

u128 :: MAX als f32 erzeugte deterministisch 17,5 auf x86 und 999,0 auf x86-64 und -555 auf ARM.

Wenn LLVM beispielsweise feststellt, dass u128::MAX as f32 überläuft und durch freeze poison , kann dies eine gültige Senkung von fn foo() -> f32 { u128::MAX as f32 } auf x86_64 sein:

foo:
  ret

(Das heißt, geben Sie einfach das zurück, was zuletzt im Rückgaberegister gespeichert wurde.)

Aha. Das ist für meine Verwendung immer noch akzeptabel (für Fälle, in denen ich Werte außerhalb des Bereichs erwarte, klemme ich vorher. Wenn ich Werte im Bereich erwarte, aber nicht, dann werde ich auf keinen Fall ein korrektes Ergebnis erhalten). .

Ich habe kein Problem damit, dass Float-Casts außerhalb des Bereichs beliebige Werte zurückgeben, solange die Werte eingefroren sind, damit sie kein weiteres undefiniertes Verhalten verursachen können.

Ist so etwas wie freeze auf LLVM verfügbar? Ich dachte, das sei ein rein theoretisches Konstrukt.

@nikomatsakis Ich habe noch nie gesehen, dass es so verwendet wird (im Gegensatz zu poison ) - es ist eine geplante Überarbeitung von Poison / Undef.

freeze existiert heute in LLVM überhaupt nicht. Es wurde nur vorgeschlagen ( dieses PLDI-Papier ist eine in sich geschlossene Version, aber es wurde auch viel auf der Mailingliste diskutiert). Der Vorschlag scheint ein beträchtliches Buy-in zu haben, aber das ist natürlich keine Garantie dafür, dass er angenommen wird, geschweige denn rechtzeitig angenommen wird. (Das Entfernen der Pointee-Typen aus den Zeigertypen wird seit Jahren akzeptiert und wird immer noch nicht durchgeführt.)

Wollen wir einen RFC eröffnen, um eine breitere Diskussion über die hier vorgeschlagenen Änderungen zu erhalten? IMO, alles, was sich möglicherweise auf die Leistung von as auswirkt, wird umstritten sein, aber es wird doppelt umstritten sein, wenn wir den Menschen nicht die Möglichkeit geben, ihre Stimme Gehör zu verschaffen.

Ich bin ein Julia-Entwickler und verfolge dieses Problem seit einiger Zeit, da wir dasselbe LLVM-Backend verwenden und daher ähnliche Probleme haben. Falls es von Interesse ist, haben wir Folgendes festgelegt (mit ungefähren Zeitangaben für eine einzelne Funktion auf meinem Computer):

  • unsafe_trunc(Int64, x) wird direkt dem entsprechenden LLVM-intrinsischen fptosi (1,5 ns) zugeordnet.
  • trunc(Int64, x) löst eine Ausnahme für Werte außerhalb des Bereichs (3 ns) aus.
  • convert(Int64, x) löst eine Ausnahme für Werte außerhalb des Bereichs oder nicht ganzzahlige Werte (6 ns) aus.

Außerdem habe ich auf der Mailingliste gefragt ,

@bstrie Mir geht es gut mit einem RFC, aber ich denke, es wäre definitiv nützlich, Daten zu haben! Der Kommentar von

Ich habe mit der JS-Semantik (das erwähnte Modulo @jorendorff ) und der Java-Semantik herumgespielt , die die Spalte "Sättigung" zu sein scheint. Falls diese Links ablaufen, sind es JS und Java .

Ich habe auch eine schnelle Implementierung der Sättigung in Rust entwickelt, die ich für richtig halte (?). Und habe auch einige Benchmark-Zahlen . Interessanterweise sehe ich, dass die sättigende Implementierung 2-3x langsamer ist als die intrinsische, was sich von dem unterscheidet, was @simonbyrne mit nur 2x langsamer gefunden hat.

Ich bin mir nicht ganz sicher, wie ich die "Mod" -Semantik in Rust implementieren soll ...

Mir scheint jedoch klar zu sein, dass wir eine Reihe von f32::as_u32_unchecked() -Methoden und dergleichen für diejenigen benötigen, die die Leistung benötigen.

Es scheint klar zu sein, dass wir eine Reihe von f32::as_u32_unchecked() -Methoden und dergleichen für diejenigen benötigen, die die Leistung benötigen.

Das ist ein Mist - oder meinst du eine sichere, aber implementierungsdefinierte Variante?

Gibt es keine Option für eine Implementierung, die als schneller Standard definiert ist?

@eddyb Ich dachte, wir hätten nur unsafe fn as_u32_unchecked(self) -> u32 auf f32 und so, was ein direktes Analogon zu dem ist, was as heute ist.

Ich werde sicherlich nicht behaupten, dass die von mir geschriebene Rust-Implementierung optimal ist, aber ich hatte den Eindruck, dass beim Lesen dieses Threads Determinismus und Sicherheit in diesem Zusammenhang die meiste Zeit wichtiger waren als Geschwindigkeit. Die unsafe Notluke ist für diejenigen auf der anderen Seite des Zauns.

Es gibt also keine billige plattformabhängige Variante? Ich möchte etwas, das schnell ist, außerhalb der Grenzen einen nicht spezifizierten Wert liefert und sicher ist. Ich möchte UB nicht für einige Eingaben und ich denke, das ist zu gefährlich für den allgemeinen Gebrauch, wenn wir es besser machen können.

Soweit mir bekannt ist , auf die meisten , wenn nicht alle Plattformen der übliche Weg , um diese Umwandlung zu implementieren etwas out-of-Range - Eingänge hat , die nicht UB ist. Aber LLVM scheint keine Möglichkeit zu haben, diese Option (was auch immer es sein mag) über UB zu wählen. Wenn wir LLVM-Entwickler davon überzeugen könnten, ein Intrinsic einzuführen, das ein Ergebnis "nicht spezifiziert, aber nicht undef / poison " für Eingaben außerhalb des Bereichs liefert, könnten wir dies verwenden.

Aber ich würde schätzen, dass jemand in diesem Thread einen überzeugenden RFC (auf der llvm-dev-Liste) schreiben, sich einkaufen und implementieren muss (in Backends, die uns wichtig sind, und mit einer Fallback-Implementierung für andere) Ziele). Wahrscheinlich einfacher als llvm-dev davon zu überzeugen, die vorhandenen Casts nicht-UB zu machen (weil es Fragen wie "Wird dies C- und C ++ - Programme langsamer machen" umgeht), aber immer noch nicht sehr einfach.

Nur für den Fall, dass Sie zwischen diesen wählen:

Sättigung (Werte außerhalb des Bereichs werden zu IntType :: max_value () / min_value ())
Modulo (Werte außerhalb des Bereichs werden so behandelt, als würden sie zuerst in einen Bigint konvertiert und dann abgeschnitten)

IMO nur Sättigung wäre hier sinnvoll, da die absolute Genauigkeit des Gleitkommas schnell abnimmt, wenn die Werte groß werden, so dass das Modulo irgendwann wie alle Nullen nutzlos wäre.

Ich habe dies als E-needs-mentor markiert und es mit WG-compiler-middle markiert, da es den Anschein hat, dass die Impl-Periode ein guter Zeitpunkt ist, um dies weiter zu untersuchen! Meine vorhandenen Notizen zum Aufnahmeplan sind allerdings ziemlich spärlich , daher wäre es großartig, wenn jemand von @ rust-lang / compiler helfen würde, diese ein wenig weiter auszuarbeiten!

@ Nikomatsakis

IIRC LLVM plant die Implementierung von freeze , was es uns ermöglichen sollte, mit der UB umzugehen, indem wir freeze .

Meine bisherigen Ergebnisse: https://gist.github.com/s3bk/4bdfbe2acca30fcf587006ebb4811744

Die _array-Varianten führen eine Schleife mit 1024 Werten aus.
_cast: x as i32
_clip: x.min (MAX) .max (MIN) als i32
_panic: Panik, wenn x außerhalb der Grenzen liegt
_zero: Setzt das Ergebnis auf Null, wenn es außerhalb der Grenzen liegt

test bench_array_cast       ... bench:       1,840 ns/iter (+/- 37)
test bench_array_cast_clip  ... bench:       2,657 ns/iter (+/- 13)
test bench_array_cast_panic ... bench:       2,397 ns/iter (+/- 20)
test bench_array_cast_zero  ... bench:       2,671 ns/iter (+/- 19)
test bench_cast             ... bench:           2 ns/iter (+/- 0)
test bench_cast_clip        ... bench:           2 ns/iter (+/- 0)
test bench_cast_panic       ... bench:           2 ns/iter (+/- 0)
test bench_cast_zero        ... bench:           2 ns/iter (+/- 0)

Möglicherweise müssen Sie die Ergebnisse für einzelne Operationen nicht auf eine Ganzzahl runden. Es muss klar sein, dass hinter diesen 2 ns / iter ein Unterschied besteht. Oder ist es wirklich so, genau 2 ns für alle 4 Varianten?

@ sp-1234 Ich frage mich, ob es teilweise optimiert ist.

@ sp-1234 Es ist zu schnell zu messen. Die Nicht-Array-Benchmarks sind grundsätzlich nutzlos.
Wenn Sie die einwertigen Funktionen als Funktionen über #[inline(never)] erzwingen, erhalten Sie 2ns vs 3ns.

@ arielb1
Ich habe einige Vorbehalte bezüglich freeze . Wenn ich das richtig verstehe, kann ein eingefrorenes undef immer noch einen beliebigen Wert enthalten, es ändert sich jedoch nicht zwischen den Verwendungen. In der Praxis wird der Compiler wahrscheinlich ein Register oder einen Stapelsteckplatz wiederverwenden.

Dies bedeutet jedoch, dass wir jetzt nicht initialisierten Speicher aus sicherem Code lesen können. Dies könnte dazu führen, dass geheime Daten verloren gehen, ähnlich wie bei Heartbleed. Es ist fraglich, ob dies aus Sicht von Rust wirklich als UB angesehen wird, aber es scheint eindeutig unerwünscht.

Ich habe den Benchmark von

Leider scheint das Spammen von black_box nicht zu helfen. Ich sehe, dass der ASM nützliche Arbeit leistet, aber das Ausführen des Benchmarks ergibt immer noch konstant 0 ns für die skalaren Benchmarks (außer cast_zero , das 1 ns anzeigt). Ich sehe, dass @alexcrichton den Vergleich 100 Mal in ihren Benchmarks durchgeführt hat, also habe ich den gleichen Hack übernommen. Ich sehe jetzt diese Zahlen ( Quellcode ):

test bench_cast             ... bench:          53 ns/iter (+/- 0)
test bench_cast_clip        ... bench:         164 ns/iter (+/- 1)
test bench_cast_panic       ... bench:         172 ns/iter (+/- 2)
test bench_cast_zero        ... bench:         100 ns/iter (+/- 0)

Die Array-Benchmarks variieren zu stark, als dass ich ihnen vertrauen könnte. Um ehrlich zu sein, bin ich ohnehin skeptisch gegenüber der test Benchmarking-Infrastruktur, insbesondere nachdem ich die obigen Zahlen im Vergleich zu den flachen 0ns gesehen habe, die ich zuvor erhalten habe. Darüber hinaus benötigen bereits 100 Iterationen von black_box(x); (als Basis) 34 ns, was es noch schwieriger macht, diese Zahlen zuverlässig zu interpretieren.

Zwei bemerkenswerte Punkte:

  • Obwohl NaN nicht speziell behandelt wird (es gibt -inf anstelle von 0 zurück?), Scheint die Implementierung von cast_clip langsamer zu sein als die gesättigte Besetzung von as Casts, 53-54ns).
  • Im Gegensatz zu den Array-Ergebnissen von cast_panic langsamer ist als die anderen überprüften Casts. Ich sehe auch eine noch stärkere Verlangsamung der Array-Benchmarks. Vielleicht hängen diese Dinge nur stark von Details der Mikroarchitektur und / oder der Stimmung des Optimierers ab?

Für die Aufzeichnung habe ich mit rustc 1.21.0-nightly (d692a91fa 2017-08-04), -C opt-level=3 , auf einem i7-6700K unter leichter Last gemessen.


Zusammenfassend komme ich zu dem Schluss, dass es bisher keine zuverlässigen Daten gibt und dass es schwierig erscheint, zuverlässigere Daten zu erhalten. Darüber hinaus bezweifle ich stark, dass eine echte Anwendung sogar 1% ihrer Wanduhrzeit für diesen Vorgang verwendet. Daher würde ich vorschlagen, vorwärts zu gehen, indem Sie sättigende as Casts in rustc hinter einem -Z -Flag implementieren und dann einige nicht künstliche Benchmarks mit und ohne dieses Flag

Bearbeiten: Ich würde auch empfehlen, solche Benchmarks auf einer Vielzahl von Architekturen (z. B. einschließlich ARM) und Mikroarchitekturen auszuführen, wenn dies überhaupt möglich ist.

Ich gebe zu, dass ich mit Rost nicht so vertraut bin, aber ich denke, diese Zeile ist subtil falsch: std::i32::MAX (2 ^ 31-1) ist nicht genau als Float32 darstellbar, also wird std::i32::MAX as f32 sein auf den nächsten darstellbaren Wert gerundet (2 ^ 31). Wenn dieser Wert als Argument x , ist das Ergebnis technisch undefiniert. Das Ersetzen durch eine strikte Ungleichung sollte diesen Fall beheben.

Ja, wir hatten genau dieses Problem in Servo. Die endgültige Lösung bestand darin, auf f64 zu gießen und dann zu klemmen.

Es gibt andere Lösungen, aber sie sind ziemlich knifflig und Rost macht keine guten APIs verfügbar, um damit gut umzugehen.

Die Verwendung von 0x7FFF_FF80i32 als Obergrenze und -0x8000_0000i32 sollte dies lösen, ohne auf f64 zu übertragen.
Bearbeiten: Verwenden Sie den richtigen Wert.

Ich denke, Sie meinen 0x7fff_ff80 , aber die Verwendung einer strengen Ungleichung würde wahrscheinlich die Absicht des Codes klarer machen.

wie in x < 0x8000_0000u32 as f32 ? Das wäre wahrscheinlich eine gute Idee.

Ich denke an alle vorgeschlagenen deterministischen Optionen, das Klemmen ist geneal am nützlichsten, weil ich denke, dass es sowieso oft gemacht wird. Wenn der Konvertierungstyp tatsächlich als gesättigt dokumentiert würde, wäre eine manuelle Klemmung nicht mehr erforderlich.

Ich bin nur ein wenig besorgt über die vorgeschlagene Implementierung, da sie nicht richtig in Maschinenanweisungen übersetzt wird und stark von Verzweigungen abhängt. Durch die Verzweigung hängt die Leistung von bestimmten Datenmustern ab. In den oben angegebenen Testfällen sieht alles (vergleichsweise) schnell aus, da immer dieselbe Verzweigung verwendet wird und der Prozessor gute Verzweigungsvorhersagedaten aus vielen vorherigen Schleifeniterationen hat. Die reale Welt wird wahrscheinlich nicht so aussehen. Zusätzlich beeinträchtigt die Verzweigung die Fähigkeit des Compilers, den Code zu vektorisieren. Ich bin mit der Meinung von @rkruppe nicht

Aus den oben genannten Gründen habe ich mit einer alternativen verzweigungslosen und datenflussorientierten Version von @simonbyrnes Fix u16 , i16 und i32 implementiert, da sie alle leicht unterschiedliche Fälle abdecken müssen, was zu einer unterschiedlichen Leistung führt.

Die Ergebnisse:

test i16_bench_array_cast       ... bench:          99 ns/iter (+/- 2)
test i16_bench_array_cast_clip  ... bench:         197 ns/iter (+/- 3)
test i16_bench_array_cast_clip2 ... bench:         113 ns/iter (+/- 3)
test i16_bench_cast             ... bench:          76 ns/iter (+/- 1)
test i16_bench_cast_clip        ... bench:         218 ns/iter (+/- 25)
test i16_bench_cast_clip2       ... bench:         148 ns/iter (+/- 4)
test i16_bench_rng_cast         ... bench:       1,181 ns/iter (+/- 17)
test i16_bench_rng_cast_clip    ... bench:       1,952 ns/iter (+/- 27)
test i16_bench_rng_cast_clip2   ... bench:       1,287 ns/iter (+/- 19)

test i32_bench_array_cast       ... bench:         114 ns/iter (+/- 1)
test i32_bench_array_cast_clip  ... bench:         200 ns/iter (+/- 3)
test i32_bench_array_cast_clip2 ... bench:         128 ns/iter (+/- 3)
test i32_bench_cast             ... bench:          74 ns/iter (+/- 1)
test i32_bench_cast_clip        ... bench:         168 ns/iter (+/- 3)
test i32_bench_cast_clip2       ... bench:         189 ns/iter (+/- 3)
test i32_bench_rng_cast         ... bench:       1,184 ns/iter (+/- 13)
test i32_bench_rng_cast_clip    ... bench:       2,398 ns/iter (+/- 41)
test i32_bench_rng_cast_clip2   ... bench:       1,349 ns/iter (+/- 19)

test u16_bench_array_cast       ... bench:          99 ns/iter (+/- 1)
test u16_bench_array_cast_clip  ... bench:         136 ns/iter (+/- 3)
test u16_bench_array_cast_clip2 ... bench:         105 ns/iter (+/- 3)
test u16_bench_cast             ... bench:          76 ns/iter (+/- 2)
test u16_bench_cast_clip        ... bench:         184 ns/iter (+/- 7)
test u16_bench_cast_clip2       ... bench:         110 ns/iter (+/- 0)
test u16_bench_rng_cast         ... bench:       1,178 ns/iter (+/- 22)
test u16_bench_rng_cast_clip    ... bench:       1,336 ns/iter (+/- 26)
test u16_bench_rng_cast_clip2   ... bench:       1,207 ns/iter (+/- 21)

Der Test wurde auf einer Intel Haswell i5-4570-CPU und Rust 1.22.0 pro Nacht ausgeführt.
clip2 ist die neue Implementierung ohne Zweig. Es stimmt mit clip für alle 2 ^ 32 möglichen f32-Eingabewerte überein.

Für die rng Benchmarks werden zufällige Eingabewerte verwendet, die häufig unterschiedliche Fälle betreffen. Dies deckt die extremen Leistungskosten auf (ungefähr das Zehnfache der normalen Kosten !!!), die anfallen, wenn die Verzweigungsvorhersage fehlschlägt. Ich denke, es ist sehr wichtig, dies zu berücksichtigen. Es ist auch nicht die durchschnittliche Leistung in der realen Welt, aber es ist immer noch ein möglicher Fall, und einige Anwendungen werden dies treffen. Die Leute erwarten von einer f32-Besetzung eine konstante Leistung.

Assmbly-Vergleich auf x86: https://godbolt.org/g/AhdF71
Die verzweigungslose Version ist sehr gut den minss / maxss-Anweisungen zugeordnet.

Leider konnte ich Godbolt nicht dazu bringen, eine ARM-Assembly aus Rust zu generieren, aber hier ist ein ARM-Vergleich der Methoden mit Clang: https://godbolt.org/g/s7ronw
Ohne den Code testen zu können und viel über ARM zu wissen: Die Codegröße scheint ebenfalls kleiner zu sein, und LLVM generiert meistens vmax / vmin, was vielversprechend aussieht. Vielleicht könnte LLVM irgendwann beigebracht werden, den größten Teil des Codes in eine einzige Anweisung zu falten?

@ActuallyaDeviloper Der rustc zu generieren als die verschachtelten Bedingungen anderer Lösungen (für den Datensatz gehe ich davon aus, dass wir Inline-IR generieren möchten, anstatt eine Lang-Item-Funktion aufzurufen). Vielen Dank für das Schreiben.

Ich habe eine Frage zu u16_cast_clip2 : Es scheint nicht mit NaN umzugehen?! Es gibt einen Kommentar über NaN, aber ich glaube, die Funktion wird NaN unverändert durchlaufen und versuchen, es in f32 (und selbst wenn dies nicht der Fall wäre, würde es einen der Grenzwerte anstelle von 0 erzeugen ).

PS: Um klar zu sein, ich wollte nicht implizieren, dass es unwichtig ist, ob die Besetzung vektorisiert werden kann. Es ist eindeutig wichtig, ob der umgebende Code ansonsten vektorisierbar ist. Die Skalarleistung ist jedoch auch wichtig, da die Vektorisierung häufig nicht anwendbar ist und die von mir kommentierten Benchmarks keine Aussage über die Skalarleistung machten. Haben Sie aus Interesse den Asm der *array* Benchmarks überprüft, um festzustellen, ob sie mit Ihrer Implementierung noch vektorisiert sind?

@rkruppe Du hast recht, ich habe versehentlich die Seiten des if getauscht und das vergessen. f32 as u16 hat das Richtige getan, indem er die oberen 0x8000 abgeschnitten hat, sodass die Tests es auch nicht verstanden haben. Ich habe das Problem jetzt behoben, indem ich die Zweige erneut ausgetauscht und diesmal alle Methoden mit if (y.is_nan()) { panic!("NaN"); } getestet habe.

Ich habe meinen vorherigen Beitrag aktualisiert. Der x86-Code hat sich überhaupt nicht wesentlich geändert, aber leider verhindert die Änderung, dass LLVM aus irgendeinem Grund vmax im Fall u16 ARM generiert. Ich gehe davon aus, dass dies mit einigen Details zur NaN-Handhabung dieses ARM-Befehls zu tun hat oder dass es sich möglicherweise um eine LLVM-Einschränkung handelt.

Beachten Sie, warum der untere Grenzwert für vorzeichenlose Werte tatsächlich 0 ist. So können NaN und die Untergrenze gleichzeitig gefangen werden.

Die Array-Versionen sind vektorisiert.
Godbolt: https://godbolt.org/g/HnmsSV

Betreff: Der ARM-Asm , ich glaube, der Grund, warum vmax nicht mehr verwendet wird, ist, dass er NaN zurückgibt, wenn einer der Operanden NaN ist . Der Code ist immer noch verzweigungslos, verwendet jedoch nur vorhergesagte Bewegungen ( vmovgt , bezogen auf das Ergebnis der früheren vcmp mit 0).

Beachten Sie, warum der untere Grenzwert für vorzeichenlose Werte tatsächlich 0 ist. So können NaN und die Untergrenze gleichzeitig gefangen werden.

Ohhh, richtig. Nett.

Ich würde vorschlagen, vorwärts zu gehen, indem Sie die Sättigung als Abgüsse in rustc hinter einer -Z-Flagge implementieren

Ich habe dies implementiert und werde eine PR einreichen, sobald ich auch # 41799 behoben habe und viel mehr Tests habe.

45134 hat auf einen Codepfad hingewiesen, den ich verpasst habe (Generierung von LLVM-Konstantenausdrücken - dies ist getrennt von der eigenen konstanten Auswertung von rustc). Ich werde einen Fix dafür in dieselbe PR rollen, aber es wird eine Weile länger dauern.

@rkruppe Du

Pull-Anfrage ist aktiv: # 45205

45205 wurde zusammengeführt, sodass jeder jetzt (beginnend mit der nächsten Nacht) die Auswirkungen der Sättigung auf die Leistung messen kann, indem er -Z saturating-float-casts über RUSTFLAGS übergibt. [1] Solche Messungen wären sehr wertvoll, um zu entscheiden, wie mit diesem Problem umgegangen werden soll.

[1] Genau genommen hat dies keine Auswirkungen auf die nicht generischen, nicht #[inline] Teile der Standardbibliothek. Um 100% genau zu sein, sollten Sie std lokal mit Xargo erstellen. Ich erwarte jedoch nicht, dass viel Code davon betroffen sein wird (die verschiedenen Implikationen für Conversion-Merkmale sind beispielsweise #[inline] ).

@rkruppe Ich schlage vor, eine Interna / Benutzer-Seite zu starten, um Daten zu sammeln, ähnlich wie https://internals.rust-lang.org/t/help-us-benchmark-incremental-compilation/6153/ (wir können es dann auch Leute damit verknüpfen, anstatt zufällige Kommentare in unserem Issue-Tracker)

@rkruppe Sie sollten ein Tracking-Problem erstellen. Diese Diskussion ist bereits in zwei Punkte unterteilt. Das ist nicht gut!

@Gankro Ja, ich stimme zu, aber es kann einige Tage dauern, bis ich die Zeit finde, diesen Beitrag richtig zu schreiben. Ich dachte mir, ich würde in der Zwischenzeit Feedback von den Leuten einholen, die dieses Problem abonniert haben.

@ est31 Hmm . Obwohl das Flag -Z beide Zauberrichtungen abdeckt (was im Nachhinein ein Fehler gewesen sein könnte), ist es unwahrscheinlich, dass wir beide gleichzeitig betätigen, und es gibt kaum Überschneidungen zwischen den beiden in Bezug auf das, was muss diskutiert werden (z. B. hängt dieses Problem von der Leistung der Sättigung ab, während in # 41799 vereinbart wird, welche Lösung die richtige ist).
Es ist ein bisschen albern, dass Benchmarks, die in erster Linie auf dieses Problem abzielen, auch die Auswirkungen des Fixes auf # 41799 messen würden, aber das kann höchstens zu einer Überberichterstattung über Leistungsregressionen führen, also bin ich damit einverstanden. (Aber wenn jemand motiviert ist, die -Z-Flagge in zwei Teile zu teilen, fahren Sie fort.)

Ich habe ein Tracking-Problem für die Aufgabe in Betracht gezogen, die Flagge zu entfernen, sobald sie ihre Nützlichkeit überlebt hat, aber ich sehe keine Notwendigkeit, die hier und in # 41799 stattfindenden Diskussionen zusammenzuführen.

Ich habe einen internen Beitrag verfasst: https://gist.github.com/Gankro/feab9fb0c42881984caf93c7ad494ebd

Fühlen Sie sich frei, das zu kopieren, oder geben Sie mir einfach Notizen, damit ich es posten kann. (Hinweis: Ich bin etwas verwirrt über das Verhalten von const fn )

Ein weiterer Leckerbissen ist, dass die Kosten für float-> int-Konvertierungen spezifisch für die aktuelle Implementierung sind und nicht grundlegend. Auf x86 gibt cvtss2si cvttss2si 0x80000000 in den Fällen zu niedrig, zu hoch und nan zurück, sodass man -Zsaturating-float-casts mit einem cvtss2si cvttss2si implementieren kann vcvt.s32.f32 bereits die Semantik -Zsaturating-float-casts . In beiden Fällen optimiert LLVM die zusätzlichen Überprüfungen derzeit nicht.

@ Gankro

Super, vielen Dank! Ich habe ein paar Notizen im Kern hinterlassen. Nachdem ich dies gelesen habe, möchte ich versuchen, u128-> f32-Casts von der -Z-Flagge zu trennen. Nur um die ablenkende Einschränkung über die Flagge, die zwei orthogonale Merkmale abdeckt, loszuwerden.

(Ich habe # 45900 eingereicht, um das Flag -Z neu zu fokussieren, sodass es nur das Problem float-> int abdeckt.)

Es wäre schön, wenn wir plattformspezifische Implementierungen a la @sunfishcode (zumindest für x86) erhalten könnten, bevor wir nach Massen-Benchmarking fragen. Es sollte nicht sehr schwierig sein.

Das Problem ist, dass LLVM derzeit meines Wissens keine Möglichkeit bietet, dies zu tun, außer vielleicht mit Inline-Asm, das ich für eine Veröffentlichung nicht unbedingt empfehlen würde.

Ich habe den Entwurf aktualisiert, um die Diskussion widerzuspiegeln (im Grunde genommen wird jede Inline-Erwähnung von u128 -> f32 in einen zusätzlichen Abschnitt am Ende gestrichen).

@sunfishcode Bist du sicher? Ist das llvm.x86.sse.cvttss2si nicht das, wonach Sie suchen?

Hier ist ein Spielplatz-Link, der ihn verwendet:

https://play.rust-lang.org/?gist=33cf9e0871df2eb2475b845af4f1b574&version=nightly

Im Release-Modus werden float_to_int_with_intrinsic und float_to_int_with_as zu einer einzigen Anweisung kompiliert. (Im Debug-Modus verschwendet float_to_int_with_intrinsic ein paar Anweisungen, um Null in das Hoch zu setzen, aber es ist nicht so schlimm.)

Es scheint sogar konstant richtig zu falten. Zum Beispiel,

float_to_int_with_intrinsic(42.0)

wird

movl    $42, %eax

Aber ein Wert außerhalb des Bereichs,

float_to_int_with_intrinsic(42.0e33)

wird nicht gefaltet:

cvttss2si   .LCPI2_0(%rip), %eax

(Idealerweise würde es auf die Konstante 0x80000000 klappen, aber das ist keine große Sache. Wichtig ist, dass es kein Undef erzeugt.)

Oh cool. Es sieht so aus, als würde das funktionieren!

Es ist cool zu wissen, dass wir schließlich eine Möglichkeit haben, auf cvttss2si aufzubauen. Ich stimme jedoch nicht zu, dass es eindeutig besser ist, die Implementierung zu ändern, um sie zu verwenden, bevor wir Benchmarks fordern:

Die meisten Benutzer werden x86-Benchmarks durchführen. Wenn wir also x86 als Sonderfall verwenden, erhalten wir weitaus weniger Daten zur allgemeinen Implementierung, die für die meisten anderen Ziele weiterhin verwendet werden. Zugegeben, es ist bereits schwierig, auf andere Architekturen zu schließen, aber eine völlig andere Implementierung macht es völlig unmöglich.

Zweitens, wenn wir jetzt mit der "einfachen" Lösung Benchmarks sammeln und feststellen, dass es in echtem Code keine Leistungsregressionen gibt (und das ist, was ich erwarte), müssen wir uns nicht einmal die Mühe machen, es zu versuchen Optimieren Sie diesen Codepfad weiter.

Schließlich bin ich mir nicht einmal sicher, ob das Bauen auf cvttss2si schneller sein wird als das, was wir jetzt haben (obwohl es bei ARM eindeutig besser ist, nur die entsprechende Anweisung zu verwenden):

  • Sie benötigen einen Vergleich, um festzustellen, dass die Konvertierung 0x80000000 zurückgibt. In diesem Fall benötigen Sie noch einen Vergleich (des Eingabewerts), um festzustellen, ob Sie int :: MIN oder int :: MAX zurückgeben sollten. Und wenn es sich um einen vorzeichenbehafteten Integer-Typ handelt, kann ich einen dritten Vergleich zur Unterscheidung von NaN nicht vermeiden. Also im schlimmsten Fall:

    • Sie speichern nicht in der Anzahl der Vergleiche / Auswahlen

    • Sie tauschen einen Float-Vergleich gegen einen Int-Vergleich, was für OoO-Kerne hilfreich sein könnte (wenn Sie einen Engpass bei FUs haben, die Vergleiche durchführen können, was wie ein relativ großes Wenn erscheint), aber dieser Vergleich hängt auch vom Float ab -> int Vergleich, während die Vergleiche in der aktuellen Implementierung alle unabhängig sind, ist es keineswegs offensichtlich, dass dies ein Gewinn ist.

  • Die Vektorisierung wird wahrscheinlich schwieriger oder unmöglich. Ich erwarte nicht, dass der Loop-Vektorisierer dies überhaupt handhabt.
  • Es ist auch erwähnenswert, dass (AFAIK) diese Strategie nur für einige ganzzahlige Typen gilt. Für f32 -> u8 sind beispielsweise zusätzliche Korrekturen des Ergebnisses erforderlich, wodurch diese Strategie eindeutig unrentabel wird. Ich bin mir nicht ganz sicher, welche Typen davon betroffen sind (z. B. weiß ich nicht, ob es eine Anweisung für f32 -> u32 gibt), aber eine Anwendung, die nur diese Typen verwendet, profitiert überhaupt nicht.
  • Sie können eine Verzweigungslösung mit nur einem Vergleich auf dem Happy Path erstellen (im Gegensatz zu zwei oder drei Vergleichen und damit Verzweigungen wie bei früheren Lösungen). Wie @ActuallyaDeviloper zuvor

Ist es sicher anzunehmen, dass wir eine Menge unsafe fn as_u32_unchecked(self) -> u32 und Freunde brauchen werden, unabhängig davon, was das Benchmarking zeigt? Was andere mögliche Regress würde jemand haben , wenn sie eine Verlangsamung zu beobachten am Ende haben?

@bstrie Ich denke, es wäre in einem as <type> [unchecked] und zu verlangen, dass unchecked nur in unsafe Kontexte.

Aus meiner Sicht wäre eine Gesamtstruktur von _unchecked eine Variante von as Casting, sowohl was die Intuitivität betrifft als auch was die Erstellung sauberer, benutzerfreundlicher Dokumentationen betrifft.

@ssokolow Das Hinzufügen von Syntax sollte immer ein letzter Ausweg sein, insbesondere wenn all dies mit nur zehn roten Funktionen erledigt werden kann. Sogar ein generisches foo.as_unchecked::<u32>() wäre syntaktischen Änderungen (und dem damit einhergehenden endlosen Bikeshed) vorzuziehen, zumal wir die Anzahl der Dinge, die unsafe entsperren, reduzieren und nicht erhöhen sollten.

Punkt. Der Turbofisch ist mir durch den Kopf gegangen, als ich über Optionen nachgedacht habe, und im Nachhinein schieße ich auch heute Abend nicht auf alle Zylinder. Deshalb hätte ich vorsichtiger sein sollen, wenn ich Designentscheidungen kommentiere.

Trotzdem fühlt es sich falsch an, den Zieltyp in den Funktionsnamen zu backen ... unelegant und eine potenzielle Belastung für die zukünftige Entwicklung der Sprache. Der Turbofisch scheint eine bessere Option zu sein.

Eine generische Methode könnte durch einen neuen Satz von UncheckedFrom / UncheckedInto Merkmalen mit unsafe fn Methoden unterstützt werden, die sich den From / Into und anschließen TryFrom / TryInto Sammlung.

@bstrie Eine alternative Lösung für Personen, deren Code langsamer wurde, könnte darin bestehen, eine intrinsische (z. B. über stdsimd) zu verwenden, um auf die zugrunde liegende Hardwareanweisung zuzugreifen. Ich habe vorhin argumentiert, dass dies Nachteile für den Optimierer hat - die automatische Vektorisierung leidet wahrscheinlich, und LLVM kann sie nicht ausnutzen, wenn sie undef für Eingaben außerhalb des Bereichs zurückgibt - aber es bietet eine Möglichkeit, die Besetzung ohne zu machen zusätzliche Arbeit zur Laufzeit. Ich kann mich nicht entscheiden, ob das gut genug ist, aber es scheint zumindest plausibel, dass es sein könnte.

Einige Hinweise zu Konvertierungen im x86-Befehlssatz:

SSE2 ist tatsächlich relativ begrenzt in den Konvertierungsvorgängen, die es Ihnen bietet. Du hast:

  • CVTTSS2SI-Familie mit 32-Bit-Register: Konvertiert Single Float in i32
  • CVTTSS2SI-Familie mit 64-Bit-Register: Konvertiert Single Float in i64 (nur x86-64)
  • CVTTPS2PI-Familie: Konvertiert zwei Floats in zwei i32s

Jede davon hat Varianten für f32 und f64 (sowie Varianten, die runden anstatt abzuschneiden, aber das ist hier nutzlos).

Aber es gibt nichts für vorzeichenlose Ganzzahlen, nichts für Größen kleiner als 32, und wenn Sie 32-Bit x86 verwenden, nichts für 64-Bit. Spätere Befehlssatzerweiterungen bieten mehr Funktionen, aber es scheint, dass kaum jemand für diese kompiliert.

Infolgedessen ist das vorhandene ("unsichere") Verhalten:

  • Um in u32 zu konvertieren, konvertieren Compiler in i64 und kürzen die resultierende Ganzzahl. (Dies führt zu einem merkwürdigen Verhalten bei Werten außerhalb des Bereichs, aber das ist UB, also wen interessiert das?)
  • Um in 16-Bit- oder 8-Bit-Dateien zu konvertieren, konvertieren Compiler in i64 oder i32 und kürzen die resultierende Ganzzahl.
  • Um auf u64 zu konvertieren, generieren Compiler eine Vielzahl von Anweisungen. Für f32 bis u64 generieren GCC und LLVM ein Äquivalent von:
fn f32_to_u64(f: f32) -> u64 {
    const CUTOFF: f32 = 0x8000000000000000 as f32; // 2^63 exactly
    if !(f >= CUTOFF) { // less, or NaN
        // just use the signed conversion
        f as i64 as u64
    } else {
        0x8000000000000000u64 + ((f - CUTOFF) as i64 as u64)
    }
}

Nicht verwandte lustige Tatsache: Die Generierung von Code "Konvertieren als Abschneiden" verursacht den Fehler " Parallele Universen " in Super Mario 64. Der Kollisionserkennungscode zuerst MIPS-Befehl zum Konvertieren von f32-Koordinaten in i32, dann Abschneiden in i16; Wenn Sie also Koordinaten haben, die in i16 passen, aber nicht in i32 'Wrap', z. B. wenn Sie 65536.0 koordinieren, erhalten Sie eine Kollisionserkennung für 0.0.

Wie auch immer, Schlussfolgerungen:

  • "Auf 0x80000000 testen und speziellen Handler haben" funktioniert nur bei Konvertierungen in i32 und i64.
  • Für Konvertierungen in u32, u / i16 und u / i8 ist jedoch "Test, ob die abgeschnittene / vorzeichenerweiterte Ausgabe vom Original abweicht" äquivalent. (Dies würde beide Ganzzahlen erfassen, die für die ursprüngliche Konvertierung im Bereich, für den endgültigen Typ jedoch außerhalb des Bereichs lagen, und 0x8000000000000000, den Indikator dafür, dass der Float NaN war oder für die ursprüngliche Konvertierung außerhalb des Bereichs lag.)
  • Aber die Kosten für eine Zweigstelle und eine Menge zusätzlichen Codes für diesen Fall sind wahrscheinlich übertrieben. Es kann in Ordnung sein, wenn Verzweigungen vermieden werden können.
  • @ActuallyaDevilopers minss / maxss-basierter Ansatz ist nicht so schlecht! Die minimale Form,
minss %xmm2, %xmm1
maxss %xmm3, %xmm1
cvttss2si %rax, %xmm1

Es gibt nur drei Befehle (die eine anständige Codegröße und Durchsatz / Latenz haben) und keine Verzweigungen.

Jedoch:

  • Die Pure-Rust-Version benötigt einen zusätzlichen Test für NaN. Bei Konvertierungen in 32-Bit oder kleiner kann dies mithilfe von Intrinsics vermieden werden, indem 64-Bit-cvttss2si verwendet und das Ergebnis abgeschnitten wird. Wenn die Eingabe nicht NaN war, stellen die Min / Max sicher, dass die Ganzzahl durch Abschneiden unverändert bleibt. Wenn die Eingabe NaN war, ist die Ganzzahl 0x8000000000000000, was auf 0 abschneidet.
  • Ich habe die Kosten für das Laden von 2147483647.0 und -2148473648.0 nicht in die Register aufgenommen, normalerweise jeweils eine Bewegung aus dem Speicher.
  • Für f32 kann 2147483647.0 nicht genau dargestellt werden, sodass dies nicht funktioniert: Sie benötigen eine weitere Überprüfung. Das macht die Sache noch viel schlimmer. Das Gleiche gilt für f64 zu u / i64, aber f64 zu u / i32 hat dieses Problem nicht.

Ich schlage einen Kompromiss zwischen den beiden Ansätzen vor:

  • Für f32 / f64 bis u / i16 und u / i8 und f64 bis u / i32 gehen Sie wie oben mit min / max + Kürzung, z.
    let f = if f > 32767.0 { 32767.0 } else { f };
    let f = if f < -32768.0 { -32768.0 } else { f };
    cvttss2si(f) as i16

(Für u / i16 und u / i8 kann die ursprüngliche Konvertierung in i32 erfolgen; für f64 in u / i32 muss sie in i64 erfolgen.)

  • Für f32 / 64 bis u32
    let r = cvttss2si64(f) as u32;
    if f >= 4294967296.0 { 4294967295 } else { r }

ist nur ein paar Anweisungen und keine Zweige:

    cvttss2si   %xmm0, %rcx
    ucomiss .LCPI0_0(%rip), %xmm0
    movl    $-1, %eax
    cmovbl  %ecx, %eax
  • Für f32 / 64 bis i64 vielleicht
    let r = cvttss2si64(f);
    if f >= 9223372036854775808. {
        9223372036854775807 
    } else if f != f {
        0
    } else {
        r
    }

Dies erzeugt eine längere (immer noch verzweigungslose) Sequenz:

    cvttss2si   %xmm0, %rax
    xorl    %ecx, %ecx
    ucomiss %xmm0, %xmm0
    cmovnpq %rax, %rcx
    ucomiss .LCPI0_0(%rip), %xmm0
    movabsq $9223372036854775807, %rax
    cmovbq  %rcx, %rax

… Aber zumindest speichern wir einen Vergleich im Vergleich zum naiven Ansatz, als ob f zu klein ist, 0x8000000000000000 ist bereits die richtige Antwort (dh i64 :: MIN).

  • Bei f32 bis i32 ist nicht sicher, ob es vorzuziehen ist, dasselbe wie zuvor zu tun, oder erst zuerst auf f64 konvertieren und dann die kürzere Min / Max-Sache ausführen.

  • u64 ist ein Durcheinander, an das ich nicht denken möchte. : p

In https://internals.rust-lang.org/t/help-us-benchmark-saturating-float-casts/6231/14 berichtete jemand von einer messbaren und signifikanten Verlangsamung der JPEG-Codierung mit der Bildkiste. Ich habe das Programm so minimiert, dass es in sich geschlossen ist und sich hauptsächlich auf die Teile konzentriert, die mit der Verlangsamung zusammenhängen: https://gist.github.com/rkruppe/4e7972a209f74654ebd872eb4bc57722 (dieses Programm zeigt für mich eine Verlangsamung von ~ 15% bei Sättigung Abgüsse).

Beachten Sie, dass die Casts zu gleichen Anteilen f32-> u8 ( rgb_to_ycbcr ) und f32-> i32 ( encode_rgb , "Quantisierungs" -Schleife) sind. Es sieht auch so aus, als ob die Eingaben alle im Bereich liegen, dh die Sättigung setzt nie ein, aber im Fall von f32-> u8 kann dies nur überprüft werden, indem das Minimum und Maximum eines Polynoms berechnet und Rundungsfehler berücksichtigt werden ist viel zu fragen. Die f32-> i32-Casts liegen für i32 offensichtlicher im Bereich, aber nur, weil die Elemente von self.tables ungleich Null sind, was (anscheinend?) Für den Optimierer nicht so einfach zu zeigen ist, insbesondere im ursprünglichen Programm. tl; dr: Die Sättigungsprüfungen bleiben bestehen, die einzige Hoffnung besteht darin, sie schneller zu machen.

Ich habe mich auch mit dem LLVM IR befasst - es scheint buchstäblich der einzige Unterschied zu sein, sind die Vergleiche und die Auswahl aus den sättigenden Besetzungen. Ein kurzer Blick zeigt an, dass der ASM entsprechende Anweisungen und natürlich eine Reihe weiterer Live-Werte enthält (was zu mehr Verschüttungen führt).

@comex Denken Sie, dass f32-> u8- und f32-> i32-Casts mit CVTTSS2SI messbar schneller gemacht werden können?

Kleinere Aktualisierungen, ab rustc 1.28.0-nightly (952f344cd 2018-05-18) , das Flag -Zsaturating-float-casts bewirkt weiterhin, dass der Code in https://github.com/rust-lang/rust/issues/10184#issuecomment -345479698 ~ 20 ist % langsamer auf x86_64. Das heißt, LLVM 6 hat nichts geändert.

| Fahnen | Timing |
| ------- | -------: |
| -Copt-Level = 3 -Ctarget-CPU = native | 325.699 ns / iter (+/- 7.607) |
| -Copt-Level = 3 -Ctarget-CPU = native -Zsaturating-Float-Casts | 386.962 ns / iter (+/- 11.601)
(19% langsamer) |
| -Copt-Level = 3 | 331.521 ns / iter (+/- 14.096) |
| -Copt-Level = 3 -Zsättigende-Float-Casts | 413.572 ns / iter (+/- 19.183)
(25% langsamer) |

@kennytm Haben wir erwartet, dass LLVM 6 etwas ändert? Besprechen sie eine bestimmte Verbesserung, die diesem Anwendungsfall zugute kommen würde? Wenn ja, wie lautet die Ticketnummer?

@insanitybit Es ... scheint noch offen zu sein ...?

image

Welp, keine Ahnung, was ich sah. Vielen Dank!

@rkruppe haben wir nicht sichergestellt, dass Float-to-Int-Casts in LLVM nicht mehr UB sind
(durch Ändern der Dokumente)?

Am 20. Juli 2018, 04:31 Uhr, schrieb "Colin" [email protected] :

Welp, keine Ahnung, was ich sah.

- -
Sie erhalten dies, weil Sie diesen Thread abonniert haben.
Antworte direkt auf diese E-Mail und sieh sie dir auf GitHub an
https://github.com/rust-lang/rust/issues/10184#issuecomment-406462053 ,
oder stumm schalten
der Faden
https://github.com/notifications/unsubscribe-auth/AApc0v3rJHhZMD7Kv7RC8xkGOiIhkGB1ks5uITMHgaJpZM4BJ45C
.

@nagisa Vielleicht denkst du an f32::from_bits(v: u32) -> f32 (und ähnlich an f64 )? Früher wurden NaNs normalisiert, jetzt sind es nur noch transmute .

Bei diesem Problem handelt es sich um as -Konvertierungen, bei denen versucht wird, den numerischen Wert zu approximieren.

@nagisa Sie denken vielleicht an Float-> Float-Casts, siehe # 15536 ​​und https://github.com/rust-lang-nursery/nomicon/pull/65.

Ah ja, das war Float to Float.

Am Fr, 20. Juli 2018, 12:24 schrieb Robin Kruppe [email protected] :

@nagisa https://github.com/nagisa Sie denken vielleicht an float-> float
Abgüsse, siehe # 15536 https://github.com/rust-lang/rust/issues/15536 und
rust-lang-kindergarten / nomicon # 65
https://github.com/rust-lang-nursery/nomicon/pull/65 .

- -
Sie erhalten dies, weil Sie erwähnt wurden.
Antworte direkt auf diese E-Mail und sieh sie dir auf GitHub an
https://github.com/rust-lang/rust/issues/10184#issuecomment-406542903 ,
oder schalten Sie den Thread stumm
https://github.com/notifications/unsubscribe-auth/AApc0gA24Hz8ndnYhRXCyacd3HdUSZjYks5uIaHegaJpZM4BJ45C
.

In den Versionshinweisen zu LLVM 7 wird Folgendes erwähnt:

Die Optimierung von Gleitkomma-Casts wurde verbessert. Dies kann zu überraschenden Ergebnissen für Code führen, der auf dem undefinierten Verhalten überlaufender Casts beruht. Die Optimierung kann durch Angabe eines Funktionsattributs deaktiviert werden: "strict-float-cast-overflow" = "false". Dieses Attribut kann durch die clang-Option -fno-strict-float-cast-overflow erstellt werden. Code-Desinfektionsmittel können verwendet werden, um betroffene Muster zu erkennen. Die Clang-Option, um dieses Problem allein zu erkennen, ist -fsanitize = float-cast-overflow:

Hat das einen Einfluss auf dieses Thema?

Es sollte uns egal sein, was LLVM für überlaufende Casts tut, solange es kein unsicheres undefiniertes Verhalten ist. Das Ergebnis kann Müll sein, solange es kein fehlerhaftes Verhalten verursacht.

Hat das einen Einfluss auf dieses Thema?

Nicht wirklich. Die UB hat sich nicht geändert, LLVM wurde noch aggressiver bei der Nutzung, was es einfacher macht, in der Praxis davon betroffen zu sein, aber das Problem der Solidität bleibt unverändert. Insbesondere entfernt das neue Attribut weder die UB noch beeinflusst es Optimierungen, die vor LLVM 7 vorhanden waren.

@rkruppe aus Neugier, ist diese Art von auf der Strecke geblieben? Es scheint, dass https://internals.rust-lang.org/t/help-us-benchmark-saturating-float-casts/6231/14 gut genug gelaufen ist und die Implementierung nicht zu viele Fehler hatte. Es scheint, dass immer eine leichte Leistungsregression erwartet wurde, aber eine korrekte Kompilierung scheint ein lohnender Kompromiss zu sein.

Wartet das nur darauf, über die Ziellinie geschoben zu werden? Oder gibt es andere bekannte Blocker?

Meistens war ich abgelenkt / beschäftigt mit anderen Dingen, aber eine x0,82-Regression in der RBG-JPEG-Codierung scheint mehr als "geringfügig" zu sein, eine ziemlich bittere Pille zum Schlucken (obwohl es beruhigend ist, dass andere Arten von Arbeitsbelastung nicht betroffen zu sein scheinen). . Es ist nicht schwerwiegend genug, dass ich es ablehnen würde, die Sättigung standardmäßig einzuschalten, aber genug, dass ich zögere, selbst darauf zu drängen, bevor wir versucht haben, "auch eine Konvertierungsfunktion bereitzustellen, die schneller als die Sättigung ist, aber (sicheren) Müll erzeugen kann "Option zuvor besprochen. Ich bin nicht dazu gekommen, und anscheinend hat es auch niemand anderes, also ist dies auf der Strecke geblieben.

Ok cool danke für das Update @rkruppe! Ich bin allerdings neugierig, ob es tatsächlich eine Implementierung der Option für sicheren Müll gibt. Ich könnte mir vorstellen, dass wir leicht etwas wie unsafe fn i32::unchecked_from_f32(...) und dergleichen bereitstellen, aber es klingt so, als ob Sie denken, dass dies eine sichere Funktion sein sollte. Ist das heute mit LLVM möglich?

Es gibt noch kein freeze aber es ist möglich, mithilfe der Inline-Assembly auf die Anweisung der Zielarchitektur zuzugreifen, um Floats in Ganzzahlen zu konvertieren (mit einem Fallback auf z. B. Sättigung von as ). Dies kann zwar einige Optimierungen verhindern, ist jedoch möglicherweise gut genug, um die Regression in einigen Benchmarks größtenteils zu beheben.

Eine unsafe -Funktion, die die UB, um die es in diesem Problem geht, beibehält (und auf die gleiche Weise wie as heute codegend ist), ist eine weitere Option, aber eine viel weniger attraktive, ich ' Ich bevorzuge eine sichere Funktion, wenn sie die Arbeit erledigen kann.

Es gibt auch erheblichen Verbesserungsbedarf bei der sicheren Sättigungs-Float-to-Int-Sequenz . LLVM hat heute nichts spezielles dafür, aber wenn Inline-ASM-Lösungen auf dem Tisch liegen, wäre es nicht schwierig, so etwas zu tun:

     cvttsd2si %xmm0, %eax   # x86's cvttsd2si returns 0x80000000 on overflow and invalid cases
     cmp $1, %eax            # a compact way to test whether %eax is equal to 0x80000000
     jno ok
     ...  # slow path: check for and handle overflow and invalid cases
ok:

Das sollte deutlich schneller sein als das, was rustc derzeit tut .

Ok, ich wollte nur sichergehen, klar zu stellen, danke! Ich habe festgestellt, dass Inline-ASM-Lösungen nicht als Standardeinstellungen verwendet werden können, da dies andere Optimierungen zu sehr hemmen würde, aber ich habe es selbst nicht ausprobiert. Ich persönlich würde es vorziehen, wenn wir dieses unsolide Loch schließen, indem wir ein vernünftiges Verhalten definieren (wie genau die heutigen sättigenden Besetzungen). Bei Bedarf können wir die heutige schnelle / unsolide Implementierung immer als unsichere Funktion beibehalten und in der begrenzten Zeit bei unendlichen Ressourcen sogar die Standardeinstellung drastisch verbessern und / oder andere spezialisierte Konvertierungsfunktionen hinzufügen (wie eine sichere Konvertierung, bei der keine Grenzen gesetzt sind) 't UB aber nur ein Müllbitmuster)

Würden andere gegen eine solche Strategie sein? Denken wir, dass dies nicht wichtig genug ist, um es in der Zwischenzeit zu beheben?

Ich denke, Inline-Assembly sollte für cvttsd2si (oder ähnliche Anweisungen) tolerierbar sein, insbesondere weil dieser Inline-ASM nicht auf den Speicher zugreifen oder Nebenwirkungen haben würde. Es handelt sich also nur um eine undurchsichtige Blackbox, die entfernt werden kann, wenn sie nicht verwendet wird und nicht Sperre Optimierungen um es sehr, LLVM kann einfach nicht Grund , über die Interna und Ergebniswert des Inline - asm. Das letzte Bit ist der Grund, warum ich skeptisch wäre, wenn ich zB Inline-Asm für die Codesequenz verwenden würde, die @sunfishcode für die Sättigung vorschlägt: Die auf Sättigung eingeführten Prüfungen können heute gelegentlich entfernt werden, wenn sie redundant sind, aber die Zweige in einem Inline-Asm-Block können ' nicht vereinfacht werden.

Würden andere gegen eine solche Strategie sein? Denken wir, dass dies nicht wichtig genug ist, um es in der Zwischenzeit zu beheben?

Ich habe nichts dagegen, jetzt auf Sättigung umzudrehen und möglicherweise später Alternativen hinzuzufügen. Ich möchte einfach nicht derjenige sein, der den Konsens dafür aufbringen und ihn gegenüber Benutzern rechtfertigen muss, deren Code langsamer geworden ist 😅

Ich habe einige Arbeiten zur Implementierung von Intrinsics für die Sättigung von Float-Int-Casts in LLVM begonnen: https://reviews.llvm.org/D54749

Wenn dies irgendwohin führt, bietet es eine relativ kostengünstige Möglichkeit, die Sättigungssemantik zu erhalten.

Wie reproduziert man dieses undefinierte Verhalten? Ich habe das Beispiel im Kommentar ausprobiert, aber das Ergebnis war 255 , was mir in Ordnung erscheint:

println!("{}", 1.04E+17 as u8);

Undefiniertes Verhalten kann auf diese Weise nicht zuverlässig beobachtet werden. Manchmal gibt es Ihnen das, was Sie erwarten, aber in komplexeren Situationen bricht es zusammen.

Kurz gesagt, die von uns verwendete Codegenerierungs-Engine (LLVM) darf davon ausgehen, dass dies nicht der Fall ist, und kann daher möglicherweise schlechten Code generieren, wenn sie sich jemals auf diese Annahme stützt.

@ AaronM04 Ein Beispiel für reproduzierbares undefiniertes Verhalten wurde heute auf reddit veröffentlicht :

fn main() {
    let a = 360.0f32;
    println!("{}", a as u8);

    let a = 360.0f32 as u8;
    println!("{}", a);

    println!("{}", 360.0f32 as u8);
}

(siehe Spielplatz )

Ich gehe davon aus, dass der letzte Kommentar für den vorherigen Kommentar gedacht war.

"Oh, das ist dann einfach genug."

  • @pcwalton , 2014

Entschuldigung, ich habe diese 6-jährige Geschichte guter Absichten sehr sorgfältig gelesen. Aber im Ernst, 6 lange von 10 Jahren !!! Wäre es ein Politikerforum gewesen, hätte man hier eine lodernde Sabotage erwartet.

Kann jemand mit einfachen Worten erklären, was die Suche nach einer Lösung interessanter macht als die Lösung selbst?

Weil es schwieriger ist als es ursprünglich schien und LLVM-Änderungen benötigt.

Ok, aber es war nicht Gott, der dieses LLVM in seiner zweiten Woche gemacht hat, und es könnte weitere 15 Jahre dauern, bis dieses grundlegende Problem gelöst ist.

Wirklich, ich habe keine Aufmerksamkeit darauf, jemanden zu verletzen, und ich bin neu in der Rust-Infrastruktur, um plötzlich zu helfen, aber als ich von diesem Fall erfahren habe, war ich einfach fassungslos.

Dieser Issue-Tracker dient zur Erörterung der Lösung dieses Problems, und die Angabe des Offensichtlichen macht in dieser Richtung keine Fortschritte. Wenn Sie also bei der Lösung des Problems helfen möchten oder neue Informationen beitragen möchten, tun Sie dies bitte. Andernfalls werden Ihre Kommentare das Update nicht auf magische Weise anzeigen. :) :)

Ich denke, die Annahme, dass dies Änderungen in der LLVM erfordert, ist verfrüht.

Ich denke, wir können es in der Sprache mit minimalen Leistungskosten tun. Wäre es eine Bruch Änderung * * ja sein , aber es könnte getan werden, und sollte getan werden.

Meine Lösung wäre, float to int casts als unsafe dann einige Hilfsfunktionen in der Standardbibliothek bereitzustellen, um Ergebnisse bereitzustellen, die an Result Types gebunden sind.

Es ist eine unsexy Lösung und eine bahnbrechende Änderung, aber letztendlich muss sich jeder Entwickler selbst codieren, um die vorhandene UB bereits zu umgehen. Dies ist der richtige Rostansatz.

Vielen Dank, @RalfJung , dass ich es verstehe. Ich hatte nicht die Absicht, jemanden zu beleidigen oder verächtlich in den produktiven Brainstorming-Prozess einzugreifen. Da ich neu in Rost bin, kann ich zwar nicht so viel tun. Trotzdem hilft es mir und vielleicht anderen, die versuchen, in Rost einzudringen, mehr über seine ungelösten Mängel zu erfahren und die entsprechenden Ergebnisse zu erzielen: Lohnt es sich, tiefer zu graben, oder ist es besser, vorerst etwas anderes zu wählen? Aber ich bin schon froh, dass das Entfernen "meiner nutzlosen Kommentare" viel einfacher sein wird.

Wie bereits im Thread erwähnt, wird dies langsam aber sicher auf die richtige Weise behoben, indem llvm so festgelegt wird, dass die erforderliche Semantik unterstützt wird, wie die entsprechenden Teams vor langer Zeit vereinbart haben.

Zu dieser Diskussion kann wirklich nichts mehr hinzugefügt werden.

https://reviews.llvm.org/D54749

@nikic Scheint, als ob die Fortschritte auf der Seite von LLVM ins Stocken geraten sind.

Kann die Sättigungsbesetzung als Bibliotheksfunktion implementiert werden, für die sich Benutzer entscheiden können, wenn sie bereit sind, eine bevorzugte Regression vorzunehmen, um gesund zu werden? Ich lese die Implementierung des Compilers, aber es scheint ziemlich subtil:

https://github.com/rust-lang/rust/blob/625451e376bb2e5283fc4741caa0a3e8a2ca4d54/src/librustc_codegen_ssa/mir/rvalue.rs#L774 -L901

Wir könnten ein Intrinsic verfügbar machen, das das LLVM-IR für die Sättigung generiert (unabhängig davon, ob es sich um das derzeit offen codierte IR oder um llvm.fpto[su]i.sat in der Zukunft handelt), unabhängig vom Flag -Z . Das ist gar nicht so schwer.

Ich bin jedoch besorgt, ob dies die beste Vorgehensweise ist. Wenn (wenn?) Die Sättigung zur Standardsemantik von as Casts wird, wird eine solche API redundant. Es erscheint auch unangemessen, den Benutzern mitzuteilen, dass sie selbst entscheiden sollten, ob sie Solidität oder Leistung wünschen, auch wenn dies nur vorübergehend ist.

Gleichzeitig ist die aktuelle Situation deutlich noch schlimmer. Wenn wir über das Hinzufügen von Bibliotheks-APIs nachdenken, warne ich immer mehr davor, standardmäßig nur die Sättigung zu aktivieren und eine unsafe -Intrinsics anzubieten, die UB für NaN und Zahlen außerhalb des Bereichs enthält (und auf sinkt) ein einfaches fpto[su]i ). Das würde im Grunde immer noch die gleiche Auswahl bieten, aber standardmäßig die Solidität, und die neue API würde wahrscheinlich in Zukunft nicht überflüssig werden.

Standardmäßig auf Sound umzuschalten klingt gut. Ich denke, wir können das Wesentliche eher auf Anfrage als von Anfang an anbieten. Wird die Sättigung auch in diesem Fall konstant sein? (cc @RalfJung @eddyb @ oli-obk)

Const eval wir machen schon Sättigung und das schon seit Ewigkeiten, denke ich schon vor miri (ich erinnere mich noch genau daran, wie ich es im alten llvm::Constant -basierten Evaluator geändert habe).

@rkruppe Super ! Möchten Sie die Standardeinstellungen ändern, da Sie mit dem betreffenden Code vertraut sind?

@rkruppe

Wir könnten ein Intrinsic aussetzen, das das LLVM-IR für die Sättigung erzeugt

Dies müssen möglicherweise 10 oder 12 separate Intrinsics für jede Kombination aus Quell- und Zieltyp sein.

@Centril

Standardmäßig auf Sound umzuschalten klingt gut. Ich denke, wir können das Wesentliche eher auf Anfrage als von Anfang an anbieten.

Ich gehe davon aus, dass im Gegensatz zu anderen Kommentaren "das Intrinsische" in Ihrem Kommentar etwas bedeutet, das weniger bevorzugte Regression hätte, wenn as Sättigung bewirkt.

Ich denke nicht, dass dies ein guter Ansatz ist, um mit bekannten signifikanten Regressionen umzugehen. Für einige Benutzer kann der Leistungsverlust ein echtes Problem sein, während ihr Algorithmus sicherstellt, dass die Eingabe immer im Bereich liegt. Wenn sie diesen Thread nicht abonniert haben, stellen sie möglicherweise erst fest, dass sie betroffen sind, wenn die Änderung den stabilen Kanal erreicht. Zu diesem Zeitpunkt können sie 6 bis 12 Wochen lang stecken bleiben, selbst wenn wir auf Anfrage sofort eine unsichere API erhalten.

Ich würde viel lieber dem Muster folgen, das bereits für Verfallswarnungen festgelegt wurde: Wechseln Sie erst in Nightly, nachdem die Alternative für einige Zeit in Stable verfügbar ist.

Dies müssen möglicherweise 10 oder 12 separate Intrinsics für jede Kombination aus Quell- und Zieltyp sein.

Gut, du hast mich, aber ich sehe nicht, wie das relevant ist? Lassen Sie es 30 intrinsische sein, es ist immer noch trivial, sie hinzuzufügen. In Wirklichkeit ist es jedoch noch einfacher, eine einzige generische Eigenschaft zu haben, die von N Thin Wrappern verwendet wird. Die Zahl ändert sich auch nicht, wenn wir die Option " as klingen lassen und eine unsafe Cast-API einführen" wählen.

Ich denke nicht, dass dies ein guter Ansatz ist, um mit bekannten signifikanten Regressionen umzugehen. Für einige Benutzer kann der Leistungsverlust ein echtes Problem sein, während ihr Algorithmus sicherstellt, dass die Eingabe immer im Bereich liegt. Wenn sie diesen Thread nicht abonniert haben, stellen sie möglicherweise erst fest, dass sie betroffen sind, wenn die Änderung den stabilen Kanal erreicht. Zu diesem Zeitpunkt können sie 6 bis 12 Wochen lang stecken bleiben, selbst wenn wir auf Anfrage sofort eine unsichere API erhalten.

+1

Ich bin mir nicht sicher, ob das Verfahren für Verfallswarnungen (nur abends veraltet, wenn der Austausch stabil ist) erforderlich ist, da es weniger wichtig erscheint, über alle Freigabekanäle hinweg perf-regressionsfrei zu bleiben, als über alle Freigabekanäle hinweg warnungsfrei zu bleiben Andererseits ist das Warten von weiteren 12 Wochen im Grunde ein Rundungsfehler, wenn man bedenkt, wie lange dieses Problem besteht.

Wir können auch die -Zsaturating-float-casts herum belassen (nur die Standardeinstellung ändern), was bedeutet, dass jeder nächtliche Benutzer den Cange noch eine Weile deaktivieren kann.

(Ja, die Anzahl der Intrinsics ist nur ein Implementierungsdetail und nicht als Argument für oder gegen irgendetwas gedacht.)

@rkruppe Ich kann hier nicht alle Kommentare verdaut haben Anspruch, aber ich habe den Eindruck , dass LLVM nun ein Einfrieren - Befehl verfügbar, die das Element blockiert den „kürzesten Weg“ war hier UB zu beseitigen, nicht wahr?

Obwohl ich denke, dass freeze so neu ist, dass es möglicherweise nicht in unserer eigenen Version von LLVM verfügbar ist, oder? Scheint dennoch etwas zu sein, an dem wir die Entwicklung untersuchen sollten, vielleicht in der ersten Hälfte des Jahres 2020?

Nominierung für die Diskussion beim T-Compiler-Meeting, um zu diesem Zeitpunkt einen groben Konsens über unseren gewünschten Weg zu erzielen.

Die Verwendung von freeze ist jedoch aus allen hier genannten Gründen immer noch problematisch. Ich bin mir nicht sicher, wie realistisch solche Bedenken bei der Verwendung von Freeze für diese Casts sind, aber im Prinzip gelten sie. Erwarten Sie grundsätzlich, dass freeze entweder zufälligen Müll oder Ihren geheimen Schlüssel zurückgibt, was auch immer schlimmer ist. (Ich habe das irgendwo online gelesen und mag es wirklich, wenn es eine Zusammenfassung hat .: D)

Und überhaupt, selbst die Rückgabe von zufälligem Müll scheint für eine Besetzung von as ziemlich schlecht zu sein. Es ist sinnvoll, bei Bedarf schnellere Operationen für die Geschwindigkeit durchzuführen, ähnlich wie bei unchecked_add , aber die Standardeinstellung scheint ziemlich stark gegen Rusts Geist zu sein.

@SimonSapin Sie haben zuerst den umgekehrten Ansatz vorgeschlagen (standardmäßig unsolide / "seltsame" Semantik und eine explizit fundierte Methode); Ich kann Ihren späteren Kommentaren nicht entnehmen, ob Sie der Meinung sind, dass die Festlegung der Solidität (nach einer geeigneten Übergangszeit) auch vernünftig / besser ist.

@pnkfelix

Ich habe den Eindruck, dass LLVM jetzt eine Einfrieranweisung hat, die den "kürzesten Weg" zur Eliminierung von UB hier blockiert hat, oder?

Es gibt einige Einschränkungen. Selbst wenn wir uns nur darum kümmern, UB loszuwerden und unser gebündeltes LLVM so zu aktualisieren, dass es freeze (was wir jederzeit tun können), unterstützen wir mehrere ältere Versionen (zurück zu LLVM 6 im Moment) und wir würden eine Fallback-Implementierung benötigen, damit die UB tatsächlich für alle Benutzer entfernt wird.

Zweitens ist natürlich die Frage, ob "nur nicht UB" alles ist, was uns interessiert, während wir dabei sind. Insbesondere möchte ich noch einmal hervorheben, dass sich ein freeze(fptosi %x) äußerst kontraintuitiv verhält: Es ist nicht deterministisch und kann bei jeder Ausführung ein anderes Ergebnis zurückgeben (sogar eines, das aus dem sensiblen Speicher stammt, wie @RalfJung sagte). Ich möchte jetzt nicht noch einmal darüber diskutieren, aber es lohnt sich, in der Besprechung darüber nachzudenken, ob wir lieber etwas mehr arbeiten möchten, um die Sättigung als Standard festzulegen und ungeprüfte (entweder unsichere oder freeze -verwendende) Conversions durchzuführen die nicht standardmäßige Option.

@RalfJung Meine Position ist, dass as unabhängig von diesem Problem am besten vollständig vermieden wird, da es je nach Eingabe- und Ausgabetyp eine sehr unterschiedliche Semantik (Abschneiden, Sättigen, Runden usw.) haben kann, und dies ist nicht immer der Fall offensichtlich beim Lesen von Code. (Sogar letzteres kann mit foo as _ .) Ich habe also einen Entwurf für einen Pre-RFC, in dem verschiedene explizit benannte Konvertierungsmethoden vorgeschlagen werden, die die Fälle abdecken, die as heute (und möglicherweise mehr) tut. .

Ich denke, as sollte definitiv kein UB haben, da es außerhalb von unsafe . Die Rückgabe von Müll klingt auch nicht gut. Aber wir sollten wahrscheinlich eine Art Abschwächung / Übergang / Alternative für bekannte Fälle von Leistungsregression haben, die durch gesättigte Besetzung verursacht werden. Ich habe nur nach einer Bibliotheksimplementierung von Sättigungsbesetzung gefragt, um diesen RFC-Entwurf bei diesem Übergang nicht zu blockieren.

@ SimonSapin

Mein Standpunkt ist, dass dies unabhängig von diesem Problem am besten vermieden wird, da es eine völlig andere Semantik haben kann (Abschneiden, Sättigen, Runden,…).

Einverstanden. Aber das hilft uns bei diesem Problem nicht wirklich.

(Ich bin auch froh, dass Sie daran arbeiten, dass as nicht benötigt wird. Ich freue mich darauf .: D)

Ich denke, da sollte UB definitiv nicht haben, da es außerhalb von unsicher verwendet werden kann. Die Rückgabe von Müll klingt auch nicht gut. Aber wir sollten wahrscheinlich eine Art Abschwächung / Übergang / Alternative für bekannte Fälle von Leistungsregression haben, die durch gesättigte Besetzung verursacht werden. Ich habe nur nach einer Bibliotheksimplementierung von Sättigungsbesetzung gefragt, um diesen RFC-Entwurf bei diesem Übergang nicht zu blockieren.

Wir scheinen uns also einig zu sein, dass der Endzustand sein sollte, dass float-to-int as gesättigt ist? Ich bin mit jedem Übergangsplan zufrieden, solange dies das Endziel ist, auf das wir hinarbeiten.

Dieses Endziel klingt gut für mich.

Ich denke nicht, dass dies ein guter Ansatz ist, um mit bekannten signifikanten Regressionen umzugehen. Für einige Benutzer kann der Leistungsverlust ein echtes Problem sein, während ihr Algorithmus sicherstellt, dass die Eingabe immer im Bereich liegt. Wenn sie diesen Thread nicht abonniert haben, stellen sie möglicherweise erst fest, dass sie betroffen sind, wenn die Änderung den stabilen Kanal erreicht. Zu diesem Zeitpunkt können sie 6 bis 12 Wochen lang stecken bleiben, selbst wenn wir auf Anfrage sofort eine unsichere API erhalten.

Meiner Ansicht nach wäre es nicht das Ende der Welt, wenn diese Benutzer mit dem Upgrade ihres rustc für diese 6-12 Wochen warten würden - sie benötigen in beiden Fällen möglicherweise nichts von den kommenden Versionen, oder ihre Bibliotheken haben möglicherweise MSRV-Einschränkungen aufrecht erhalten.

In der Zwischenzeit erhalten Benutzer, die den Thread ebenfalls nicht abonniert haben, möglicherweise Fehlkompilierungen, ebenso wie Leistungseinbußen. Welche sollten wir priorisieren? Wir geben Garantien für Stabilität und wir geben Garantien für Sicherheit - aber meines Wissens gibt es keine solchen Garantien für die Leistung (z. B. erwähnt RFC 1122 Perf überhaupt nicht).

Ich würde viel lieber dem Muster folgen, das bereits für Verfallswarnungen festgelegt wurde: Wechseln Sie erst in Nightly, nachdem die Alternative für einige Zeit in Stable verfügbar ist.

Im Fall von Abwertungswarnungen sind die Konsequenzen des Wartens mit der Abwertung, bis es eine stabile Alternative gibt, zumindest meines Wissens keine soliden Löcher während der Wartezeit. (Auch wenn hier Intrinsics bereitgestellt werden können, können wir im allgemeinen Fall möglicherweise keine vernünftigen Alternativen anbieten, wenn wir solide Löcher reparieren. Daher denke ich nicht, dass es eine schwierige Anforderung sein kann, Alternativen für Stables zu haben.)

Gut, du hast mich, aber ich sehe nicht, wie das relevant ist? Lassen Sie es 30 intrinsische sein, es ist immer noch trivial, sie hinzuzufügen. In Wirklichkeit ist es jedoch noch einfacher, eine einzige generische Eigenschaft zu haben, die von N Thin Wrappern verwendet wird. Die Zahl ändert sich auch nicht, wenn wir die Option " as klingen lassen und eine unsafe Cast-API einführen" wählen.

Erfordert dieses einzelne generische Intrinsic nicht separate Implementierungen im Compiler für diese 12/30 spezifischen monomorphen Instanziierungen?

Es mag trivial sein, dem Compiler Intrinsics hinzuzufügen, da LLVM bereits den größten Teil der Arbeit erledigt hat, aber das ist auch weit von den vollen Kosten entfernt. Darüber hinaus gibt es die Implementierung in Miri, Cranelift, sowie die eventuell in einer Spezifikation erforderlichen Arbeiten. Ich denke also nicht, dass wir der Möglichkeit, dass jemand sie braucht, Eigenheiten hinzufügen sollten.

Ich bin jedoch nicht dagegen, mehr Intrinsics aufzudecken, aber wenn jemand sie braucht, sollte er einen Vorschlag machen (z. B. als PR mit einer ausführlichen Beschreibung) und die Hinzufügung mit einigen Benchmarking-Zahlen oder dergleichen rechtfertigen.

Wir können auch die -Zsaturating-float-casts herum belassen (nur die Standardeinstellung ändern), was bedeutet, dass jeder nächtliche Benutzer den Cange noch eine Weile deaktivieren kann.

Dies scheint mir in Ordnung zu sein, aber ich würde vorschlagen, die Flagge in -Zunsaturating-float-casts umzubenennen, um zu vermeiden, dass die Semantik für diejenigen, die diese Flagge bereits verwenden, in Richtung Unklarheit geändert wird.

@Centril

Erfordert dieses einzelne generische Intrinsic nicht separate Implementierungen im Compiler für diese 12/30 spezifischen monomorphen Instanziierungen?

Nein, der größte Teil der Implementierung kann und wird bereits gemeinsam genutzt, indem die Quell- und Zielbitbreiten parametrisiert werden. Nur wenige Bits benötigen Fallunterscheidungen. Gleiches gilt für die Implementierung in miri und höchstwahrscheinlich auch für andere Implementierungen und Spezifikationen.

(Bearbeiten: Um klar zu sein, diese Freigabe kann auch dann erfolgen, wenn N verschiedene Eigenheiten vorhanden sind, aber eine einzige generische Eigenheit die Kesselplatte verringert, die pro Eigen erforderlich ist.)

Ich denke also nicht, dass wir der Möglichkeit, dass jemand sie braucht, Eigenheiten hinzufügen sollten.

Ich bin jedoch nicht dagegen, mehr Intrinsics aufzudecken, aber wenn jemand sie braucht, sollte er einen Vorschlag machen (z. B. als PR mit einer ausführlichen Beschreibung) und die Hinzufügung mit einigen Benchmarking-Zahlen oder dergleichen rechtfertigen. Ich denke nicht, dass dies die Reparatur des Schalllochs blockieren sollte.

Wir haben bereits einige Benchmarking-Zahlen. Wir wissen aus der Forderung nach Benchmarks vor langer Zeit, dass die JPEG-Codierung auf x86_64 mit sättigenden Casts @nikic, an dem gearbeitet wurde, würde dies grundlegend ändern. Obwohl es schwierig ist, sich über die Zukunft sicher zu sein, ist meine Vermutung, dass der einzig plausible Weg, um diese Leistung zurückzugewinnen, darin besteht, etwas zu verwenden, das Code ohne Bereichsprüfungen generiert, wie eine unsafe -Konvertierung oder etwas, das freeze .

Aus den vorhandenen Benchmarking-Zahlen geht hervor, dass ein aktiver Wunsch nach diesen Eigenschaften besteht. Wenn ja, würde ich folgenden Aktionsplan vorschlagen:

  1. Gleichzeitig:

    • Führen Sie die nächtlichen Funktionen durch #[unstable(...)] -Funktionen ein.

    • Entfernen Sie -Zsaturating-float-casts und führen Sie -Zunsaturating-float-casts .

    • Schalten Sie die Standardeinstellung auf -Zsaturating-float-casts .

  2. Wir stabilisieren die Eigenschaften nach einiger Zeit; wir können ein bisschen beschleunigen.
  3. Entfernen Sie nach einer Weile -Zunsaturating-float-casts .

Klingt gut. Abgesehen davon, dass Intrinsics Implementierungsdetails einiger öffentlicher APIs sind, wahrscheinlich Methoden für f32 und f64 . Sie könnten entweder sein:

  • Methoden eines generischen Merkmals (mit einem Parameter für den Integer-Rückgabetyp der Konvertierung), optional im Vorspiel
  • Inhärente Methoden mit einem unterstützenden Merkmal (ähnlich wie str::parse und FromStr ), um verschiedene Rückgabetypen zu unterstützen
  • Mehrere nicht generische inhärente Methoden mit dem Zieltyp im Namen

Ja, ich meinte, die Intrinsics über Methoden oder ähnliches aufzudecken.

Mehrere nicht generische inhärente Methoden mit dem Zieltyp im Namen

Dies fühlt sich wie das übliche an, was wir tun - irgendwelche Einwände gegen diese Option?

Ist es trotzdem? Ich habe das Gefühl, wenn der Name eines Typs (der Signatur) als Teil des Namens einer Methode verwendet wird, handelt es sich um Ad-hoc-Konvertierungen (wie Vec::as_slice und [T]::to_vec ). oder eine Reihe von Conversions, bei denen der Unterschied kein Typ ist (wie to_ne_bytes , to_be_bytes , to_le_bytes ). Ein Teil der Motivation für die Eigenschaften von std::convert bestand darin, Dutzende verschiedener Methoden wie u8::to_u16 , u8::to_u32 , u8::to_u64 usw. zu vermeiden.

Mein Wunder ist, ob dies natürlich auf ein Merkmal verallgemeinerbar wäre, da die Methoden unsafe fn . Wenn wir inhärente Methoden hinzufügen, können Sie diese immer an diejenigen in Trait-Implementierungen delegieren und so weiter.

Es erscheint mir seltsam, Merkmale für unsichere Konvertierungen hinzuzufügen, aber ich denke, Simon denkt wahrscheinlich darüber nach, dass wir möglicherweise für jede Kombination aus Gleitkomma- und Ganzzahltyp eine andere Methode benötigen würden (z. B. f32::to_u8_unsaturated , f32::to_u16_unsaturated usw.).

Nicht um einen langen Thread abzuwägen, den ich nicht in völliger Unwissenheit gelesen habe, aber ist das erwünscht oder reicht es aus, zB f32::to_integer_unsaturated das sich in u32 oder so umwandelt? Gibt es eine offensichtliche Wahl für den Zieltyp für die unsichere Konvertierung?

Wenn Sie beispielsweise unsichere Konvertierungen nur für i32 / u32 bereitstellen, werden alle Ganzzahltypen vollständig ausgeschlossen, deren Wertebereich nicht unbedingt kleiner ist, und dies ist definitiv manchmal erforderlich. Eine Verkleinerung (bis auf u8, wie bei der JPEG-Codierung) ist ebenfalls häufig erforderlich, kann jedoch emuliert werden, indem in einen breiteren Integer-Typ konvertiert und mit as abgeschnitten wird (was billig, aber normalerweise nicht kostenlos ist).

Wir können jedoch nicht nur die Konvertierung in die größte Ganzzahlgröße bereitstellen. Diese werden nicht immer nativ unterstützt (daher langsam), und Optimierungen können dies nicht beheben: Es ist nicht sinnvoll, "in große int konvertieren und dann direkt in" in kleinere int konvertieren "zu kürzen, da letztere UB (in LLVM IR) hat. / Unterschiedliche Ergebnisse (auf Maschinencodeebene, auf den meisten Architekturen) in den Fällen, in denen das ursprüngliche Konvertierungsergebnis beim Abschneiden umgangen worden wäre.

Beachten Sie, dass selbst das pragmatische Ausschließen von 128-Bit-Ganzzahlen und das Fokussieren auf 64-Bit-Ganzzahlen für gängige 32-Bit-Ziele immer noch schlecht ist.

Ich bin neu in diesem Gespräch, aber nicht in der Programmierung. Ich bin gespannt, warum die Leute glauben, dass gesättigte Conversions und die Konvertierung von NaN in Null vernünftige Standardverhalten sind. Ich verstehe, dass Java dies tut (obwohl das Umhüllen viel häufiger zu sein scheint), aber es gibt keinen ganzzahligen Wert, für den NaN wirklich als korrekte Konvertierung bezeichnet werden kann. In ähnlicher Weise scheint beispielsweise die Konvertierung von 1000000.0 in 65535 (u16) falsch zu sein. Es gibt einfach kein U16, das eindeutig die richtige Antwort ist. Zumindest sehe ich es nicht als besser an als das derzeitige Verhalten bei der Konvertierung in 16960, das zumindest mit C / C ++, C #, go und anderen geteilt wird und daher zumindest nicht überraschend ist.

Verschiedene Leute haben die Ähnlichkeit mit der Überlaufprüfung kommentiert, und ich stimme ihnen zu. Es ähnelt auch der Ganzzahldivision durch Null. Ich denke, ungültige Konvertierungen sollten genauso in Panik geraten wie ungültige Arithmetik. Sich auf NaN -> 0 und 1000000.0 -> 65535 (oder 16960) zu verlassen, scheint genauso fehleranfällig zu sein wie sich auf einen ganzzahligen Überlauf oder eine hypothetische n / 0 == 0 zu verlassen. Diese Art von Dingen sollte standardmäßig einen Fehler erzeugen. (In Release-Builds kann Rost die Fehlerprüfung umgehen, genau wie bei der Ganzzahlarithmetik.) Und in den seltenen Fällen, in denen Sie NaN in Null konvertieren möchten oder eine Gleitkommasättigung haben möchten, sollten Sie sich genau wie diese dafür entscheiden müssen Sie müssen sich für einen ganzzahligen Überlauf entscheiden.

Was die Leistung betrifft, scheint es, als würde die höchste allgemeine Leistung durch eine einfache Konvertierung und das Verlassen auf Hardwarefehler erzielt. Sowohl x86 als auch ARM lösen beispielsweise Hardware-Ausnahmen aus, wenn eine Gleitkomma-Ganzzahl-Konvertierung nicht korrekt dargestellt werden kann (einschließlich NaN- und Fällen außerhalb des Bereichs). Diese Lösung ist mit Ausnahme ungültiger Konvertierungen kostengünstig, außer bei der direkten Konvertierung von Gleitkomma- in kleine Ganzzahltypen in Debug-Builds - ein seltener Fall - wo sie immer noch vergleichsweise günstig sein sollte. (Auf theoretischer Hardware, die diese Ausnahmen nicht unterstützt, kann sie in Software emuliert werden, aber wiederum nur in Debug-Builds.) Ich stelle mir vor, dass Hardware-Ausnahmen genau die Implementierung der Ganzzahldivision durch Null heute implementieren. Ich habe viel über LLVM gesprochen, vielleicht sind Sie hier eingeschränkt, aber es wäre bedauerlich, bei jeder Gleitkommakonvertierung eine Softwareemulation zu haben, selbst in Release-Builds, um zweifelhafte alternative Verhaltensweisen für inhärent ungültige Konvertierungen bereitzustellen.

@admilazz Wir sind durch die

Die Sättigung liegt daran, dass die Sprache as Casts definiert, um immer erfolgreich zu sein. Daher können wir den Operator nicht in Panik ändern.

In ähnlicher Weise scheint beispielsweise die Konvertierung von 1000000.0 in 65535 (u16) falsch zu sein. Es gibt einfach kein U16, das eindeutig die richtige Antwort ist. Zumindest sehe ich es nicht als besser an als das derzeitige Verhalten bei der Konvertierung in 16960,

Es war mir nicht klar, daher denke ich, dass es sich lohnt, darauf hinzuweisen: 16960 ist das Ergebnis der Konvertierung von 1000000.0 in eine ausreichend breite Ganzzahl und des anschließenden Abschneidens, um die 16 unteren Bits beizubehalten.

Dies ist ~ keine Option, die zuvor in diesem Thread vorgeschlagen wurde, und es ist ~ (Bearbeiten: Ich habe mich hier geirrt, sorry, dass ich sie nicht gefunden habe) auch nicht das aktuelle Verhalten. Das aktuelle Verhalten in Rust ist, dass die Konvertierung von Float in Integer außerhalb des Bereichs undefiniertes Verhalten ist. In der Praxis führt dies häufig zu einem Müllwert, der im Prinzip zu Fehlkompilierungen und Schwachstellen führen kann. In diesem Thread geht es darum, das zu beheben. Wenn ich das folgende Programm in Rust 1.39.0 ausführe, erhalte ich jedes Mal einen anderen Wert:

fn main() {
    dbg!(1000000.0 as u16);
}

Spielplatz . Beispielausgabe:

[src/main.rs:2] 1000000.0 as u16 = 49072

Ich persönlich denke, eine ganzzahlige Kürzung ist nicht besser oder schlechter als eine Sättigung. Beide Werte sind für Werte außerhalb des Bereichs numerisch falsch. Eine unfehlbare Bekehrung hat ihren Platz, solange sie deterministisch und nicht UB ist. Möglicherweise wissen Sie bereits aus Ihrem Algorithmus, dass die Werte im Bereich liegen, oder kümmern sich möglicherweise nicht um solche Fälle.

Ich denke, wir sollten auch fehlbare Konvertierungs-APIs hinzufügen, die Result , aber ich muss den Entwurf dieses Pre-RFC noch fertig schreiben :)

Die „convert mathematischen integer, trunkieren dann an Zielbreite“ oder „wraparound“ Semantik vor (https://github.com/rust-lang/rust/issues/10184#issuecomment-299229143) in diesem Thread vorgeschlagen. Ich mag es nicht besonders:

  • Ich denke, es ist etwas weniger sinnvoll als die Sättigung. Die Sättigung liefert im Allgemeinen keine vernünftigen Ergebnisse für Zahlen, die weit außerhalb des Bereichs liegen, aber:

    • Es verhält sich vernünftiger als ein Umlauf, wenn die Zahlen leicht außerhalb des Bereichs liegen (z. B. aufgrund eines akkumulierten Rundungsfehlers). Im Gegensatz dazu kann eine Umwandlung, die sich umgibt, einen leichten Rundungsfehler in der Float-Berechnung auf den maximal möglichen Fehler in der Ganzzahldomäne verstärken.

    • Es wird häufig in der digitalen Signalverarbeitung verwendet, daher gibt es zumindest einige Anwendungen, bei denen es tatsächlich erwünscht ist. Im Gegensatz dazu kenne ich keinen einzigen Algorithmus, der von der Umlaufsemantik profitiert.

  • AFAIK Der einzige Grund, die umlaufende Semantik zu
  • Während die Frage, was für NaN zu tun ist, ein hässliches Problem für jede Konvertierung in eine Ganzzahl ist, erfordert die Sättigung zumindest kein spezielles Gehäuse (weder in der Semantik noch in den meisten Implementierungen) für unendlich. Aber für das Wraparound, was ist das ganzzahlige Äquivalent, das +/- unendlich sein soll? JavaScript sagt, es ist 0, und ich nehme an, wenn wir auf NaN as Panik machen, dann könnte es auch auf Unendlich in Panik geraten, aber so oder so scheint es, als würde es schwieriger sein, Wraparound schnell zu machen, als normale und denormale Zahlen zu betrachten allein würde vorschlagen.

Ich vermute, dass der größte Teil des Codes, der durch die Sättigungssemantik für die Konvertierung zurückgegangen ist, mit SIMD besser geeignet wäre. Obwohl dies unglücklich ist, verhindert diese Änderung nicht, dass Hochleistungscode geschrieben wird (insbesondere wenn intrinsische Inhalte mit unterschiedlicher Semantik bereitgestellt werden), und kann sogar einige Projekte zu einer schnelleren (wenn auch weniger portablen) Implementierung führen.

In diesem Fall sollten einige geringfügige Leistungsrückgänge nicht als Rechtfertigung verwendet werden, um das Schließen eines Schalllochs zu vermeiden.

https://github.com/rust-lang/rust/pull/66841 fügt unsafe fn Methoden hinzu, die mit LLVMs fptoui und fptosi konvertiert werden, wenn Werte bekannt sind In Reichweite und Sättigung zu sein, ist eine messbare Leistungsregression.

Nachdem das gelandet ist, denke ich, ist es in Ordnung, die Standardeinstellung für as (und vielleicht ein weiteres -Z Flag hinzuzufügen, um sich abzumelden?), Obwohl dies wahrscheinlich eine formelle Entscheidung des Lang-Teams sein sollte.

Nach diesen Landungen denke ich, dass es in Ordnung ist, die Standardeinstellung für as (und möglicherweise ein weiteres -Z -Flag hinzuzufügen, um sich abzumelden?), Obwohl dies wahrscheinlich eine formelle Entscheidung des Lang-Teams sein sollte.

Also haben wir (Sprachteam, zumindest mit den Leuten, die dort waren) dies in https://github.com/rust-lang/lang-team/blob/master/minutes/2019-11-21.md besprochen und wir dachten Das Hinzufügen neuer Intrinsics + das Hinzufügen von -Zunsaturated-float-casts wäre ein guter erster Schritt.

Ich denke, es wäre gut, die Standardeinstellung als Teil davon oder kurz danach zu ändern, möglicherweise mit FCP, falls erforderlich.

Ich gehe davon aus, dass Sie mit neuen Eigenschaften so etwas wie https://github.com/rust-lang/rust/pull/66841 meinen

Was bedeutet es, -Z unsaturated-float-casts hinzuzufügen, ohne die Standardeinstellung zu ändern? Akzeptieren Sie es als No-Op, anstatt "Fehler: Unbekannte Debugging-Option" auszugeben?

Ich gehe davon aus, dass Sie mit neuen Eigenheiten so etwas wie # 66841 meinen

Ja 👍 - danke, dass du das angeführt hast.

Was bedeutet es, -Z unsaturated-float-casts hinzuzufügen, ohne die Standardeinstellung zu ändern? Akzeptieren Sie es als No-Op, anstatt "Fehler: Unbekannte Debugging-Option" auszugeben?

Ja im Grunde. Alternativ entfernen wir -Z saturated-float-casts zugunsten von -Z unsaturated-float-casts und wechseln den Standard direkt, aber es sollte über weniger PRs zum gleichen Ergebnis führen.

Ich verstehe den "ungesättigten" Vorschlag wirklich nicht. Wenn das Ziel nur darin besteht, einen Knopf zum Deaktivieren der neuen Standardeinstellung bereitzustellen, ist es einfacher, die Standardeinstellung des vorhandenen Flags zu ändern und nichts weiter zu tun. Wenn das Ziel darin besteht, einen neuen Namen zu wählen, der klarer über den Kompromiss (Unklarheit) ist, dann ist "ungesättigt" schrecklich - ich würde stattdessen einen Namen vorschlagen, der "unsicher" oder "UB" oder einen ähnlichen Namen enthält gruseliges Wort, zum Beispiel -Z fix-float-cast-ub .

unchecked ist der Begriff mit einem Präzedenzfall in API-Namen.

@admilazz Wir sind durch die

Vermutlich können Sie Laufzeitprüfungen jedoch nur in Debug-Builds hinzufügen, wie dies bei einem Ganzzahlüberlauf der Fall ist.

AFAIK Der einzige Grund, umlaufende Semantik zu bevorzugen, ist die Effizienz der Software-Emulation

Ich denke nicht, dass wir entweder Wraparound oder Sättigung bevorzugen sollten, da beide falsch sind, aber Wraparound hat zumindest den Vorteil, dass es die Methode ist, die von vielen rostähnlichen Sprachen verwendet wird: C / C ++, C #, go, wahrscheinlich D und sicher mehr und auch das aktuelle Verhalten von Rost zu sein (zumindest manchmal). Trotzdem denke ich, dass "Panik bei ungültigen Konvertierungen (möglicherweise nur in Debug-Builds)" ideal ist, genau wie wir es für einen Ganzzahlüberlauf und eine ungültige Arithmetik wie die Division durch Null tun.

(Interessanterweise habe ich 16960 auf dem Spielplatz bekommen . Aber ich sehe aus anderen Beispielen, dass Rost es manchmal anders macht ...)

Die Sättigung liegt daran, dass die Sprache als Casts definiert wird, um immer erfolgreich zu sein. Daher können wir den Operator nicht stattdessen in Panik ändern.

Das Ändern der Bewertung der Operation ist bereits eine bahnbrechende Änderung, sofern wir uns um die Ergebnisse der Personen kümmern, die dies bereits tun. Auch dieses panikfreie Verhalten könnte sich ändern.

Ich nehme an, wenn wir NaN in Panik versetzen, könnte es auch im Unendlichen in Panik geraten, aber so oder so scheint es, als würde es das Umwickeln schwieriger machen, schnell zu machen

Wenn es nur in Debug-Builds aktiviert ist, wie es der Integer-Überlauf ist, können wir meiner Meinung nach das Beste aus beiden Welten herausholen: Konvertierungen sind garantiert korrekt (in Debug-Builds), Benutzerfehler werden eher abgefangen, Sie können sich anmelden zu seltsamen Verhaltensweisen wie Umlauf und / oder Sättigung, wenn Sie möchten, und die Leistung ist so gut wie möglich.

Es scheint auch seltsam, dieses Zeug über einen Befehlszeilenschalter zu steuern. Das ist ein großer Hammer. Sicherlich hängt das gewünschte Verhalten einer Konvertierung außerhalb des Bereichs von den Besonderheiten des Algorithmus ab, daher sollte dies pro Konvertierung gesteuert werden. Ich würde vorschlagen, f.to_u16_sat () und f.to_u16_wrap () oder ähnliches wie die Opt-Ins zu verwenden und keine Befehlszeilenoption zu haben, die die Semantik des Codes ändert. Das würde es schwierig machen, verschiedene Codeteile zu mischen und abzugleichen, und Sie können nicht verstehen, was etwas tut, wenn Sie es lesen ...

Und wenn es wirklich inakzeptabel ist, "Panik, wenn ungültig" zum Standardverhalten zu machen, wäre es schön, eine intrinsische Methode zu haben, die es implementiert, aber nur die Gültigkeitsprüfung in Debug-Builds durchführt, damit wir sicherstellen können, dass unsere Konvertierungen im (riesigen) korrekt sind Die meisten?) Fälle, in denen wir erwarten, dass wir nach der Konvertierung die gleiche Anzahl erhalten, ohne jedoch eine Strafe für Release-Builds zu zahlen.

Interessanterweise habe ich 16960 auf dem Spielplatz bekommen.

So funktioniert undefiniertes Verhalten: Abhängig von der genauen Formulierung des Programms und der genauen Compilerversion sowie den genauen Kompilierungsflags erhalten Sie möglicherweise deterministisches Verhalten oder einen Garbage-Wert, der sich bei jedem Lauf ändert, oder Fehlkompilierungen. Der Compiler darf alles machen.

Wraparound hat zumindest den Vorteil, dass es die Methode ist, die von vielen rostähnlichen Sprachen verwendet wird: C / C ++, C #, go, wahrscheinlich D und sicherlich mehr.

Tut es wirklich? Zumindest nicht in C und C ++ haben sie das gleiche undefinierte Verhalten wie Rust. Dies ist kein Zufall, wir verwenden LLVM, das hauptsächlich für die Clang-Implementierung von C und C ++ entwickelt wurde. Sind Sie sicher über C # und gehen?

C11-Standard https://port70.net/~nsz/c/c11/n1570.html#6.3.1.4

Wenn ein endlicher Wert vom realen schwebenden Typ in einen anderen ganzzahligen Typ als _Bool konvertiert wird, wird der Bruchteil verworfen (dh der Wert wird gegen Null abgeschnitten). Wenn der Wert des Integralteils nicht durch den Integer-Typ dargestellt werden kann, ist das Verhalten undefiniert.

Die verbleibende Operation, die ausgeführt wird, wenn ein Wert vom Typ Integer in einen Typ ohne Vorzeichen konvertiert wird, muss nicht ausgeführt werden, wenn ein Wert vom Typ Real Floating in einen Typ ohne Vorzeichen konvertiert wird. Somit ist der Bereich der tragbaren realen schwebenden Werte (-1, Utype_MAX + 1).

C ++ 17-Standard http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/n4659.pdf#section.7.10

Ein Wert eines Gleitkommatyps kann in einen Wert eines ganzzahligen Typs konvertiert werden. Die Konvertierung wird abgeschnitten, dh der Bruchteil wird verworfen. Das Verhalten ist undefiniert, wenn der abgeschnittene Wert im Zieltyp nicht dargestellt werden kann.

C # -Referenz https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/numeric-conversions

Wenn Sie einen Double- oder Float-Wert in einen Integraltyp konvertieren, wird dieser Wert auf den nächsten Integralwert auf Null gerundet. Wenn der resultierende Integralwert außerhalb des Bereichs des Zieltyps liegt, hängt das Ergebnis vom Kontext der Überlaufprüfung ab. In einem aktivierten Kontext wird eine OverflowException ausgelöst, während in einem nicht aktivierten Kontext das Ergebnis ein nicht angegebener Wert des Zieltyps ist.

Es ist also nicht UB, sondern nur ein "nicht spezifizierter Wert".

@admilazz Es gibt einen großen Unterschied zwischen diesem und dem Integer-Überlauf: Der Integer-Überlauf ist unerwünscht, aber gut definiert . Gleitkomma-Casts sind undefiniertes Verhalten .

Was Sie verlangen, ähnelt dem Deaktivieren der Überprüfung von Vec -Begrenzungen im Release-Modus, aber das wäre falsch, da dies undefiniertes Verhalten zulassen würde.

Das Zulassen von undefiniertem Verhalten in sicherem Code ist nicht akzeptabel, selbst wenn dies nur im Freigabemodus geschieht. Daher muss jeder Fix sowohl für den Release- als auch für den Debug-Modus gelten.

Natürlich ist es möglich, im Debug-Modus eine restriktivere Korrektur vorzunehmen, aber die Korrektur für den Release-Modus muss noch genau definiert sein.

@admilazz Es gibt einen großen Unterschied zwischen diesem und dem Integer-Überlauf: Der Integer-Überlauf ist unerwünscht, aber gut definiert. Gleitkomma-Casts sind undefiniertes Verhalten.

Sicher, aber in diesem Thread geht es darum, das Verhalten zu definieren. Wenn definiert würde, dass "ein nicht spezifizierter Wert des Zieltyps" erzeugt wird, wie in der C # -Spezifikation, auf die Amanieu oben hilfreich verwiesen hat, wäre es nicht länger undefiniert (auf gefährliche Weise). Sie können die wohldefinierte Natur des Integer-Überlaufs in praktischen Programmen nicht einfach nutzen, da er bei Debug-Builds immer noch in Panik gerät. In ähnlicher Weise muss der Wert, der durch eine ungültige Besetzung in Release-Builds erzeugt wird, nicht vorhersehbar oder besonders nützlich sein, da Programme ihn ohnehin praktisch nicht nutzen könnten, wenn er in Debug-Builds in Panik gerät. Dies gibt dem Compiler tatsächlich maximalen Spielraum für Optimierungen, während die Auswahl eines Verhaltens wie Sättigung den Compiler einschränkt und auf Hardware ohne native Sättigungskonvertierungsanweisungen erheblich langsamer sein kann. (Und es ist nicht so, dass die Sättigung eindeutig korrekt ist.)

Was Sie verlangen, ähnelt dem Deaktivieren der Überprüfung von Vec-Grenzen im Freigabemodus, aber das wäre falsch, da dies undefiniertes Verhalten zulassen würde. Das Zulassen von undefiniertem Verhalten in sicherem Code ist nicht akzeptabel ...

Nicht jedes undefinierte Verhalten ist gleich. Undefiniertes Verhalten bedeutet nur, dass der Compiler-Implementierer entscheiden muss, was passiert. Solange es keine Möglichkeit gibt, die Sicherheitsgarantien von Rost zu verletzen, indem ein Float auf einen Int geworfen wird, ist es meiner Meinung nach nicht so, als würde man Leuten erlauben, an beliebige Speicherorte zu schreiben. Trotzdem stimme ich natürlich zu, dass es in dem Sinne definiert werden sollte, dass es sicher ist, auch wenn es nicht unbedingt vorhersehbar ist.

Tut es wirklich? Zumindest nicht in C und C ++, sie haben das gleiche undefinierte Verhalten wie Rust ... Sind Sie sich über C # sicher und gehen Sie?

Fair genug. Ich habe nicht alle Spezifikationen gelesen. Ich habe gerade verschiedene Compiler getestet. Sie haben Recht, dass die Aussage "Alle Compiler, die ich auf diese Weise ausprobiert habe" sich von der Aussage "Die Sprachspezifikationen definieren es so" unterscheidet. Aber ich plädiere sowieso nicht für einen Überlauf, sondern weise nur darauf hin, dass dies am häufigsten vorkommt. Ich plädiere wirklich für eine Konvertierung, die 1) vor "falschen" Ergebnissen wie 1000000.0 schützt, die 65535 oder 16960 werden, aus dem gleichen Grund, aus dem wir vor einem Ganzzahlüberlauf schützen - es ist höchstwahrscheinlich ein Fehler, daher sollten Benutzer sich dafür entscheiden müssen , und 2) ermöglicht maximale Leistung bei Release-Builds.

Nicht jedes undefinierte Verhalten ist gleich. Undefiniertes Verhalten bedeutet nur, dass der Compiler-Implementierer entscheiden muss, was passiert. Solange es keine Möglichkeit gibt, die Sicherheitsgarantien von Rost zu verletzen, indem ein Float auf einen Int geworfen wird, ist es meiner Meinung nach nicht so, als würde man Leuten erlauben, an beliebige Speicherorte zu schreiben. Trotzdem stimme ich natürlich zu, dass es definiert werden sollte: definiert, aber nicht unbedingt vorhersehbar.

Undefiniertes Verhalten bedeutet, dass die Optimierer (die von LLVM-Entwicklern bereitgestellt werden, die sich auf C und C ++ konzentrieren) davon ausgehen können, dass dies niemals passieren kann, und den Code basierend auf dieser Annahme transformieren können, einschließlich des Löschens von Codeblöcken, die nur durch Durchlaufen der undefinierten Besetzung erreichbar sind oder, wie dieses Beispiel zeigt, unter der Annahme , dass eine Zuordnung genannt worden sein, obwohl es eigentlich nicht, weil Code aufgerufen wird, die ohne zuerst aufgerufen zu ruft wäre es nicht definiertes Verhalten sein.

Auch wenn es sinnvoll war zu beweisen , dass die verschiedene Optimierungs Komponieren geht nicht gefährlich emergentes Verhalten produziert, wird der LLVM - Entwickler keine bewusste Anstrengung , um das zu bewahren.

Ich würde argumentieren , dass alle undefinierten Verhalten auf dieser Basis gleich ist.

Selbst wenn es vernünftig wäre zu beweisen, dass das Verfassen der verschiedenen Optimierungsdurchläufe keine gefährlichen Verhaltensweisen hervorruft, werden die LLVM-Entwickler keine bewussten Anstrengungen unternehmen, um dies zu bewahren.

Nun, es ist bedauerlich, dass LLVM auf diese Weise das Rostdesign beeinflusst, aber ich habe gerade einige der LLVM-Anweisungsreferenzen gelesen und es wird die oben erwähnte "Einfrier" -Operation erwähnt ("... eine andere besteht darin, darauf zu warten, dass LLVM ein Einfrieren hinzufügt Konzept… "), das undefiniertes Verhalten auf LLVM-Ebene verhindern würde. Ist Rost an eine alte Version von LLVM gebunden? Wenn nicht, könnten wir es benutzen. Ihre Dokumentation ist jedoch unklar über das genaue Verhalten.

Wenn das Argument undef oder giftig ist, gibt 'freeze' einen beliebigen, aber festen Wert vom Typ 'ty' zurück. Andernfalls ist diese Anweisung ein No-Op und gibt das Eingabeargument zurück. Bei allen Verwendungen eines Werts, der von derselben Anweisung zum Einfrieren zurückgegeben wird, wird garantiert immer derselbe Wert eingehalten, während unterschiedliche Anweisungen zum Einfrieren unterschiedliche Werte ergeben können.

Ich weiß nicht, was sie unter "fester Wert" oder "derselben 'Einfrier'-Anweisung" verstehen. Ich denke, im Idealfall würde es zu einem No-Op kompiliert und eine unvorhersehbare Ganzzahl ergeben, aber es klingt so, als würde es möglicherweise etwas Teuereres bewirken. Hat jemand diese Einfrieroperation versucht?

Nun, es ist bedauerlich, dass LLVM auf diese Weise das Design von Rost beeinflusst

Es ist nicht nur so, dass LLVM-Entwickler die Optimierer schreiben. Selbst wenn die Entwickler von rustc die Optimierer geschrieben haben, ist das Flirten mit Undefiniertheit aufgrund der neuen Eigenschaften der Verkettung von Optimierern von Natur aus eine große Waffe. Das menschliche Gehirn hat sich einfach nicht entwickelt, um "die potenzielle Größe des Rundungsfehlers zu verstehen", wenn es sich bei der fraglichen Rundung um ein emergentes Verhalten handelt, das durch Verketten von Optimierungsdurchläufen aufgebaut wird.

Ich werde dir dort nicht widersprechen. :-) Ich hoffe, dass diese LLVM-Anweisung zum Einfrieren eine kostengünstige Möglichkeit bietet, dieses undefinierte Verhalten zu vermeiden.

Dies wurde oben diskutiert und die Schlussfolgerung war, dass das Gießen und dann das Einfrieren zwar ein definiertes Verhalten ist, aber keineswegs ein vernünftiges Verhalten. Im Release-Modus würden solche Casts beliebige Ergebnisse für nicht gebundene Eingaben zurückgeben (in völlig sicherem Code). Das ist keine gute Semantik für etwas so unschuldig aussehendes wie as .

IMO eine solche Semantik wäre schlechtes Sprachdesign, das wir lieber vermeiden würden.

Mein Standpunkt ist, dass as unabhängig von diesem Problem am besten vollständig vermieden wird, da es je nach Eingabe- und Ausgabetyp eine sehr unterschiedliche Semantik (Abschneiden, Sättigen, Runden usw.) haben kann, und diese sind nicht immer offensichtlich, wenn Code lesen. (Sogar letzteres kann mit foo as _ .) Ich habe also einen Entwurf für einen Pre-RFC, in dem verschiedene explizit benannte Konvertierungsmethoden vorgeschlagen werden, die die Fälle abdecken, die as heute (und möglicherweise mehr) tut. .

Ich habe diesen Entwurf fertiggestellt! https://internals.rust-lang.org/t/pre-rfc-add-explicitly-named-numeric-conversion-apis/11395

Jedes Feedback ist sehr willkommen, aber bitte geben Sie es in den internen Threads und nicht hier.

Im Release-Modus würden solche Casts beliebige Ergebnisse für nicht gebundene Eingaben zurückgeben (in völlig sicherem Code). Das ist keine gute Semantik für etwas so unschuldig aussehendes wie.

Es tut mir leid, mich zu wiederholen, aber ich denke, dasselbe Argument gilt für den Ganzzahlüberlauf. Wenn Sie einige Zahlen multiplizieren und das Ergebnis überläuft, erhalten Sie ein völlig falsches Ergebnis, das mit ziemlicher Sicherheit die von Ihnen versuchte Berechnung ungültig macht, aber bei Debug-Builds in Panik gerät und der Fehler wahrscheinlich abgefangen wird. Ich würde sagen, dass eine numerische Konvertierung, die völlig falsche Ergebnisse liefert, ebenfalls in Panik geraten sollte, da die Wahrscheinlichkeit sehr hoch ist, dass sie einen Fehler im Code des Benutzers darstellt. (Der Fall einer typischen Gleitkomma-Ungenauigkeit wird bereits behandelt. Wenn eine Berechnung 65535.3 ergibt, ist es bereits gültig, diese in u16 zu konvertieren. Um eine Konvertierung außerhalb der Grenzen zu erhalten, benötigen Sie normalerweise einen Fehler in Ihrem Code, und wenn ich einen habe Fehler Ich möchte benachrichtigt werden, damit ich ihn beheben kann.)

Die Fähigkeit von Release-Builds, willkürliche, aber definierte Ergebnisse für ungültige Konvertierungen zu liefern, ermöglicht auch maximale Leistung, was meiner Meinung nach für etwas so Grundlegendes wie numerische Konvertierungen wichtig ist. Immer gesättigt zu sein hat erhebliche Auswirkungen auf die Leistung, verbirgt Fehler und führt selten eine Berechnung durch, bei der unerwartet das richtige Ergebnis erzielt wird.

Es tut mir leid, mich zu wiederholen, aber ich denke, dasselbe Argument gilt für den Ganzzahlüberlauf. Wenn Sie einige Zahlen multiplizieren und das Ergebnis überläuft, erhalten Sie ein völlig falsches Ergebnis, das die Berechnung, die Sie durchführen wollten, mit ziemlicher Sicherheit ungültig macht

Wir sprechen jedoch nicht über Multiplikation, sondern über Casts. Und ja, dasselbe gilt für den Ganzzahlüberlauf: Int-to-Int-Casts geraten auch dann nicht in Panik, wenn sie überlaufen. Dies liegt daran, dass as von Natur aus nie in Panik gerät, auch nicht in Debug-Builds. Eine Abweichung davon für Gleitkomma-Casts ist im besten Fall überraschend und im schlimmsten Fall gefährlich, da die Richtigkeit und Sicherheit von unsicherem Code von bestimmten Vorgängen abhängen kann, die nicht in Panik geraten.

Wenn Sie argumentieren möchten, dass das Design von as fehlerhaft ist, weil es eine unfehlbare Konvertierung zwischen Typen ermöglicht, bei denen eine ordnungsgemäße Konvertierung nicht immer möglich ist, werden die meisten von uns dem zustimmen. Für diesen Thread, bei dem es darum geht, Float-zu-Int-Konvertierungen innerhalb des vorhandenen Frameworks von as Casts zu korrigieren, ist dies jedoch völlig ausgeschlossen. Diese müssen unfehlbar sein, sie dürfen nicht in Panik geraten, auch nicht in Debug-Builds. Schlagen Sie also entweder eine vernünftige (ohne freeze ), nicht panische Semantik für Float-to-Int-Casts vor, oder versuchen Sie, eine neue Diskussion über die Neugestaltung von as zu beginnen, um Panik zuzulassen Wenn die Besetzung verlustbehaftet ist (und dies konsequent für Int-to-Int- und Float-to-Int-Casts), aber letzteres in dieser Ausgabe nicht zum Thema gehört, öffnen Sie bitte einen neuen Thread (Pre-RFC-Stil). dafür.

Wie wäre es, wenn wir jetzt nur die freeze -Semantik implementieren, um die UB zu reparieren, und dann können wir uns weltweit auf die Semantik einigen, die wir tatsächlich wollen, da jede von uns gewählte Semantik abwärtskompatibel mit freeze Semantik.

Wie wäre es, wenn wir zunächst freeze semantics _now_ implementieren, um die UB zu reparieren, und dann können wir uns weltweit auf die Semantik einigen, die wir tatsächlich wollen, da jede von uns gewählte Semantik abwärtskompatibel mit freeze Semantik.

  1. Panik ist nicht abwärtskompatibel mit Einfrieren, daher müssten wir zumindest alle Vorschläge ablehnen, die Panik beinhalten. Der Wechsel von UB in Panik ist weniger offensichtlich inkompatibel, obwohl es, wie oben erläutert, einige andere Gründe gibt, as Panik zu versetzen.
  2. Wie ich vorher schrieb ,
    > Wir unterstützen mehrere ältere Versionen (derzeit zurück zu LLVM 6) und benötigen eine Fallback-Implementierung, damit die UB für alle Benutzer tatsächlich entfernt wird.

Ich stimme @RalfJung zu , dass es höchst unerwünscht ist, nur einige as Casts in Panik zu versetzen , aber abgesehen davon denke ich nicht, dass dieser Punkt, den

(Der Fall einer typischen Gleitkomma-Ungenauigkeit wird bereits behandelt. Wenn eine Berechnung 65535.3 ergibt, ist es bereits gültig, diese in u16 zu konvertieren. Um eine Konvertierung außerhalb der Grenzen zu erhalten, benötigen Sie normalerweise einen Fehler in Ihrem Code, und wenn ich einen habe Fehler Ich möchte benachrichtigt werden, damit ich ihn beheben kann.)

Für f32-> u16 mag es zutreffen, dass Sie einen außerordentlich großen Rundungsfehler benötigen, um den u16-Bereich nur aufgrund eines Rundungsfehlers zu verlassen, aber für Konvertierungen von f32 in 32-Bit-Ganzzahlen ist dies nicht so offensichtlich. i32::MAX ist in f32 nicht genau darstellbar, die nächste darstellbare Zahl ist 47 von i32::MAX . Wenn Sie also eine Berechnung haben, die mathematisch zu einer Zahl von bis zu i32::MAX , werden Sie durch jeden Fehler> = 1 ULP von Null entfernt. Und es wird viel schlimmer, wenn wir Floats mit geringerer Genauigkeit betrachten (IEEE 754 binary16 oder das nicht standardmäßige bfloat16).

Wir sprechen jedoch nicht über Multiplikation, sondern über Casts

Nun, Gleitkomma-Ganzzahl-Konvertierungen werden fast ausschließlich im selben Kontext wie Multiplikation verwendet: numerische Berechnungen, und ich denke, es gibt eine nützliche Parallele zum Verhalten des Ganzzahlüberlaufs.

Und ja, dasselbe gilt für den Ganzzahlüberlauf: Int-to-Int-Casts geraten nie in Panik, selbst wenn sie überlaufen ... Abweichungen davon sind für Gleitkomma-Casts im besten Fall überraschend und im schlimmsten Fall gefährlich, da die Richtigkeit und Sicherheit von unsicherem Code kann von bestimmten Operationen abhängen, die nicht in Panik geraten.

Ich würde argumentieren, dass die Inkonsistenz hier durch gängige Praxis gerechtfertigt ist und nicht so überraschend wäre. Das Abschneiden und Zerlegen von Ganzzahlen mit Verschiebungen, Masken und Casts - effektives Verwenden von Casts als Form des bitweisen UND plus einer Größenänderung - ist sehr verbreitet und hat eine lange Geschichte in der Systemprogrammierung. Das mache ich mindestens mehrmals in der Woche. Aber in den letzten mehr als 30 Jahren kann ich mich nicht erinnern, jemals ein vernünftiges Ergebnis erwartet zu haben, wenn NaN, Infinity oder ein Gleitkommawert außerhalb des Bereichs in eine Ganzzahl umgewandelt werden. (Jede Instanz, an die ich mich erinnern kann, war ein Fehler in der Berechnung, die den Wert erzeugt hat.) Daher denke ich nicht, dass der Fall von Ganzzahl-> Ganzzahl-Casts und Gleitkomma-Casts -> Integer-Casts identisch behandelt werden muss. Trotzdem kann ich verstehen, dass einige Entscheidungen bereits in Stein gemeißelt wurden.

Bitte… schlagen Sie eine vernünftige (nicht einfrierende), nicht panische Semantik für Float-to-Int-Casts vor

Mein Vorschlag lautet:

  1. Verwenden Sie keine globalen Kompilierungsschalter, die wesentliche Änderungen an der Semantik bewirken. (Ich gehe davon aus, dass -Zsaturating-float-casts ein Befehlszeilenparameter oder ähnliches ist.) Code, der beispielsweise vom Sättigungsverhalten abhängt, würde beschädigt, wenn er ohne ihn kompiliert würde. Vermutlich konnte Code mit unterschiedlichen Erwartungen nicht im selben Projekt gemischt werden. Es sollte eine lokale Möglichkeit für eine Berechnung geben, die gewünschte Semantik anzugeben, wahrscheinlich so etwas wie diese Vor-RFC .
  2. Make as Casts haben standardmäßig die maximale Leistung, wie es von einem Cast erwartet wird.

    • Ich denke, dies sollte über das Einfrieren von LLVM-Versionen erfolgen, die dies unterstützen, sowie über jede andere Konvertierungssemantik bei LLVM-Versionen, die dies nicht tun (z. B. Abschneiden, Sättigen usw.). Ich gehe davon aus, dass die Behauptung "Einfrieren könnte Werte aus dem sensiblen Speicher verlieren" rein hypothetisch ist. (Oder wenn y = freeze(fptosi(x)) lediglich y unverändert lässt und somit nicht initialisierter Speicher verloren geht, kann dies behoben werden, indem zuerst y gelöscht wird.)

    • Wenn as standardmäßig relativ langsam ist (z. B. weil es gesättigt ist), geben Sie eine Möglichkeit an, um maximale Leistung zu erzielen (z. B. eine Methode - wenn nötig unsicher -, die das Einfrieren verwendet).

  1. Verwenden Sie keine globalen Kompilierungsschalter, die wesentliche Änderungen an der Semantik bewirken. (Ich gehe davon aus, dass -Zsaturating-float-casts ein Befehlszeilenparameter oder ähnliches ist.)

Um es klar zu sagen, ich glaube nicht, dass jemand anderer Meinung ist. Dieses Flag wurde immer nur als kurzfristiges Tool vorgeschlagen, um Leistungsregressionen einfacher zu messen und zu umgehen, während Bibliotheken aktualisiert werden, um diese Regressionen zu beheben.

Für f32-> u16 mag es zutreffen, dass Sie einen außerordentlich großen Rundungsfehler benötigen, um den u16-Bereich nur aufgrund eines Rundungsfehlers zu verlassen, aber für Konvertierungen von f32 in 32-Bit-Ganzzahlen ist dies nicht so offensichtlich. i32 :: MAX ist in f32 nicht genau darstellbar, die am nächsten darstellbare Zahl ist 47 von i32 :: MAX entfernt. Wenn Sie also eine Berechnung haben, die mathematisch zu einer Zahl bis zu i32 :: MAX führen sollte, werden Sie durch jeden Fehler> = 1 ULP von Null entfernt

Dies wird ein wenig vom Thema abweichen, aber nehmen wir an, Sie haben diesen hypothetischen Algorithmus, der mathematisch f32s bis zu 2 ^ 31-1 erzeugen soll (aber _nicht_ 2 ^ 31 oder höher erzeugen sollte, außer möglicherweise aufgrund eines Rundungsfehlers). Es scheint bereits fehlerhaft zu sein.

  1. Ich denke, der am nächsten darstellbare i32 liegt tatsächlich 127 unter i32 :: MAX. Selbst in einer perfekten Welt ohne Gleitkomma-Ungenauigkeit kann der Algorithmus, von dem Sie erwarten, dass er Werte bis zu 2 ^ 31-1 erzeugt, tatsächlich nur (legal) erzeugen ) Werte bis 2 ^ 31-128. Vielleicht ist das schon ein Fehler. Ich bin mir nicht sicher, ob es sinnvoll ist, über Fehler zu sprechen, die von 2 ^ 31-1 gemessen werden, wenn diese Zahl nicht dargestellt werden kann. Sie müssten um 64 von der nächsten darstellbaren Zahl abweichen (unter Berücksichtigung der Rundung), um die Grenzen zu überschreiten. Zugegeben, das ist nicht viel prozentual, wenn Sie sich 2 ^ 32 nähern.
  2. Sie sollten keine Unterscheidung von Werten erwarten, die 1 voneinander entfernt sind (dh 2 ^ 31-1, aber nicht 2 ^ 31), wenn die am nächsten darstellbaren Werte 128 voneinander entfernt sind. Darüber hinaus sind nur 3,5% der i32s als f32 (und <2% der u32s) darstellbar. Sie können diese Reichweite nicht erreichen, während Sie mit einem f32 diese Präzision haben. Der Algorithmus scheint das falsche Werkzeug für den Job zu verwenden.

Ich nehme an, dass jeder praktische Algorithmus, der das tut, was Sie beschreiben, irgendwie eng mit ganzen Zahlen verbunden ist. Wenn Sie beispielsweise einen zufälligen i32 in f32 und zurück konvertieren, kann dies fehlschlagen, wenn er über i32 :: MAX-64 liegt. Aber das verschlechtert Ihre Präzision erheblich und ich weiß nicht, warum Sie so etwas tun würden. So ziemlich jede i32 -> f32 -> i32-Berechnung, die den gesamten i32-Bereich ausgibt, kann mit ganzzahliger Mathematik schneller und genauer ausgedrückt werden, und wenn nicht, gibt es f64.

Obwohl ich sicher bin, dass es möglich ist, einige Fälle zu finden, in denen Algorithmen, die Konvertierungen außerhalb der Grenzen durchführen, durch Sättigung behoben werden, denke ich, dass sie selten sind - selten genug, dass wir nicht alle Konvertierungen verlangsamen sollten, um sie zu berücksichtigen . Und ich würde argumentieren, dass solche Algorithmen wahrscheinlich immer noch fehlerhaft sind und behoben werden sollten. Und wenn ein Algorithmus nicht repariert werden kann, kann er vor der Konvertierung außerhalb der Grenzen immer eine Überprüfung der Grenzen durchführen (oder eine Sättigungskonvertierungsfunktion aufrufen). Auf diese Weise werden die Kosten für die Begrenzung des Ergebnisses nur bei Bedarf bezahlt.

PS Verspätetes Happy Thanksgiving an alle.

Um es klar zu sagen, ich glaube nicht, dass jemand anderer Meinung ist. Diese Flagge wurde immer nur als kurzfristiges Werkzeug vorgeschlagen ...

Ich bezog mich hauptsächlich auf den Vorschlag, -Zsättigte-Float-Casts durch -Zunsättigte-Float-Casts zu ersetzen. Selbst wenn die Sättigung zur Standardeinstellung wird, scheinen Flags wie -Zunsättigt-Float-Casts aus Kompatibilitätsgründen schlecht zu sein, aber wenn sie auch vorübergehend sein sollen, ist das in Ordnung, egal. :-)

Wie auch immer, ich bin sicher, jeder hofft, dass ich genug zu diesem Thema gesagt habe - ich selbst eingeschlossen. Ich weiß, dass das Rust-Team traditionell versucht hat, mehrere Möglichkeiten bereitzustellen, damit die Menschen ihre eigenen Entscheidungen zwischen Leistung und Sicherheit treffen können. Ich habe meine Perspektive geteilt und vertraue darauf, dass ihr am Ende eine gute Lösung finden werdet. Pass auf!

Ich nahm an, dass -Zunsaturated-float-casts nur vorübergehend existieren und irgendwann entfernt werden würde. Dass es sich um eine -Z -Option handelt (nur bei Nightly verfügbar) und nicht um -C legt dies zumindest nahe.

Für das, was es wert ist, sind Sättigung und UB nicht die einzigen Möglichkeiten. Eine andere Möglichkeit besteht darin, LLVM so zu ändern, dass eine Variante von fptosi hinzugefügt wird, die das native Überlaufverhalten der CPU verwendet. Das Verhalten beim Überlauf wäre also nicht über Architekturen hinweg portierbar, aber für jede Architektur gut definiert ( zB 0x80000000 auf x86 zurückgeben), und es würde niemals Gift oder nicht initialisierten Speicher zurückgeben. Selbst wenn die Standardeinstellung gesättigt wird, wäre es schön, dies als Option zu haben. Während sättigende Casts bei Architekturen, bei denen dies nicht das Standardverhalten ist, einen inhärenten Overhead haben, hat "Tun, was die CPU tut" nur dann Overhead, wenn dies eine bestimmte Compileroptimierung verhindert. Ich bin mir nicht sicher, aber ich vermute, dass alle Optimierungen, die durch die Behandlung des Float-to-Int-Überlaufs als UB ermöglicht werden, eine Nische sind und für den meisten Code nicht zutreffen.

Ein Problem kann jedoch sein, wenn eine Architektur über mehrere Float-to-Int-Anweisungen verfügt, die beim Überlauf unterschiedliche Werte zurückgeben. In diesem Fall würde der Compiler, der das eine oder das andere auswählt, das beobachtbare Verhalten beeinflussen, was an sich kein Problem darstellt, aber eines werden kann, wenn ein einzelnes fptosi dupliziert wird und sich die beiden Kopien unterschiedlich verhalten. Ich bin mir jedoch nicht sicher, ob diese Art von Divergenz tatsächlich bei populären Architekturen besteht. Das gleiche Problem gilt auch für andere Gleitkommaoptimierungen, einschließlich der Gleitkommakontraktion ...

const fn (miri) hat das gesättigte Cast-Verhalten bereits seit Rust 1.26 gewählt (vorausgesetzt, wir möchten, dass das CTFE- und RTFE-Ergebnis konsistent ist) (vor 1.26 gibt der überlaufende Cast zur Kompilierungszeit 0 zurück).

const fn g(a: f32) -> i32 {
    a as i32
}

const Q: i32 = g(1e+12);

fn main() {
    println!("{}", Q); // always 2147483647
    println!("{}", g(1e+12)); // unspecified value, but always 2147483647 in miri
}

Miri / CTFE verwendet die to_u128 / to_i128 -Methoden von apfloat, um die Konvertierung durchzuführen. Ich bin mir jedoch nicht sicher, ob dies eine stabile Garantie ist - insbesondere angesichts der Tatsache, dass sie sich anscheinend zuvor geändert hat (was uns bei der Implementierung dieses Materials in Miri nicht bewusst war).

Ich denke, wir könnten dies an das anpassen, was Codegen letztendlich auswählt. Die Tatsache, dass LLVMs Apfloat (von dem die Rust-Version ein direkter Port ist) die Sättigung verwendet, ist ein guter Indikator dafür, dass dies eine Art "vernünftiger Standard" ist.

Eine Lösung für das beobachtbare Verhalten könnte darin bestehen, zufällig eine der verfügbaren Methoden zur Erstellungszeit des Compilers oder die resultierende Binärdatei auszuwählen.
Dann haben Sie Funktionen wie a.saturating_cast::<i32>() für Benutzer, die ein bestimmtes Verhalten erfordern.

@ dns2utf8

Das Wort "zufällig" würde dem Versuch, reproduzierbare Builds zu erhalten, zuwiderlaufen. Wenn es in einer Compiler-Version vorhersehbar ist, wissen Sie, dass jemand entscheiden wird, ob es sich nicht ändert.

IMO, was @comex beschrieben hat (nicht neu in diesem Thread IIRC, alles alte ist wieder neu), ist dies die nächstbeste Option, wenn wir keine Sättigung wollen. Beachten Sie, dass wir nicht einmal LLVM-Änderungen benötigen, um dies zu testen. Wir können Inline-ASM verwenden (auf Architekturen, in denen solche Anweisungen vorhanden sind).

Ein Problem kann jedoch sein, wenn eine Architektur über mehrere Float-to-Int-Anweisungen verfügt, die beim Überlauf unterschiedliche Werte zurückgeben. In diesem Fall würde der Compiler, der das eine oder das andere auswählt, das beobachtbare Verhalten beeinflussen, was an sich kein Problem darstellt, aber eines werden kann, wenn ein einzelnes fptosi dupliziert wird und sich die beiden Kopien unterschiedlich verhalten.

IMO würde ein solcher Nichtdeterminismus fast alle praktischen Vorteile gegenüber freeze aufgeben. Wenn wir dies tun, sollten wir einen Befehl pro Architektur auswählen und dabei bleiben, sowohl aus Gründen des Determinismus als auch damit sich Programme tatsächlich auf das Verhalten des Befehls verlassen können, wenn es für sie sinnvoll ist. Wenn dies auf einer Architektur nicht möglich ist, könnten wir auf eine Software-Implementierung zurückgreifen (aber wie Sie sagen, ist dies völlig hypothetisch).

Dies ist am einfachsten, wenn wir diese Entscheidung nicht an LLVM delegieren, sondern die Operation stattdessen mit Inline-ASM implementieren. Was übrigens auch viel einfacher wäre, als LLVM zu ändern, um neue Intrinsics hinzuzufügen und diese in jedem Backend zu senken.

@rkruppe

[...] Was im Übrigen auch viel einfacher wäre, als LLVM zu ändern, um neue Intrinsics hinzuzufügen und diese in jedem Backend zu senken.

Darüber hinaus ist LLVM nicht gerade glücklich über Intrinsics mit zielabhängiger Semantik:

Wenn Sie jedoch möchten, dass die Darsteller gut definiert sind, sollten Sie ihr Verhalten definieren. "Mach etwas schnelles" ist nicht wirklich eine Definition, und ich glaube nicht, dass wir zielunabhängige Konstrukte zielabhängiges Verhalten geben sollten.

https://groups.google.com/forum/m/#!msg/llvm -dev / cgDFaBmCnDQ / CZAIMj4IBAA

Ich werde # 10184 nur als T-lang zurückverfolgen: Ich denke, die Probleme, die gelöst werden müssen, sind semantische Entscheidungen darüber, was float as int bedeutet

(dh ob wir bereit sind, eine panische Semantik zuzulassen oder nicht, ob wir bereit sind, eine auf freeze basierende Unterspezifikation zuzulassen oder nicht usw.)

Dies sind Fragen, die sich besser an das T-lang-Team richten, nicht an den T-Compiler, zumindest für die erste Diskussion, IMO

Ich bin gerade auf dieses Problem gestoßen und habe Ergebnisse erzielt, die zwischen den Läufen auch ohne Neukompilierung nicht reproduzierbar sind. Der Operator as scheint in solchen Fällen Müll aus dem Speicher zu holen.

Ich schlage vor, die Verwendung von as für "float as int" vollständig zu verbieten und stattdessen auf bestimmte Rundungsmethoden zu setzen. Begründung: as ist für andere Typen nicht verlustbehaftet.

Begründung: wie es für andere Typen nicht verlustbehaftet ist.

Ist es?

Basierend auf Rust Book kann ich davon ausgehen, dass es nur in bestimmten Fällen verlustfrei ist (nämlich in Fällen, in denen From<X> für einen Typ Y definiert ist), dh Sie können u8 in u32 mit From , aber nicht umgekehrt.

Mit "nicht verlustbehaftet" meine ich das Casting von Werten, die klein genug sind, um zu passen. Beispiel: 1_u64 as u8 ist nicht verlustbehaftet, daher ist u8 as u64 as u8 nicht verlustbehaftet. Für Floats gibt es keine einfache Definition von "Passungen", da 20000000000000000000000000000_u128 as f32 nicht verlustbehaftet ist, während 20000001_u32 as f32 ist, sodass weder float as int noch int as float verlustfrei sind.

256u64 as u8 ist allerdings verlustbehaftet.

Aber <anything>_u8 as u64 as u8 ist nicht.

Ich denke, Verlust ist normal und wird mit Casts erwartet und ist kein Problem. Das Abschneiden von Ganzzahlen mit Casts (z. B. u32 as u8 ) ist eine häufige Operation mit einer gut verstandenen Bedeutung, die in allen mir bekannten C-ähnlichen Sprachen konsistent ist (zumindest bei Architekturen, die Zweierkomplement-Ganzzahldarstellungen verwenden) ist im Grunde alle von ihnen in diesen Tagen). Gültige Gleitkommakonvertierungen (dh wo der integrale Teil in das Ziel passt) haben ebenfalls eine gut verstandene und vereinbarte Semantik. 1.6 as u32 ist verlustbehaftet, aber alle mir bekannten C-ähnlichen Sprachen stimmen darin überein, dass das Ergebnis 1 sein sollte. Beide Fälle ergeben sich aus dem Konsens der Hardwarehersteller über die Funktionsweise dieser Konvertierungen und die Konvention in C. -ähnliche Sprachen, die Casts sein sollten, sollten leistungsstarke Operatoren sein. "Ich weiß, was ich tue".

Ich denke also nicht, dass wir diese Probleme genauso betrachten sollten wie ungültige Gleitkommakonvertierungen, da diese keine vereinbarte Semantik in C-ähnlichen Sprachen oder in Hardware haben (aber sie führen normalerweise zu Fehlerzuständen oder Hardware-Ausnahmen) und weisen fast immer auf Fehler hin (meiner Erfahrung nach) und sind daher normalerweise nicht im richtigen Code vorhanden.

Ich bin gerade auf dieses Problem gestoßen und habe Ergebnisse erzielt, die zwischen den Läufen auch ohne Neukompilierung nicht reproduzierbar sind. Der Operator as scheint in solchen Fällen Müll aus dem Speicher zu holen.

Persönlich denke ich, dass das in Ordnung ist, solange es nur passiert, wenn die Konvertierung ungültig ist und außer der Erzeugung eines Müllwerts keine Nebenwirkungen hat. Wenn Sie wirklich eine ansonsten ungültige Konvertierung in einem Code benötigen, können Sie den ungültigen Fall selbst mit der Semantik behandeln, die Sie für erforderlich halten.

und es hat keine Nebenwirkungen außer der Erzeugung eines Müllwerts

Der Nebeneffekt ist, dass der Müllwert irgendwo im Speicher entsteht und einige (möglicherweise sensible) Daten anzeigt. Die Rückgabe eines "zufälligen" Werts, der ausschließlich aus float selbst berechnet wurde, wäre in Ordnung, das aktuelle Verhalten jedoch nicht.

Gültige Gleitkommakonvertierungen (dh wo der integrale Teil in das Ziel passt) haben ebenfalls eine gut verstandene und vereinbarte Semantik.

Gibt es Anwendungsfälle für Float-zu-Int-Konvertierungen, die nicht von expliziten trunc() , round() , floor() oder ceil() ? Die aktuelle Rundungsstrategie von as ist "undefiniert", sodass as für nicht gerundete Zahlen kaum verwendbar ist. Ich glaube, dass in den meisten Fällen derjenige, der x as u32 schreibt, tatsächlich x.round() as u32 will.

Ich denke, Verlust ist normal und wird mit Casts erwartet und ist kein Problem.

Ich stimme zu, aber nur, wenn Verlust leicht vorhersehbar ist. Für ganze Zahlen sind die Bedingungen für eine verlustbehaftete Konvertierung offensichtlich. Für Schwimmer sind sie dunkel. Sie sind für einige sehr große Zahlen verlustfrei, für einige kleinere verlustfrei, selbst wenn sie rund sind. Meine persönliche Präferenz ist es, zwei verschiedene Operatoren für verlustbehaftete und verlustfreie Konvertierungen zu haben, um zu vermeiden, dass versehentlich verlustbehaftete Konvertierungen eingeführt werden. Ich kann aber auch nur einen Operator verwenden, vorausgesetzt, ich kann feststellen, ob er verlustbehaftet ist oder nicht.

Der Nebeneffekt ist, dass der Müllwert irgendwo im Speicher entsteht und einige (möglicherweise sensible) Daten anzeigt.

Ich würde erwarten, dass das Ziel einfach unverändert bleibt oder was auch immer, aber wenn das wirklich ein Problem ist, könnte es zuerst auf Null gesetzt werden.

Gibt es Anwendungsfälle für Float-to-Int-Konvertierungen, die nicht von explizitem Trunc (), Round (), Floor () oder Ceil () begleitet werden? Die aktuelle Rundungsstrategie von as ist "undefiniert" und daher für nicht gerundete Zahlen kaum verwendbar.

Wenn die Rundungsstrategie wirklich undefiniert ist, wäre das eine Überraschung für mich, und ich würde zustimmen, dass der Operator kaum nützlich ist, wenn Sie ihm nicht bereits eine Ganzzahl geben. Ich würde erwarten, dass es gegen Null abschneidet.

Ich glaube, dass in den meisten Fällen derjenige, der x as u32 schreibt, tatsächlich x.round() as u32 will.

Ich denke, es hängt von der Domain ab, aber ich gehe davon aus, dass x.trunc() as u32 auch häufig gewünscht wird.

Ich stimme zu, aber nur, wenn Verlust leicht vorhersehbar ist.

Ich stimme definitiv zu. Ob 1.6 as u32 1 oder 2 wird, sollte zum Beispiel nicht undefiniert sein.

https://doc.rust-lang.org/nightly/reference/expressions/operator-expr.html#type -cast-expression

Das Umwandeln von einem Float in eine Ganzzahl rundet den Float gegen Null
HINWEIS: Derzeit führt dies zu undefiniertem Verhalten, wenn der gerundete Wert nicht durch den ganzzahligen Zieltyp dargestellt werden kann. Dies beinhaltet Inf und NaN. Dies ist ein Fehler und wird behoben.

Der Hinweis verlinkt hier.

Das Runden von Werten, die „passen“, ist genau definiert. Darum geht es in diesem Thema nicht. Dieser Thread ist bereits lang, es wäre schön, ihn nicht dazu zu bringen, Tangenten über Fakten zu spekulieren, die bereits festgelegt und dokumentiert sind. Vielen Dank.

Es bleibt zu entscheiden, wie f as $Int in den folgenden Fällen definiert werden soll:

  • f.trunc() > $Int::MAX (einschließlich positiver Unendlichkeit)
  • f.trunc() < $Int::MIN (einschließlich negativer Unendlichkeit)
  • f.is_nan()

Eine Option, die bereits in Nightly mit dem Compiler-Flag -Z saturating-casts implementiert und verfügbar ist, besteht darin, sie so zu definieren, dass sie jeweils zurückgegeben werden: $Int::MAX , $Int::MIN und Null. Aber es ist immer noch möglich, ein anderes Verhalten zu wählen.

Meiner Ansicht nach sollte das Verhalten definitiv deterministisch sein und einen ganzzahligen Wert zurückgeben (anstatt beispielsweise Panik), aber der genaue Wert ist nicht zu wichtig, und Benutzer, die sich für diese Fälle interessieren, sollten lieber Konvertierungsmethoden verwenden, die ich separat vorschlage Hinzufügen: https://internals.rust-lang.org/t/pre-rfc-add-explicitly-named-numeric-conversion-apis/11395

Ich denke, es hängt von der Domain ab, aber ich gehe davon aus, dass x.trunc() as u32 auch häufig gewünscht wird.

Richtig. Im Allgemeinen x.anything() as u32 , höchstwahrscheinlich round() , könnte aber auch trunc() , floor() , ceil() . Nur x as u32 ohne Angabe eines konkreten Rundungsverfahrens ist höchstwahrscheinlich ein Fehler.

Meiner Ansicht nach sollte das Verhalten definitiv deterministisch sein und einen ganzzahligen Wert zurückgeben (anstatt beispielsweise Panik), aber der genaue Wert ist nicht zu wichtig

Ich persönlich bin in Ordnung, auch wenn der Wert "undefiniert" ist, vorausgesetzt, er hängt von nichts anderem ab als von sich selbst und legt, was am wichtigsten ist, keine nicht zusammenhängenden Register- und Speicherinhalte frei.

Eine Option, die bereits in Nightly mit dem Compiler-Flag -Z saturating-casts implementiert und verfügbar ist, besteht darin, sie so zu definieren, dass sie jeweils zurückgegeben werden: $Int::MAX , $Int::MIN und Null. Aber es ist immer noch möglich, ein anderes Verhalten zu wählen.

Das Verhalten, das ich für f.trunc() > $Int::MAX und f.trunc() < $Int::MIN erwarten würde, ist das gleiche wie wenn die imaginäre Gleitkommazahl in eine unendlich große Ganzzahl umgewandelt wird und dann die niedrigstwertigen Bits davon zurückgegeben werden ( wie bei der Konvertierung von Integer-Typen). Technisch gesehen wären dies einige Bits des Signifikanten, die je nach Exponent nach links verschoben sind (für positive Zahlen müssen negative Zahlen gemäß dem Zweierkomplement invertiert werden).

So würde ich zum Beispiel erwarten, dass wirklich große Zahlen in 0 .

Es scheint schwieriger / willkürlicher zu sein, zu definieren, in was Unendlichkeit und NaN umgewandelt werden.

@CryZe Wenn ich das richtig lese, stimmt das mit -Z saturating-casts überein (und was Miri bereits implementiert)?

@RalfJung Das stimmt.

Genial, ich kopiere dann https://github.com/WebAssembly/testsuite/blob/master/conversions.wast (wobei Traps durch die angegebenen Ergebnisse ersetzt werden) in Miris Testsuite. :) :)

@RalfJung Bitte aktualisieren Sie auf die neueste Version von

@sunfishcode danke für die Aktualisierung! Ich muss die Tests sowieso in Rust übersetzen, damit ich noch viele Dinge ersetzen muss. ;)

Unterscheiden sich die _sat -Tests hinsichtlich der getesteten Werte? (BEARBEITEN: Dort gibt es einen Kommentar, der besagt, dass die Werte gleich sind.) Für Rusts sättigende Besetzungen habe ich viele dieser Werte genommen und sie in https://github.com/rust-lang/miri/pull/1321 hinzugefügt

Für das UB-Intrinsic sollten die Fallen auf der Wasmseite dann zu Compile-Fail-Tests in Miri werden, denke ich.

Die Eingabewerte sind alle gleich. Der einzige Unterschied besteht darin, dass _sat -Operatoren Ausgabewerte für Eingaben erwartet haben, bei denen die Überfüllungsoperatoren Traps erwartet haben.

Tests für Miri (und damit auch für den Rust CTFE-Motor) wurden unter https://github.com/rust-lang/miri/pull/1321 hinzugefügt rustc -Zmir-opt-level=0 -Zsaturating-float-casts auch die Tests in dieser Datei besteht.
Ich habe jetzt auch das ungeprüfte Intrinsic in Miri implementiert, siehe https://github.com/rust-lang/miri/pull/1325.

Ich habe https://github.com/rust-lang/rust/pull/71269#issuecomment -615537137 gepostet, das den aktuellen Status dokumentiert, wie ich ihn verstanden habe, und dass PR auch versucht, das Verhalten des gesättigten -Z-Flags zu stabilisieren.

Angesichts der Länge dieses Threads denke ich, wenn Leute das Gefühl haben, dass ich etwas in diesem Kommentar verpasst habe, würde ich einen Kommentar an die PR richten oder, wenn es geringfügig ist, mich gerne auf Zulip oder Discord (Simulacrum) anpingen und ich kann Beheben Sie Probleme, um unnötiges Rauschen im PR-Thread zu vermeiden.

Ich gehe davon aus, dass jemand aus dem Sprachteam wahrscheinlich bald einen FCP-Vorschlag für diese PR starten wird, und das Zusammenführen wird dieses Problem automatisch schließen :)

Gibt es Pläne für überprüfte Conversions? So etwas wie fn i32::checked_from(f64) -> Result<i32, DoesntFit> ?

Sie müssen überlegen, was i32::checked_from(4.5) zurückgeben soll.

War diese Seite hilfreich?
0 / 5 - 0 Bewertungen