Rust: les casts virgule flottante en entier peuvent provoquer un comportement indéfini

Créé le 31 oct. 2013  ·  234Commentaires  ·  Source: rust-lang/rust

État au 18/04/2020

Nous avons l'intention de stabiliser le comportement saturating-float-cast pour as , et avons stabilisé les fonctions de bibliothèque non sécurisées qui gèrent le comportement précédent. Voir # 71269 pour la dernière discussion sur ce processus de stabilisation.

État au 2018-11-05

Un drapeau a été implémenté dans le compilateur, -Zsaturating-float-casts , ce qui fera que tous les virages flottants en entiers auront un comportement "saturant" où s'il est hors limites, il sera serré à la borne la plus proche. Un appel à l'analyse comparative de ce changement a été lancé il y a quelque temps. Les résultats, bien que positifs dans de nombreux projets, sont plutôt négatifs pour certains projets et indiquent que nous n'avons pas terminé ici.

Les étapes suivantes consistent à déterminer comment récupérer les performances dans ces cas:

  • Une option est de prendre le comportement de conversion as (qui est UB dans certains cas) et d'ajouter des fonctions unsafe pour les types pertinents et autres.
  • Une autre consiste à attendre que LLVM ajoute un concept freeze , ce qui signifie que nous obtenons un motif de bits de déchets, mais ce n'est au moins pas UB
  • Une autre consiste à implémenter des transtypages via l'assemblage en ligne dans LLVM IR, car le codegen actuel n'est pas fortement optimisé.

Ancien statut

MISE À JOUR (par @nikomatsakis): Après de longues discussions, nous avons les rudiments d'un plan sur la façon de résoudre ce problème. Mais nous avons besoin d'aide pour enquêter sur l'impact sur les performances et élaborer les derniers détails!


LE NUMÉRO ORIGINAL SUIT:

Si la valeur ne tient pas dans ty2, les résultats ne sont pas définis.

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

Commentaire le plus utile

J'ai commencé un travail pour implémenter des intrinsèques pour saturer float en cast int dans LLVM: https://reviews.llvm.org/D54749

Si cela va quelque part, cela fournira un moyen relativement faible d'obtenir la sémantique saturante.

Tous les 234 commentaires

Nomination

accepté pour P-high, même raisonnement que # 10183

Je ne pense pas que ce soit rétrocompatible au niveau linguistique. Cela n'entraînera pas l'arrêt du code qui fonctionnait correctement. Nomination.

passage à P-high, même raisonnement que # 10183

Comment proposons-nous de résoudre ce problème et # 10185? Puisque le comportement est défini ou non dépend de la valeur dynamique du nombre en cours de conversion, il semble que la seule solution soit d'insérer des vérifications dynamiques. Nous semblons convenir que nous ne voulons pas faire cela pour le débordement arithmétique, sommes-nous heureux de le faire pour le débordement de fonte?

Nous pourrions ajouter un intrinsèque à LLVM qui effectue une "conversion sûre". @zwarich peut avoir d'autres idées.

AFAIK la seule solution pour le moment est d'utiliser les intrinsèques spécifiques à la cible. C'est ce que fait JavaScriptCore, du moins selon quelqu'un à qui j'ai demandé.

Oh, c'est assez facile alors.

ping @pnkfelix est-ce couvert par la nouvelle vérification de débordement?

Ces conversions ne sont pas vérifiées par rustc avec des assertions de débogage.

Je suis heureux de gérer cela, mais j'ai besoin d'une solution concrète. Personnellement, je pense qu'il devrait être vérifié avec l'arithmétique des nombres entiers débordants, car c'est un problème très similaire. Cela ne me dérange pas vraiment ce que nous faisons.

Notez que ce problème provoque actuellement un ICE lorsqu'il est utilisé dans certaines expressions constantes.

Cela permet de violer la sécurité de la mémoire dans la rouille sûre, exemple de ce message de forum :

Undefs, hein? Les Undefs sont amusants. Ils ont tendance à se propager. Après quelques minutes de dispute.

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

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

segfaults sur mon système (dernier soir) avec -O.

Marquage avec I-unsound compte tenu de la violation de la sécurité de la mémoire dans la rouille sûre.

@bluss , cela ne segfualt pas pour moi, donne juste une erreur d'assertion. non étiqueté puisque c'est moi qui l'ai ajouté

Soupir, j'ai oublié le -O, re-tagging.

re-nominer pour P-high. Apparemment, c'était à un certain point P-élevé mais a diminué avec le temps. Cela semble assez important pour l'exactitude.

EDIT: n'a pas réagi au commentaire de triage, ajoutant l'étiquette manuellement.

Il semble que le précédent des trucs de débordement (par exemple pour le décalage) soit simplement de se contenter d'un comportement. Java semble produire le résultat modulo la plage, ce qui ne semble pas déraisonnable; Je ne suis pas sûr du type de code LLVM dont nous aurions besoin pour gérer cela.

Selon https://docs.oracle.com/javase/specs/jls/se7/html/jls-5.html#jls -5.1.3 Java garantit également que les valeurs NaN sont mappées à 0 et à l'infini à l'entier représentable minimum / maximum. De plus, la règle Java pour la conversion est plus complexe que le simple wrapping, elle peut être une combinaison de saturation (pour la conversion en int ou long ) et de wrapping (pour la conversion en types intégraux plus petits , si besoin). La réplication de l'ensemble de l'algorithme de conversion à partir de Java est certainement possible, mais cela nécessiterait une bonne quantité d'opérations pour chaque conversion. En particulier, pour s'assurer que le résultat d'une opération fpto[us]i dans LLVM ne présente pas un comportement indéfini, une vérification de plage serait nécessaire.

Comme alternative, je suggérerais que les castings float-> int ne soient valides que si la troncature de la valeur d'origine peut être représentée comme une valeur du type de destination (ou peut-être comme [iu]size ?) Et avoir des assertions sur les builds de débogage qui déclenchent une panique lorsque la valeur n'a pas été représentée fidèlement.

Les principaux avantages de l'approche Java sont que la fonction de conversion est totale, mais cela signifie également qu'un comportement inattendu pourrait s'infiltrer: cela empêcherait un comportement indéfini, mais il serait facile d'être amené à ne pas vérifier si la distribution avait réellement un sens (ceci est malheureusement vrai aussi pour les autres lancers: inquiet :).

L'autre approche correspond à celle actuellement utilisée pour les opérations arithmétiques: implémentation simple et efficace dans la version, paniques déclenchées par la vérification de plage dans le débogage. Malheureusement, contrairement aux autres casts de as , cette conversion serait vérifiée, ce qui peut surprendre l'utilisateur (bien que peut-être que l'analogie avec les opérations arithmétiques puisse aider ici). Cela briserait aussi du code, mais AFAICT cela ne devrait arriver que pour le code qui repose actuellement sur un comportement indéfini (c'est-à-dire qu'il remplacerait le comportement non défini "retournons n'importe quel entier, vous ne vous en souciez évidemment pas" avec une panique).

Le problème n'est pas "retournons n'importe quel entier, vous ne vous en souciez évidemment pas", c'est qu'il provoque un undef qui n'est pas une valeur aléatoire mais plutôt une valeur démon nasale et LLVM est autorisé à supposer que l'undef ne se produit jamais permettant des optimisations qui font des choses horribles et incorrectes. S'il s'agissait d'une valeur aléatoire, mais surtout pas undef, cela suffirait à résoudre les problèmes de solidité. Nous n'avons pas besoin de définir comment les valeurs non représentables sont représentées, nous devons simplement empêcher undef.

Discuté lors de la réunion @ rust-lang / compiler. Le plan d'action le plus cohérent reste:

  1. lorsque les vérifications de débordement sont activées, vérifiez les lancers illégaux et la panique.
  2. sinon, nous avons besoin d'un comportement de repli, ce devrait être quelque chose qui a un coût d'exécution minimal (idéalement nul) pour les valeurs valides, mais le comportement précis n'est pas si important, tant qu'il n'est pas LLVM undef.

Le principal problème est que nous avons besoin d'une suggestion concrète pour l'option 2.

triage: P-moyen

@nikomatsakis Est- as panique actuellement dans les versions de débogage? Si ce n'est pas le cas, pour des raisons de cohérence et de prévisibilité, il semble préférable de le maintenir ainsi. (Je pense que cela aurait dû l'être, tout comme l'arithmétique, mais c'est un débat séparé et passé.)

sinon, nous avons besoin d'un comportement de repli, ce devrait être quelque chose qui a un coût d'exécution minimal (idéalement nul) pour les valeurs valides, mais le comportement précis n'est pas si important, tant qu'il n'est pas LLVM undef.

Suggestion concrète: extraire les chiffres et l'exposant sous la forme u64 et décaler les chiffres par exposant

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

Oui, ce n'est pas un coût nul, mais c'est quelque peu optimisable (ce serait mieux si nous marquions integer_decode inline ) et au moins déterministe. Un futur MIR-pass qui étend un float-> int cast pourrait probablement analyser si le float est garanti pour être ok pour lancer et sauter cette conversion lourde.

LLVM n'a-t-il pas de plate-forme intrinsèque pour les fonctions de conversion?

EDIT : @zwarich a dit (il y a longtemps):

AFAIK la seule solution pour le moment est d'utiliser les intrinsèques spécifiques à la cible. C'est ce que fait JavaScriptCore, du moins selon quelqu'un à qui j'ai demandé.

Pourquoi même paniquer? AFAIK, @glaebhoerl est correct, as est censé tronquer / étendre, _pas_ vérifier les opérandes.

Le samedi 05 mars 2016 à 03:47:55 -0800, Gábor Lehel a écrit:

@nikomatsakis Est- as panique actuellement dans les versions de débogage? Si ce n'est pas le cas, pour des raisons de cohérence et de prévisibilité, il semble préférable de le maintenir ainsi. (Je pense que cela aurait dû l'être, tout comme l'arithmétique, mais c'est un débat séparé et passé.)

Vrai. Je trouve cela convaincant.

Le mer 09 mars 2016 à 02:31:05 -0800, Eduard-Mihai Burtescu a écrit:

LLVM n'a-t-il pas de plate-forme intrinsèque pour les fonctions de conversion?

MODIFIER :

AFAIK la seule solution pour le moment est d'utiliser les intrinsèques spécifiques à la cible. C'est ce que fait JavaScriptCore, du moins selon quelqu'un à qui j'ai demandé.

Pourquoi même paniquer? AFAIK, @glaebhoerl est correct, as est censé tronquer / étendre, _pas_ vérifier les opérandes.

Oui, je pense que je me suis trompé avant. as est la "troncature non vérifiée"
opérateur, pour le meilleur ou pour le pire, et il semble préférable de rester cohérent
avec cette philosophie. Utiliser des éléments intrinsèques spécifiques à une cible peut être
bonne solution cependant?

@nikomatsakis : il semble que le comportement n'ait pas encore été défini? Pouvez-vous donner une mise à jour sur la planification à ce sujet?

Je viens de rencontrer cela avec des nombres beaucoup plus petits

    let x: f64 = -1.0;
    x as u8

Résultats en 0, 16, etc. en fonction des optimisations, j'espérais qu'il serait défini comme 255 donc je n'ai pas à écrire x as i16 as u8 .

@gmorenz Avez-vous essayé !0u8 ?

Dans un contexte qui n'aurait pas de sens, j'obtenais le f64 d'une transformation sur des données envoyées sur le réseau, avec une plage de [-255, 255]. J'espérais que cela s'enroulerait bien (de la manière exacte dont <i32> as u8 s'enroule).

Voici une récente proposition de LLVM de "tuer undef" http://lists.llvm.org/pipermail/llvm-dev/2016-October/106182.html , même si je suis à peine suffisamment informé pour savoir si cela résoudrait automatiquement ou non ce problème.

Ils remplacent undef par poison, la sémantique étant légèrement différente. Cela ne fera pas que int -> float lance un comportement défini.

Nous devrions probablement fournir un moyen explicite de faire un casting saturant? Je voulais ce comportement exact tout à l'heure.

On dirait que cela devrait être marqué I-crash, donné https://github.com/rust-lang/rust/issues/10184#issuecomment -139858153.

Nous avions une question à ce sujet dans #rust-beginners aujourd'hui, quelqu'un est tombé dessus dans la nature.

Le livre que j'écris avec @jimblandy , _Programming Rust_, mentionne ce bug.

Plusieurs types de moulages sont autorisés.

  • Les nombres peuvent être convertis de n'importe quel type numérique intégré à n'importe quel autre.

    (...)

    Cependant, au moment d'écrire ces lignes, convertir une grande valeur à virgule flottante en un type entier trop petit pour le représenter peut entraîner un comportement indéfini. Cela peut provoquer des plantages même dans Rust en toute sécurité. Il s'agit d'un bogue dans le compilateur, github.com/rust-lang/rust/issues/10184 .

Notre date limite pour ce chapitre est le 19 mai. J'aimerais supprimer ce dernier paragraphe, mais je pense que nous devrions au moins avoir une sorte de plan ici en premier.

Apparemment, JavaScriptCore actuel utilise un hack intéressant sur x86. Ils utilisent l'instruction CVTTSD2SI, puis se rabattent sur du C ++ poilu si la valeur est hors limites. Étant donné que les valeurs hors limites explosent actuellement, l'utilisation de cette instruction (sans repli!) Serait une amélioration par rapport à ce que nous avons maintenant, bien que pour une seule architecture.

Honnêtement, je pense que nous devrions désapprouver les conversions numériques avec as et utiliser From et TryFrom ou quelque chose comme le conv crate à la place.

Peut-être, mais cela me semble orthogonal.

OK, je viens de relire toute cette conversation. Je pense qu'il est entendu que cette opération ne doit pas paniquer (pour une cohérence générale avec as ). Il y a deux principaux prétendants à ce que devrait être le comportement:

  • Une sorte de résultat défini

    • Pro: Je pense que cela correspond au maximum à notre philosophie générale jusqu'à présent.

    • Inconvénient: Il ne semble pas y avoir de moyen vraiment portable de produire un résultat défini particulier dans ce cas. Cela implique que nous utiliserions des intrinsèques spécifiques à la plate-forme avec une sorte de repli pour les valeurs hors plage (par exemple, le retour à la saturation , cette fonction proposée par @ oli-obk , à la définition de Java , ou à tout autre "C ++ poilu" JSC utilise .

    • Au pire, nous pouvons simplement insérer des ifs pour les cas "hors limites".

  • Une valeur indéfinie (pas un comportement indéfini)

    • Pro: Cela nous permet d'utiliser simplement les intrinsèques spécifiques à la plate-forme qui sont disponibles sur chaque plate-forme.

    • Con: C'est un risque de portabilité. En général, j'ai l'impression que nous n'avons pas utilisé très souvent des résultats non définis, du moins dans le langage (je suis sûr que nous le faisons dans les bibliothèques à divers endroits).

Je ne vois pas clairement s'il existe un précédent clair quant à ce que devrait être le résultat dans le premier cas?

Après avoir écrit cela, ma préférence serait de maintenir un résultat déterministe. Je pense que chaque endroit où nous pouvons tenir la ligne sur le déterminisme est une victoire. Je ne sais pas vraiment quel devrait être le résultat.

J'aime la saturation parce que je peux la comprendre et cela semble utile, mais cela semble en quelque sorte incongru avec la façon dont u64 as u32 effectue la troncature. Alors peut-être qu'une sorte de résultat basé sur la troncature a du sens, ce qui, je suppose, est probablement ce que @ oli-obk a proposé - je ne comprends pas entièrement ce que ce code est censé faire. =)

Mon code donne la valeur correcte pour les choses dans la plage 0..2 ^ 64 et des valeurs déterministes mais fausses pour tout le reste.

les flottants sont représentés par mantisse ^ exposant, par exemple 1.0 est (2 << 52) ^ -52 et comme les décalages de bits et les exposants sont la même chose en binaire, nous pouvons simplement inverser le décalage (donc la négation de l'exposant et la droite décalage).

+1 pour le déterminisme.

Je vois deux sémantiques qui ont du bon sens pour les humains, et je pense que nous devrions choisir celle qui est la plus rapide pour les valeurs qui sont dans la plage, lorsque le compilateur ne peut pas optimiser le calcul . (Lorsque le compilateur sait qu'une valeur est dans la plage, les deux options donnent les mêmes résultats, donc elles sont également optimisables.)

  • Saturation (les valeurs hors plage deviennent IntType::max_value() / min_value() )
  • Modulo (les valeurs hors limites sont traitées comme si elles étaient converties en bigint d'abord, puis tronquées)

Le tableau ci-dessous est destiné à spécifier complètement les deux options. T est n'importe quel type entier machine. Tmin et Tmax sont T::min_value() et T::max_value() . RTZ (v) signifie prendre la valeur mathématique de v et Round Toward Zero pour obtenir un entier mathématique.

v | v as T (saturation) | v as T (modulo)
---- | ---- | ----
dans la plage (Tmin <= v <= Tmax) | RTZ (v) | RTZ (v)
zéro négatif | 0 | 0
NaN | 0 | 0
Infinity | Tmax | 0
-Infinity | Tmin | 0
v> Tmax | Tmax | RTZ (v) tronqué pour s'adapter à T
v <Tmin | Tmin | RTZ (v) tronqué pour s'adapter à T

La norme ECMAScript spécifie les opérations ToInt32 , ToUint32, ToInt16, ToUint16, ToInt8, ToUint8 et mon intention avec l'option "modulo" ci-dessus est de faire correspondre ces opérations dans tous les cas.

ECMAScript précise également ToInt8Clamp qui ne correspond pas à tous les cas ci - dessus: il ne « demi - tour même à » arrondir les valeurs plutôt que des fractions « autour de zéro ».

La suggestion de @ oli-obk est une troisième façon, à considérer si elle est plus rapide à calculer, pour les valeurs qui sont dans la plage.

@ oli-obk Qu'en est-il des types d'entiers signés?

Jeter une autre proposition dans le mélange: Mark u128 jette sur les flotteurs comme dangereux et oblige les gens à choisir explicitement une façon de le gérer. u128 est assez rare actuellement.

@Manishearth J'espère avoir des entiers sémantiques similaires → flottants en tant que flottants → entiers. Puisque les deux sont UB-pleins, et que nous ne pouvons plus rendre float → integer unsafe, nous devrions probablement éviter de rendre integer → float unsafe également.

Pour float → entier, la saturation sera plus rapide AFAICT (résultant en une séquence de and , test + jump float comparaison et saut, le tout pour 0,66 ou 0,5 2-3 cycles sur les arches modernes). Personnellement, je ne me soucie guère du comportement exact sur lequel nous décidons tant que les valeurs dans la plage sont aussi rapides qu'elles pourraient l'être.

Ne serait-il pas logique de le faire se comporter comme un débordement? Donc, dans une version de débogage, cela paniquerait si vous faisiez un casting avec un comportement non défini. Ensuite, vous pourriez avoir des méthodes pour spécifier le comportement de casting comme 1.04E+17.saturating_cast::<u8>() , unsafe { 1.04E+17.unsafe_cast::<u8>() } et potentiellement d'autres.

Oh, je pensais que le problème concernait uniquement u128, et nous pouvons rendre cela dangereux dans les deux sens.

@cryze UB ne devrait pas exister même en mode release en code sécurisé. Le truc de débordement est toujours un comportement défini.

Cela dit, panique lors du débogage, età la sortie serait génial.

Cela affecte:

  • f32 -> u8, u16, u32, u64, u128, usize ( -1f32 as _ pour tous, f32::MAX as _ pour tous sauf u128)
  • f32 -> i8, i16, i32, i64, i128, isize ( f32::MAX as _ pour tous)
  • f64 -> tous les entiers ( f64::MAX as _ pour tous)

f32::INFINITY as u128 est aussi UB

@CryZe

Ne serait-il pas logique de le faire se comporter comme un débordement? Donc, dans une version de débogage, cela paniquerait si vous faisiez un casting avec un comportement non défini.

C'est ce que je pensais au départ, mais on m'a rappelé que les conversions as ne paniquent jamais à l'heure actuelle (nous ne faisons pas de vérification de débordement avec as , pour le meilleur ou pour le pire). Donc, la chose la plus analogue est qu'elle "fasse quelque chose de défini".

FWIW, la chose "tuer undef" fournirait, en fait, un moyen de corriger l'insécurité de la mémoire, mais en laissant le résultat non déterministe. L'un des éléments clés est:

3) Créez une nouvelle instruction, '% y = freeze% x', qui arrête la propagation de
poison. Si l'entrée est poison, alors elle renvoie un arbitraire, mais fixe,
valeur. (comme l'ancien undef, mais chaque utilisation a la même valeur), sinon il
renvoie simplement sa valeur d'entrée.

La raison pour laquelle les undefs peuvent être utilisés pour violer la sécurité de la mémoire aujourd'hui est qu'ils peuvent changer comme par magie les valeurs entre les utilisations: en particulier, entre une vérification des limites et l'arithmétique ultérieure des pointeurs. Si rustc ajoutait un gel après chaque lancer dangereux, vous obtiendriez simplement une valeur inconnue mais par ailleurs bien élevée. Du point de vue des performances, le gel est fondamentalement gratuit ici, car bien sûr l'instruction machine correspondant à la distribution produit une valeur unique, pas une valeur fluctuante; même si l'optimiseur a envie de dupliquer l'instruction de conversion pour une raison quelconque, il devrait être prudent de le faire car le résultat pour les entrées hors de portée est généralement déterministe sur une architecture donnée.

... Mais pas déterministe à travers les architectures, si quelqu'un se demandait. x86 renvoie 0x80000000 pour toutes les mauvaises entrées; ARM sature pour les entrées hors plage et (si je lis ce pseudo-code à droite) renvoie 0 pour NaN. Donc, si l'objectif est de produire un résultat déterministe et indépendant de la plate-forme , il ne suffit pas d'utiliser simplement le fp-to-int intrinsèque de la plate-forme; au moins sur ARM, vous devez également vérifier le registre d'état pour une exception. Cela peut avoir une surcharge en soi et empêche certainement l'autovectorisation dans le cas peu probable où l'utilisation de l'intrinsèque ne l'a pas déjà fait. Alternativement, je suppose que vous pouvez tester explicitement les valeurs dans la plage en utilisant des opérations de comparaison régulières, puis utiliser un float-to-int régulier. Cela semble beaucoup plus agréable sur l'optimiseur ...

as conversions

À un moment donné, nous avons changé + en panique (en mode débogage). Je ne serais pas choqué de voir la panique as dans des cas qui étaient auparavant UB.

Si nous nous soucions de vérifier (ce que nous devrions), alors nous devrions soit déprécier as (y a-t-il un cas d'utilisation où c'est la seule bonne option?) Ou au moins déconseiller de l'utiliser, et déplacer les gens vers des choses comme TryFrom et TryInto place, ce que nous avons dit que nous prévoyions de faire quand il a été décidé de laisser as tel quel. Je ne pense pas que les cas en discussion soient qualitativement différents, dans l'abstrait , des cas où as est déjà défini pour ne pas faire de vérifications. La différence est simplement que dans la pratique, la mise en œuvre de ces cas est actuellement incomplète et a UB. Un monde où vous ne pouvez pas compter sur as faire des contrôles (parce que pour la plupart des types, il ne fonctionne pas), et vous ne pouvez pas compter sur elle pas paniquer (parce que pour certains types, ce serait), et ce n'est pas cohérent, et nous n'avons toujours pas déprécié cela me semble être le pire de tous.

Donc, je pense qu'à ce stade, @jorendorff a essentiellement énuméré ce qui me semble être le meilleur plan :

  • as aura un comportement déterministe;
  • nous choisirons un comportement basé sur une combinaison de sa sensibilité et de son efficacité

Il a énuméré trois possibilités. Je pense que le travail restant consiste à étudier ces possibilités - ou du moins à en étudier une . Autrement dit, implémentez-le et essayez d'avoir une idée de sa "lenteur" ou de sa "rapidité".

Y a-t-il quelqu'un là-bas qui se sent motivé pour tenter de répondre à cela? Je vais marquer cela comme E-help-wanted dans l'espoir d'attirer une personne. (@ oli-obk?)

Euh, je préfère ne pas payer le prix pour la cohérence multiplateforme : / C'est garbage in, je me fiche de ce qui sort (cependant une assertion de débogage serait super utile).

Actuellement, toutes les fonctions d'arrondi / troncature dans Rust sont très lentes (appels de fonction avec des implémentations minutieusement précises), donc le as est mon dernier hack pour l'arrondi flottant rapide.

Si vous voulez faire de as autre chose que du simple cvttss2si , veuillez également ajouter une alternative stable qui est juste cela.

@pornel ce n'est pas seulement UB du genre théorique où tout va bien si vous ignorez que c'est ub, cela a des implications dans le monde réel. J'ai extrait # 41799 d'un exemple de code réel.

@ est31 Je suis d'accord pour dire que le laisser comme UB est faux, mais j'ai vu freeze proposé comme solution à UB. AFAIK qui en fait une valeur déterministe définie, vous ne pouvez tout simplement pas dire laquelle. Ce comportement me convient.

Donc ça irait si, par exemple, u128::MAX as f32 produit de manière déterministe 17.5 sur x86, et 999.0 sur x86-64, et -555 sur ARM.

freeze ne produirait pas de valeur définie, déterministe et non spécifiée. Son résultat est toujours "n'importe quel modèle de bits que le compilateur aime", et il n'est cohérent que pour les utilisations de la même opération. Cela peut contourner les exemples de production d'UB que les gens ont rassemblés ci-dessus, mais cela ne donnerait pas ceci:

u128 :: MAX en tant que f32 a produit de manière déterministe 17,5 sur x86, 999,0 sur x86-64 et -555 sur ARM.

Par exemple, si LLVM remarque que u128::MAX as f32 déborde et le remplace par freeze poison , un abaissement valide de fn foo() -> f32 { u128::MAX as f32 } sur x86_64 pourrait être ceci:

foo:
  ret

(c'est-à-dire, retournez simplement ce qui a été stocké en dernier dans le registre de retour)

Je vois. C'est toujours acceptable pour mes utilisations (pour les cas où j'attends des valeurs hors plage, je fais un serrage à l'avance. Là où j'attends des valeurs dans la plage, mais ce n'est pas le cas, alors je n'obtiendrai pas un résultat correct quoi qu'il arrive) .

Je n'ai aucun problème avec les casts flottants hors plage renvoyant des valeurs arbitraires tant que les valeurs sont gelées afin qu'elles ne puissent pas provoquer un comportement indéfini supplémentaire.

Quelque chose comme freeze disponible sur LLVM? Je pensais que c'était une construction purement théorique.

@nikomatsakis Je ne l'ai jamais vu utilisé comme ça (contrairement à poison ) - c'est une refonte prévue de poison / undef.

freeze n'existe pas du tout dans LLVM aujourd'hui. Il a seulement été proposé ( ce papier PLDI est une version autonome, mais il a également été beaucoup discuté sur la liste de diffusion). La proposition semble avoir une adhésion considérable, mais ce n'est évidemment pas une garantie qu'elle sera adoptée, encore moins adoptée en temps opportun. (La suppression des types pointes des types pointeurs est acceptée depuis des années et ce n'est toujours pas fait.)

Voulons-nous ouvrir un RFC pour obtenir une discussion plus large sur les changements proposés ici? OMI, tout ce qui pourrait avoir un impact sur les performances de as sera controversé, mais ce sera doublement litigieux si nous ne donnons pas aux gens la possibilité de faire entendre leur voix.

Je suis un développeur Julia et je suis ce problème depuis un certain temps, car nous partageons le même backend LLVM et avons donc des problèmes similaires. Dans le cas où cela vous intéresse, voici ce sur quoi nous nous sommes installés (avec des horaires approximatifs pour une seule fonction sur ma machine):

  • unsafe_trunc(Int64, x) correspond directement à l'intrinsèque LLVM correspondante fptosi (1,5 ns)
  • trunc(Int64, x) lève une exception pour les valeurs hors limites (3 ns)
  • convert(Int64, x) lève une exception pour les valeurs hors limites ou non entières (6 ns)

De plus, j'ai demandé à la liste de diffusion de définir un peu plus le comportement non défini, mais je n'ai pas reçu de réponse très prometteuse.

@bstrie Je suis d' @simonbyrne est cependant très utile à cet égard.

J'ai joué avec la sémantique JS (le modulo @jorendorff mentionné) et la sémantique Java qui semble être la colonne "saturation". Au cas où ces liens expireraient, c'est JS et Java .

J'ai également mis au point une implémentation rapide de la saturation dans Rust qui je pense (?) Est correcte. Et j'ai aussi des chiffres de référence . Fait intéressant, je constate que l'implémentation saturante est 2 à 3 fois plus lente que l'intrinsèque, ce qui est différent de ce que @simonbyrne a trouvé avec seulement 2 fois plus lent.

Je ne sais pas vraiment comment implémenter la sémantique "mod" dans Rust ...

Pour moi, cependant, il semble clair que nous aurons besoin d'une multitude de méthodes f32::as_u32_unchecked() et autres pour ceux qui ont besoin de la performance.

il semble clair que nous aurons besoin d'une multitude de méthodes f32::as_u32_unchecked() et autres pour ceux qui ont besoin de la performance.

C'est décevant - ou voulez-vous dire une variante sûre mais définie par l'implémentation?

N'existe-t-il pas d'option pour une implémentation définie par défaut rapide?

@eddyb Je pensais que nous aurions juste unsafe fn as_u32_unchecked(self) -> u32 sur f32 et ce qui est un analogue direct à ce que as est aujourd'hui.

Je ne vais certainement pas prétendre que l'implémentation de Rust que j'ai écrite est optimale, mais j'avais l'impression qu'en lisant ce fil, le déterminisme et la sécurité étaient plus importants que la vitesse dans ce contexte la plupart du temps. La trappe d'évacuation unsafe est pour ceux de l'autre côté de la clôture.

Il n'y a donc pas de variante dépendant de la plate-forme bon marché? Je veux quelque chose de rapide, qui donne une valeur non spécifiée en dehors des limites et qui est sûr. Je ne veux pas d'UB pour certaines entrées et je pense que c'est trop dangereux pour une utilisation courante, si nous pouvons faire mieux.

Pour autant que je sache, sur la plupart sinon toutes les plates-formes, la manière canonique d'implémenter cette conversion fait quelque chose aux entrées hors de portée qui ne sont pas UB. Mais LLVM ne semble avoir aucun moyen de choisir cette option (quelle qu'elle soit) sur UB. Si nous pouvions convaincre les développeurs LLVM d'introduire un résultat intrinsèque qui donne un résultat "non spécifié mais pas undef / poison " sur des entrées hors limites, nous pourrions l'utiliser.

Mais j'estime que quelqu'un dans ce fil devrait écrire un RFC convaincant (sur la liste llvm-dev), obtenir l'adhésion et l'implémenter (dans les backends qui nous intéressent, et avec une implémentation de secours pour d'autres cibles). Probablement plus facile que de convaincre llvm-dev de rendre les castes existantes non-UB (car cela évite des questions telles que "cela ralentira-t-il les programmes C et C ++"), mais ce n'est toujours pas très facile.

Juste au cas où vous choisiriez entre ceux-ci:

Saturation (les valeurs hors plage deviennent IntType :: max_value () / min_value ())
Modulo (les valeurs hors limites sont traitées comme si elles étaient converties en bigint d'abord, puis tronquées)

Seule la saturation IMO aurait du sens ici, car la précision absolue de la virgule flottante diminue rapidement à mesure que les valeurs deviennent grandes, de sorte qu'à un moment donné, le modulo serait quelque chose d'inutile comme tous les zéros.

J'ai marqué ceci comme E-needs-mentor et l'ai marqué avec WG-compiler-middle car il semble que la période implicite pourrait être un bon moment pour approfondir cette question! Mes notes existantes

@nikomatsakis

IIRC LLVM envisage d'implémenter éventuellement freeze , ce qui devrait nous permettre de traiter l'UB en faisant un freeze .

Mes résultats jusqu'à présent: https://gist.github.com/s3bk/4bdfbe2acca30fcf587006ebb4811744

Les variantes _array exécutent une boucle de 1024 valeurs.
_cast: x as i32
_clip: x.min (MAX) .max (MIN) comme i32
_panic: panique si x est hors limites
_zero: met le résultat à zéro s'il est hors limites

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)

Vous n'avez peut-être pas besoin d'arrondir les résultats à un entier pour des opérations individuelles. Il doit clairement y avoir une différence derrière ces 2 ns / iter. Ou est-ce vraiment comme ça, _exactement_ 2 ns pour les 4 variantes?

@ sp-1234 Je me demande s'il est partiellement optimisé.

@ sp-1234 C'est trop rapide pour mesurer. Les benchmarks non-array sont fondamentalement inutiles.
Si vous forcez les fonctions à valeur unique à être des fonctions via #[inline(never)] , vous obtenez 2ns contre 3ns.

@ arielb1
J'ai quelques réserves concernant freeze . Si je comprends bien, un undef figé peut toujours contenir une valeur arbitraire, cela ne changera tout simplement pas entre les utilisations. En pratique, le compilateur réutilisera probablement un registre ou un emplacement de pile.

Cependant, cela signifie que nous pouvons maintenant lire la mémoire non initialisée à partir du code sécurisé. Cela pourrait entraîner une fuite de données secrètes, un peu comme Heartbleed. On peut se demander si cela est vraiment considéré comme UB du point de vue de Rust, mais cela semble clairement indésirable.

J'ai exécuté le benchmark de @ s3bk localement. Je peux confirmer que les versions scalaires sont complètement optimisées et que l'asm pour les variantes de tableau semble également étrangement bien optimisé: par exemple, les boucles sont vectorisées, ce qui est bien mais rend difficile l'extrapolation des performances au code scalaire.

Malheureusement, les spams black_box ne semblent pas aider. Je vois que l'asm fait un travail utile, mais l'exécution du benchmark donne toujours systématiquement 0ns pour les benchmarks scalaires (sauf cast_zero , qui montre 1ns). Je vois que @alexcrichton a effectué la comparaison 100 fois dans ses benchmarks, j'ai donc adopté le même hack. Je vois maintenant ces chiffres ( code source ):

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)

Les benchmarks des tableaux varient trop pour que je leur fasse confiance. Eh bien, à vrai dire, je suis sceptique quant à l'infrastructure d'analyse comparative test , surtout après avoir vu les chiffres ci-dessus par rapport aux 0ns plats que j'ai obtenus précédemment. De plus, même seulement 100 itérations de black_box(x); (comme base de référence) prennent 34ns, ce qui rend encore plus difficile d'interpréter ces nombres de manière fiable.

Deux points à noter:

  • Bien qu'il ne gère pas spécifiquement NaN (il retourne -inf au lieu de 0?), L'implémentation cast_clip semble être plus lente que le casting saturant de @alexcrichton (notez que leur exécution et la mienne ont à peu près le même timing pour as lance, 53-54ns).
  • Contrairement aux résultats du tableau de @ s3bk , je vois que cast_panic est plus lent que les autres castes cochées. Je vois également un ralentissement encore plus important sur les benchmarks de la baie. Peut-être que ces choses dépendent fortement des détails microarchitecturaux et / ou de l'humeur de l'optimiseur?

Pour mémoire, j'ai mesuré avec rustc 1.21.0-nightly (d692a91fa 2017-08-04), -C opt-level=3 , sur un i7-6700K sous faible charge.


En conclusion, je conclus qu'il n'y a pas de données fiables à ce jour et qu'obtenir des données plus fiables semble difficile. De plus, je doute fortement qu'une application réelle consacre même 1% de son temps d'horloge murale à cette opération. Par conséquent, je suggérerais d'aller de l'avant en implémentant des as saturants dans rustc , derrière un drapeau -Z , puis en exécutant des repères non artificiels avec et sans cet indicateur pour déterminer l'impact sur réaliste applications.

Edit: Je recommanderais également d'exécuter de tels benchmarks sur une variété d'architectures (par exemple, y compris ARM) et de microarchitectures, si possible.

J'avoue que je ne suis pas très familier avec la rouille, mais je pense que cette ligne est subtilement incorrecte: std::i32::MAX (2 ^ 31-1) n'est pas exactement représentable en tant que Float32, donc std::i32::MAX as f32 sera arrondi à la valeur représentable la plus proche (2 ^ 31). Si cette valeur est utilisée comme argument x , le résultat est techniquement indéfini. Le remplacement par une inégalité stricte devrait résoudre ce cas.

Ouais, nous avons eu exactement ce problème dans Servo avant. La solution finale consistait à couler en f64 puis à serrer.

Il existe d'autres solutions mais elles sont assez délicates et la rouille n'expose pas de bonnes API pour bien gérer cela.

l'utilisation de 0x7FFF_FF80i32 comme limite supérieure et -0x8000_0000i32 devrait résoudre ce problème sans convertir en f64.
edit: utilisez la valeur correcte.

Je pense que vous voulez dire 0x7fff_ff80 , mais simplement utiliser une inégalité stricte rendrait probablement l'intention du code plus claire.

comme dans x < 0x8000_0000u32 as f32 ? Ce serait probablement une bonne idée.

Je pense à toutes les options déterministes suggérées, le clampage est généralement celui qui est le plus utile car je pense que c'est souvent fait de toute façon. Si le type de conversion était effectivement documenté comme saturant, le serrage manuel deviendrait inutile.

Je ne suis qu'un peu inquiet de l'implémentation suggérée car elle ne se traduit pas correctement en instructions machine et repose fortement sur le branchement. Le branchement rend les performances dépendantes de modèles de données spécifiques. Dans les cas de test donnés ci-dessus, tout semble (comparativement) rapide car la même branche est toujours prise et le processeur a de bonnes données de prédiction de branche provenant de nombreuses itérations de boucle précédentes. Le monde réel ne ressemblera probablement pas à ça. De plus, le branchement nuit à la capacité du compilateur à vectoriser le code. Je ne suis pas d'accord avec l'opinion de @rkruppe , selon laquelle l'opération ne doit pas être testée également en combinaison avec la vectorisation. La vectorisation est importante dans le code haute performance et être capable de vectoriser des transtypages simples sur des architectures communes devrait être une exigence cruciale.

Pour les raisons données ci-dessus, j'ai joué avec une version alternative sans branches et orientée flux de données du casting de correctif de u16 , i16 et i32 , car ils ont tous pour couvrir les cas légèrement différentes qui se traduisent par des performances contrastées .

Les resultats:

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)

Le test a été exécuté sur un processeur Intel Haswell i5-4570 et Rust 1.22.0 tous les soirs.
clip2 est la nouvelle implémentation sans branche. Il est d'accord avec clip sur les 2 ^ 32 valeurs d'entrée f32 possibles.

Pour les benchmarks rng , des valeurs d'entrée aléatoires sont utilisées qui touchent souvent différents cas. Cela découvre le coût des performances _extreme_ (environ 10 fois le coût normal !!!) qui se produit si la prédiction de branche échoue. Je pense qu'il est très important de considérer cela. Ce n'est pas non plus la performance moyenne du monde réel, mais c'est toujours un cas possible et certaines applications le feront. Les gens s'attendront à ce qu'un casting f32 ait des performances constantes.

Comparaison Assmbly sur x86: https://godbolt.org/g/AhdF71
La version branchless correspond très bien aux instructions minss / maxss.

Malheureusement, je n'ai pas pu faire en sorte que godbolt génère l'assemblage ARM à partir de Rust, mais voici une comparaison ARM des méthodes avec Clang: https://godbolt.org/g/s7ronw
Sans pouvoir tester le code et connaître beaucoup d'ARM: la taille du code semble également plus petite et LLVM génère principalement vmax / vmin, ce qui semble prometteur. Peut-être que LLVM pourrait éventuellement apprendre à plier la plupart du code en une seule instruction?

@ActuallyaDeviloper L' rustc que les conditionnelles imbriquées d'autres solutions (pour mémoire, je suppose que nous voulons générer des IR en ligne au lieu d'appeler une fonction d'élément lang). Merci beaucoup d'avoir écrit ceci.

J'ai une question sur u16_cast_clip2 : il ne semble pas gérer NaN?! Il y a un commentaire qui parle de NaN, mais je pense que la fonction transmettra NaN sans modification et tentera de le convertir en f32 (et même si ce n'était pas le cas, cela produirait l'une des valeurs limites plutôt que 0 ).

PS: Pour être clair, je n'essayais pas de laisser entendre qu'il est sans importance que le casting puisse être vectorisé. Il est clairement important que le code environnant soit par ailleurs vectorisable. Mais les performances scalaires sont également importantes, car la vectorisation n'est souvent pas applicable et les benchmarks sur lesquels je faisais des commentaires ne faisaient aucune déclaration sur les performances scalaires. Par intérêt, avez-vous vérifié l'asm des benchmarks *array* pour voir s'ils sont toujours vectorisés avec votre implémentation?

@rkruppe Vous avez raison, j'ai accidentellement échangé les côtés du if et j'ai oublié ça. f32 as u16 arrivé à faire la bonne chose en tronquant le 0x8000 supérieur, donc les tests ne l'ont pas non plus attrapé. J'ai résolu le problème maintenant en échangeant à nouveau les branches et en testant toutes les méthodes avec if (y.is_nan()) { panic!("NaN"); } cette fois.

J'ai mis à jour mon post précédent. Le code x86 n'a pas du tout changé de manière significative, mais malheureusement, le changement empêche LLVM de générer vmax dans le cas u16 ARM pour une raison quelconque. Je suppose que cela a à voir avec certains détails sur la gestion NaN de cette instruction ARM ou peut-être une limitation LLVM.

Pour savoir pourquoi cela fonctionne, notez que la valeur limite inférieure est en fait 0 pour les valeurs non signées. Ainsi, NaN et la borne inférieure peuvent être capturés en même temps.

Les versions de tableaux sont vectorisées.
Godbolt: https://godbolt.org/g/HnmsSV

Re: le ARM asm , je crois que la raison vmax laquelle qu'il renvoie NaN si l'un des opérandes est NaN . Le code est toujours sans branche, cependant, il utilise juste des mouvements prédiqués ( vmovgt , faisant référence au résultat du précédent vcmp avec 0).

Pour savoir pourquoi cela fonctionne, notez que la valeur limite inférieure est en fait 0 pour les valeurs non signées. Ainsi, NaN et la borne inférieure peuvent être capturés en même temps.

Ohhh, c'est vrai. Agréable.

Je suggérerais d'aller de l'avant en implémentant la saturation comme jette dans rustc, derrière un drapeau -Z

J'ai implémenté cela et je déposerai un PR une fois que j'aurai également corrigé # 41799 et que j'aurai beaucoup plus de tests.

45134 a signalé un chemin de code que j'ai manqué (génération d'expressions constantes LLVM - ceci est distinct de la propre évaluation constante de rustc). Je vais mettre un correctif pour cela dans le même PR, mais cela prendra un peu plus de temps.

@rkruppe Vous

La demande de tirage est terminée: # 45205

45205 a été fusionné, donc n'importe qui peut maintenant (enfin, en commençant par le soir suivant) mesurer l'impact de la saturation sur -Z saturating-float-casts via RUSTFLAGS . [1] De telles mesures seraient très utiles pour décider de la manière de traiter cette question.

[1] Strictement parlant, cela n'affectera pas les parties non génériques, non- #[inline] de la bibliothèque standard, donc pour être précis à 100%, vous voudrez construire localement std avec Xargo. Cependant, je ne m'attends pas à ce qu'il y ait beaucoup de code affecté par cela (les différents impls de trait de conversion sont #[inline] , par exemple).

@rkruppe Je suggère de démarrer une page internals / users pour collecter des données, dans la même veine que https://internals.rust-lang.org/t/help-us-benchmark-incremental-compilation/6153/ (on peut alors aussi liez les gens à cela, plutôt que des commentaires aléatoires dans notre suivi des problèmes)

@rkruppe, vous devez créer un problème de suivi. Cette discussion est déjà divisée en deux questions. Ce n'est pas bon!

@Gankro Oui, je suis d'accord, mais il faudra peut-être quelques jours avant que je trouve le temps d'écrire correctement ce message, alors j'ai pensé que je solliciterais les commentaires des personnes souscrites à ce numéro entre-temps.

@ est31 Hmm. Bien que le drapeau -Z couvre les deux directions de distribution (ce qui a peut-être été une erreur, rétrospectivement), il semble peu probable que nous allumions l'interrupteur sur les deux en même temps, et il y a peu de chevauchement entre les deux en termes de ce qui doit être discuté (par exemple, ce problème dépend de la performance de la saturation, alors que dans # 41799, il est convenu de la bonne solution).
Il est un peu ridicule que les benchmarks principalement ciblés sur ce problème mesurent également l'impact du correctif sur # 41799, mais cela peut tout au plus conduire à une surestimation des régressions de performances, donc je suis en quelque sorte d'accord avec cela. (Mais si quelqu'un est motivé pour diviser l'indicateur -Z en deux, allez-y.)

J'ai envisagé un problème de suivi pour la tâche de supprimer le drapeau une fois qu'il a dépassé son utilité, mais je ne vois pas la nécessité de fusionner les discussions qui se produisent ici et dans # 41799.

J'ai rédigé un article interne: https://gist.github.com/Gankro/feab9fb0c42881984caf93c7ad494ebd

N'hésitez pas à le copier, ou donnez-moi simplement des notes pour que je puisse le poster. (notez que je suis un peu confus à propos du comportement const fn )

Un autre détail est que le coût des conversions float-> int est spécifique à l'implémentation actuelle, plutôt que fondamental. Sur x86, cvtss2si cvttss2si renvoie 0x80000000 dans les cas trop bas, trop élevé et nan, donc on peut implémenter -Zsaturating-float-casts avec un cvtss2si cvttss2si suivi d'un code spécial dans le cas 0x80000000, il pourrait donc s'agir d'une seule branche de comparaison et de prévisibilité dans le cas courant. Sur ARM, vcvt.s32.f32 a déjà la sémantique -Zsaturating-float-casts . LLVM n'optimise actuellement pas les vérifications supplémentaires dans les deux cas.

@Gankro

Génial, merci beaucoup! J'ai laissé quelques notes sur l'essentiel. Après avoir lu ceci, j'aimerais essayer de séparer les lancers u128-> f32 du drapeau -Z. Juste pour se débarrasser de la mise en garde distrayante concernant le drapeau couvrant deux caractéristiques orthogonales.

(J'ai déposé # 45900 pour recentrer l'indicateur -Z afin qu'il ne couvre que le problème float-> int)

Ce serait bien si nous pouvions obtenir des implémentations spécifiques à la plate-forme à la @sunfishcode (au moins pour x86) avant de demander une analyse comparative de masse. Cela ne devrait pas être très difficile.

Le problème est que LLVM ne fournit actuellement pas de moyen de le faire, pour autant que je sache, sauf peut-être avec asm en ligne que je ne recommanderais pas nécessairement pour une version.

J'ai mis à jour le brouillon pour refléter la discussion (en gros, en supprimant toute mention en ligne de u128 -> f32 dans une section supplémentaire à la fin).

@sunfishcode Êtes-vous sûr? Le llvm.x86.sse.cvttss2si intrinsèque n'est-il pas ce que vous recherchez?

Voici un lien de terrain de jeu qui l'utilise:

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

En mode release, float_to_int_with_intrinsic et float_to_int_with_as se compilent tous deux en une seule instruction. (En mode débogage, float_to_int_with_intrinsic gaspille quelques instructions mettant zéro dans le haut, mais ce n'est pas trop mal.)

Il semble même faire un pliage constant correctement. Par exemple,

float_to_int_with_intrinsic(42.0)

devient

movl    $42, %eax

Mais une valeur hors limites,

float_to_int_with_intrinsic(42.0e33)

ne se plie pas:

cvttss2si   .LCPI2_0(%rip), %eax

(Idéalement, il se plierait à 0x80000000, mais ce n'est pas grave. L'important est qu'il ne produise pas undef.)

Oh cool. Il semble que cela fonctionnerait!

C'est cool de savoir que nous avons, après tout, un moyen de construire sur cvttss2si . Cependant, je ne suis pas d'accord qu'il soit clairement préférable de modifier l'implémentation pour l'utiliser avant d'appeler à des benchmarks:

La plupart des gens effectueront des tests sur x86, donc si nous avons un cas spécial x86, nous obtiendrons beaucoup moins de données sur l'implémentation générale, qui seront toujours utilisées sur la plupart des autres cibles. Certes, il est déjà difficile de déduire quoi que ce soit sur d'autres architectures, mais une implémentation totalement différente le rend carrément impossible.

Deuxièmement, si nous collectons des benchmarks maintenant, avec la solution "simple", et constatons qu'il n'y a pas de régressions de performances dans le code réel (et tbh c'est ce que j'attends), alors nous n'avons même pas besoin d'essayer optimiser davantage ce chemin de code.

Enfin, je ne suis même pas sûr que la construction sur cvttss2si sera plus rapide que ce que nous avons maintenant (bien que sur ARM, il est clairement préférable d'utiliser l'instruction appropriée):

  • Vous avez besoin d'une comparaison pour remarquer que la conversion renvoie 0x80000000, et si c'est le cas, vous avez toujours besoin d'une autre comparaison (de la valeur d'entrée) pour savoir si vous devez renvoyer int :: MIN ou int :: MAX. Et si c'est un type entier signé, je ne vois pas comment éviter une troisième comparaison pour distinguer NaN. Donc dans le pire des cas:

    • vous n'économisez pas dans le nombre de comparaisons / sélections

    • vous échangez une comparaison de flottant contre une comparaison int, ce qui pourrait être bien pour les cœurs OoO (si vous êtes goulot d'étranglement sur les FU qui peuvent faire des comparaisons, ce qui semble être un si relativement gros), mais cette comparaison dépend également du flottant -> int comparaison, alors que les comparaisons dans l'implémentation actuelle sont toutes indépendantes, il est donc loin d'être évident que ce soit une victoire.

  • La vectorisation devient probablement plus difficile voire impossible. Je ne m'attends pas à ce que le vectoriseur de boucle gère du tout cela intrinsèque.
  • Il convient également de noter que (AFAIK) cette stratégie ne s'applique qu'à certains types d'entiers. f32 -> u8, par exemple, nécessitera des corrections supplémentaires du résultat, ce qui rend cette stratégie assez clairement non rentable. Je ne sais pas trop quels types sont affectés par cela (par exemple, je ne sais pas s'il existe une instruction pour f32 -> u32), mais une application qui n'utilise que ces types n'en bénéficiera pas du tout.
  • Vous pouvez faire une solution de branchement avec une seule comparaison dans le chemin heureux (par opposition à deux ou trois comparaisons, et donc des branches, comme le faisaient les solutions précédentes). Cependant, comme @ActuallyaDeviloper l'a expliqué plus tôt, la

Est-il prudent de supposer que nous allons avoir besoin d'une flopée de unsafe fn as_u32_unchecked(self) -> u32 et d'amis indépendamment de ce que montre l'analyse comparative? Quel autre recours potentiel une personne aurait-elle si elle finissait par observer un ralentissement?

@bstrie Je pense qu'il serait plus logique, dans un cas comme celui-là, de faire quelque chose comme étendre la syntaxe à as <type> [unchecked] et exiger que unchecked ne soit présent que dans unsafe contextes.

Comme je le vois, une forêt de _unchecked fonctionne comme des variantes de as casting serait une verrue, à la fois en ce qui concerne l'intuitivité et lorsqu'il s'agit de générer une documentation propre et utilisable.

@ssokolow L'ajout de syntaxe doit toujours être un dernier recours, surtout si tout cela peut être pris en charge avec seulement dix fonctions par cœur. Même avoir un foo.as_unchecked::<u32>() générique serait préférable aux changements syntaxiques (et à l'interminable bikeshed concomitant), d'autant plus que nous devrions réduire, et non augmenter, le nombre de choses que unsafe déverrouille.

Point. Le turbofish m'a glissé dans la tête lors de l'examen des options et, avec le recul, je ne tire pas exactement sur tous les cylindres ce soir non plus, j'aurais donc dû être plus prudent lorsque je commente les décisions de conception.

Cela dit, il se sent mal d'incorporer le type de destination dans le nom de la fonction ... inélégant et un fardeau potentiel sur l'évolution future du langage. Le turbofish semble être une meilleure option.

Une méthode générique pourrait être prise en charge par un nouvel ensemble de traits UncheckedFrom / UncheckedInto avec les méthodes unsafe fn , rejoignant les méthodes From / Into et TryFrom / TryInto collection.

@bstrie Une solution alternative pour les personnes dont le code est devenu plus lent pourrait être d'utiliser un intrinsèque (par exemple, via stdsimd) pour accéder à l'instruction matérielle sous-jacente. J'ai fait valoir plus tôt que cela avait des inconvénients pour l'optimiseur - la vectorisation automatique en souffre probablement, et LLVM ne peut pas l'exploiter en retournant undef sur des entrées hors de portée - mais cela offre un moyen de faire le cast sans tout travail supplémentaire au moment de l'exécution. Je ne peux pas décider si cela est assez bon, mais il semble au moins plausible que ce soit le cas.

Quelques notes sur les conversions dans le jeu d'instructions x86:

SSE2 est en fait relativement limité dans les opérations de conversion qu'il vous donne. Vous avez:

  • Famille CVTTSS2SI avec registre 32 bits: convertit un flottant unique en i32
  • Famille CVTTSS2SI avec registre 64 bits: convertit un flottant unique en i64 (x86-64 uniquement)
  • Famille CVTTPS2PI: convertit deux flottants en deux i32s

Chacun de ceux-ci a des variantes pour f32 et f64 (ainsi que des variantes qui arrondissent au lieu de tronquer, mais c'est inutile ici).

Mais il n'y a rien pour les entiers non signés, rien pour les tailles inférieures à 32, et si vous êtes sur 32 bits x86, rien pour 64 bits. Les extensions de jeu d'instructions ultérieures ajoutent plus de fonctionnalités, mais il semble que presque personne ne compile pour celles-ci.

En conséquence, le comportement existant ('unsafe'):

  • Pour convertir en u32, les compilateurs convertissent en i64 et tronquent l'entier résultant. (Cela produit un comportement étrange pour les valeurs hors limites, mais c'est UB, alors qui s'en soucie.)
  • Pour convertir en quelque chose de 16 bits ou 8 bits, les compilateurs convertissent en i64 ou i32 et tronquent l'entier résultant.
  • Pour convertir en u64, les compilateurs génèrent un tas d'instructions. Pour f32 à u64, GCC et LLVM génèrent un équivalent de:
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)
    }
}

Fait amusant sans rapport: la génération de code "Convertir-que-tronquer" est ce qui cause le problème des " univers parallèles " dans Super Mario 64. Le code de détection de collision commence par une instruction MIPS pour convertir les coordonnées f32 en i32, puis tronque en i16; ainsi, les coordonnées qui tiennent dans i16 mais pas dans i32 «enveloppent», par exemple aller à la coordonnée 65536.0 vous permet de détecter une collision pour 0.0.

Quoi qu'il en soit, conclusions:

  • «Tester 0x80000000 et avoir un gestionnaire spécial» ne fonctionne que pour les conversions en i32 et i64.
  • Pour les conversions en u32, u / i16 et u / i8, cependant, "tester si la sortie tronquée / étendue de signe diffère de l'original" est un équivalent. (Cela ramasserait les deux entiers qui étaient dans la plage pour la conversion d'origine mais hors de la plage pour le type final, et 0x8000000000000000, l'indicateur que le flottant était NaN ou hors de la plage pour la conversion d'origine.)
  • Mais le coût d'une succursale et d'un tas de code supplémentaire pour ce cas est probablement excessif. Cela peut être OK si les branches peuvent être évitées.
  • L' approche basée sur minss / maxss de @ActuallyaDeviloper n'est pas si mal! La forme minimale,
minss %xmm2, %xmm1
maxss %xmm3, %xmm1
cvttss2si %rax, %xmm1

est seulement trois instructions (qui ont une taille de code et un débit / latence décents) et aucune branche.

Toutefois:

  • La version pure-Rust nécessite un test supplémentaire pour NaN. Pour les conversions en 32 bits ou moins, cela peut être évité en utilisant des valeurs intrinsèques, en utilisant cvttss2si 64 bits et en tronquant le résultat. Si l'entrée n'était pas NaN, les min / max garantissent que l'entier n'est pas modifié par la troncature. Si l'entrée était NaN, l'entier est 0x8000000000000000 qui tronque à 0.
  • Je n'ai pas inclus le coût de chargement de 2147483647.0 et -2148473648.0 dans les registres, généralement un mouvement de mémoire chacun.
  • Pour f32, 2147483647.0 ne peut pas être représenté exactement, donc cela ne fonctionne pas réellement: vous avez besoin d'une autre vérification. Cela rend les choses bien pires. Idem pour f64 à u / i64, mais f64 à u / i32 n'a pas ce problème.

Je propose un compromis entre les deux approches:

  • Pour f32 / f64 à u / i16 et u / i8, et f64 à u / i32, utilisez min / max + troncature, comme ci-dessus, par exemple:
    let f = if f > 32767.0 { 32767.0 } else { f };
    let f = if f < -32768.0 { -32768.0 } else { f };
    cvttss2si(f) as i16

(Pour u / i16 et u / i8, la conversion d'origine peut être en i32; pour f64 en u / i32, elle doit être en i64.)

  • Pour f32 / 64 à u32,
    let r = cvttss2si64(f) as u32;
    if f >= 4294967296.0 { 4294967295 } else { r }

est seulement quelques instructions et pas de branches:

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

Cela produit une séquence plus longue (toujours sans branche):

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

… Mais au moins on garde une comparaison par rapport à l'approche naïve, comme si f était trop petit, 0x8000000000000000 est déjà la bonne réponse (ie i64 :: MIN).

  • Pour f32 à i32, vous ne savez pas s'il serait préférable de faire la même chose que la précédente, ou simplement de convertir d'abord en f64, puis de faire la chose la plus courte min / max.

  • u64 est un gâchis auquel je n'ai pas envie de penser. : p

Dans https://internals.rust-lang.org/t/help-us-benchmark-saturating-float-casts/6231/14, quelqu'un a signalé un ralentissement mesurable et significatif de l'encodage JPEG avec la caisse d'image. J'ai minimisé le programme pour qu'il soit autonome et principalement concentré sur les parties liées au ralentissement: https://gist.github.com/rkruppe/4e7972a209f74654ebd872eb4bc57722 (ce programme montre un ralentissement d'environ 15% pour moi avec saturation moulages).

Notez que les transtypages sont f32-> u8 ( rgb_to_ycbcr ) et f32-> i32 ( encode_rgb , boucle "Quantization") dans des proportions égales. Il semble également que les entrées soient toutes dans la plage, c'est-à-dire que la saturation n'entre jamais en jeu, mais dans le cas du f32-> u8, cela ne peut être vérifié qu'en calculant le minimum et le maximum d'un polynominal et en tenant compte de l'erreur d'arrondi, qui c'est beaucoup demander. Les transtypages f32-> i32 sont plus évidemment dans la plage pour i32, mais uniquement parce que les éléments de self.tables sont différents de zéro, ce qui n'est (apparemment?) Pas si facile à montrer pour l'optimiseur, en particulier dans le programme original. tl; dr: Les contrôles de saturation sont là pour rester, le seul espoir est de les rendre plus rapides.

J'ai également piqué quelques-uns sur le LLVM IR - il semble littéralement que la seule différence réside dans les comparaisons et les sélections parmi les moulages saturants. Un coup d'œil rapide indique que l'asm a des instructions correspondantes et bien sûr un tas de valeurs plus en direct (ce qui conduit à plus de déversements).

@comex Pensez-vous que les moulages f32-> u8 et f32-> i32 peuvent être réalisés plus rapidement avec CVTTSS2SI?

Mise à jour mineure, à partir de rustc 1.28.0-nightly (952f344cd 2018-05-18) , l'indicateur -Zsaturating-float-casts entraîne toujours le code dans https://github.com/rust-lang/rust/issues/10184#issuecomment -345479698 à ~ 20 % plus lent sur x86_64. Ce qui signifie que LLVM 6 n'a rien changé.

| Drapeaux | Timing |
| ------- | -------: |
| -Copt-level = 3 -Ctarget-cpu = native | 325 699 ns / iter (+/- 7 607) |
| -Copt-level = 3 -Ctarget-cpu = natif -Zsaturating-float-cast | 386 962 ns / iter (+/- 11 601)
(19% plus lent) |
| -Copt-level = 3 | 331 521 ns / iter (+/- 14 096) |
| -Copt-level = 3 -Zsaturating-float-cast | 413572 ns / iter (+/- 19.183)
(25% plus lent) |

@kennytm Nous attendions-nous à ce que LLVM 6 change quelque chose? Discutent-ils d'une amélioration particulière qui profiterait à ce cas d'utilisation? Si oui, quel est le numéro du billet?

@insanitybit Il ... semble toujours être ouvert ...?

image

Welp, aucune idée de ce que je regardais. Merci!

@rkruppe n'avons-nous pas
(en changeant de documentation)?

Le 20 juillet 2018 04:31, "Colin" [email protected] a écrit:

Welp, aucune idée de ce que je regardais.

-
Vous recevez ceci parce que vous êtes abonné à ce fil.
Répondez directement à cet e-mail, affichez-le sur GitHub
https://github.com/rust-lang/rust/issues/10184#issuecomment-406462053 ,
ou muet
le fil
https://github.com/notifications/unsubscribe-auth/AApc0v3rJHhZMD7Kv7RC8xkGOiIhkGB1ks5uITMHgaJpZM4BJ45C
.

@nagisa Vous pensez peut-être à f32::from_bits(v: u32) -> f32 (et de même f64 )? Auparavant, il faisait une certaine normalisation des NaN, mais il ne s'agit plus que de transmute .

Ce problème concerne les conversions as qui tentent d'approcher la valeur numérique.

@nagisa Vous lancers float- > float, voir # 15536 ​​et https://github.com/rust-lang-nursery/nomicon/pull/65.

Ah, oui, c'était flotter pour flotter.

Le vendredi 20 juillet 2018, 12:24 Robin Kruppe [email protected] a écrit:

@nagisa https://github.com/nagisa Vous pensez peut-être à float-> float
cast, voir # 15536 https://github.com/rust-lang/rust/issues/15536 et
rust-lang-nursery / nomicon # 65
https://github.com/rust-lang-nursery/nomicon/pull/65 .

-
Vous recevez cela parce que vous avez été mentionné.
Répondez directement à cet e-mail, affichez-le sur GitHub
https://github.com/rust-lang/rust/issues/10184#issuecomment-406542903 ,
ou couper le fil
https://github.com/notifications/unsubscribe-auth/AApc0gA24Hz8ndnYhRXCyacd3HdUSZjYks5uIaHegaJpZM4BJ45C
.

Les notes de publication de LLVM 7 mentionnent quelque chose:

L'optimisation des moulages en virgule flottante est améliorée. Cela peut entraîner des résultats surprenants pour le code qui repose sur le comportement indéfini des conversions débordantes. L'optimisation peut être désactivée en spécifiant un attribut de fonction: "strict-float-cast-overflow" = "false". Cet attribut peut être créé par l'option clang -fno-strict-float-cast-overflow. Les désinfectants de code peuvent être utilisés pour détecter les modèles affectés. L'option clang pour détecter ce problème seul est -fsanitize = float-cast-overflow:

Cela a-t-il une incidence sur cette question?

Nous ne devrions pas nous soucier de ce que fait LLVM pour les lancers débordants, tant que ce n'est pas un comportement non défini dangereux. Le résultat peut être des ordures tant qu'il ne peut pas provoquer un comportement malsain.

Cela a-t-il une incidence sur cette question?

Pas vraiment. L'UB n'a pas changé, LLVM est devenu encore plus agressif dans son exploitation, ce qui permet d'être plus facilement affecté par celui-ci dans la pratique, mais le problème de solidité est inchangé. En particulier, le nouvel attribut ne supprime pas l'UB et n'affecte pas les optimisations qui existaient avant LLVM 7.

@rkruppe par curiosité, est-ce que ce genre de https://internals.rust-lang.org/t/help-us-benchmark-saturating-float-casts/6231/14 s'est assez bien passé et que l'implémentation n'a pas eu trop de bogues. Il semble qu'une légère régression des performances était toujours attendue, mais compiler correctement semble être un compromis valable.

Est-ce juste attendre d'être poussé à travers la ligne d'arrivée? Ou existe-t-il d'autres bloqueurs connus?

La plupart du temps, j'ai été distrait / occupé par d'autres choses, mais une régression x0.82 dans l'encodage JPEG RBG semble plus que «légère», une pilule plutôt amère à avaler (même s'il est rassurant que d'autres types de charge de travail ne semblent pas affectés) . Ce n'est pas assez grave pour que je m'oppose à l'activation de la saturation par défaut, mais assez pour que j'hésite à le pousser moi-même avant d'essayer le "fournir également une fonction de conversion plus rapide que la saturation mais pouvant générer des déchets (sûrs) "option discutée auparavant. Je ne suis pas arrivé à cela, et apparemment personne d'autre ne l'a fait non plus, donc cela est tombé au bord du chemin.

Ok cool merci pour la mise à jour @rkruppe! Je suis cependant curieux de savoir s'il existe réellement une implémentation de l'option de récupération des déchets? Je pourrais imaginer que nous fournissions facilement quelque chose comme unsafe fn i32::unchecked_from_f32(...) et autres, mais il semble que vous pensez que cela devrait être une fonction sûre . Est-ce possible avec LLVM aujourd'hui?

Il n'y a pas encore de freeze mais il est possible d'utiliser l'assemblage en ligne pour accéder aux instructions de l'architecture cible pour convertir les flottants en nombres entiers (avec une solution de secours par exemple pour saturer as ). Bien que cela puisse empêcher certaines optimisations, cela peut être suffisant pour corriger la régression dans certains benchmarks.

Une fonction unsafe qui conserve l'UB dont parle ce problème (et qui est codée de la même manière que as est aujourd'hui) est une autre option, mais beaucoup moins intéressante, je ' d préfère une fonction sûre si elle peut faire le travail.

Il y a également une marge d' amélioration significative

     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:

ce qui devrait être nettement plus rapide que ce que fait actuellement rustc .

Ok, je voulais juste être sûr de clarifier, merci! J'ai pensé que les solutions asm en ligne ne sont pas réalisables par défaut car elles inhiberaient trop les autres optimisations, mais je n'ai pas essayé moi-même. Je préférerais personnellement que nous fermions ce trou malsain en définissant un comportement raisonnable (comme exactement les lancers saturants d'aujourd'hui). Si nécessaire, nous pouvons toujours préserver l'implémentation rapide / défectueuse d'aujourd'hui en tant que fonction non sécurisée, et dans la limite de temps donnée aux ressources infinies, nous pouvons même améliorer considérablement la valeur par défaut et / ou ajouter d'autres fonctions de conversion spécialisées (comme une conversion sûre où hors limites n'est pas 'pas UB mais juste un motif de bit d'ordures)

D'autres seraient-ils opposés à une telle stratégie? Pensons-nous que ce n'est pas assez important pour être corrigé entre-temps?

Je pense que l'assemblage en ligne devrait être tolérable pour cvttsd2si (ou des instructions similaires) spécifiquement parce que cet asm en ligne n'accéderait pas à la mémoire ou n'aurait pas d'effets secondaires, donc c'est juste une boîte noire opaque qui peut être supprimée si elle n'est pas utilisée et ne le fait pas inhibent beaucoup les optimisations autour de lui, LLVM ne peut tout simplement pas raisonner sur les internes et la valeur de résultat de l'asm en ligne. Ce dernier élément est la raison pour laquelle je serais sceptique, par exemple, à propos de l'utilisation de l'asm en ligne pour la séquence de code suggérée par

D'autres seraient-ils opposés à une telle stratégie? Pensons-nous que ce n'est pas assez important pour être corrigé entre-temps?

Je ne m'oppose pas à l'idée de saturer maintenant et éventuellement d'ajouter des alternatives plus tard, je ne veux tout simplement pas être celui qui doit trouver le consensus pour cela et le justifier auprès des utilisateurs dont le code est devenu plus lent 😅

J'ai commencé un travail pour implémenter des intrinsèques pour saturer float en cast int dans LLVM: https://reviews.llvm.org/D54749

Si cela va quelque part, cela fournira un moyen relativement faible d'obtenir la sémantique saturante.

Comment reproduire ce comportement indéfini? J'ai essayé l'exemple dans le commentaire mais le résultat était 255 , ce qui me semble correct:

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

Un comportement non défini ne peut pas être observé de manière fiable de cette façon, parfois il vous donne ce que vous attendez, mais dans des situations plus complexes, il s'effondre.

En bref, le moteur de génération de code (LLVM) que nous utilisons est autorisé à supposer que cela ne se produit pas, et donc il peut générer un mauvais code s'il s'appuie un jour sur cette hypothèse.

@ AaronM04, un exemple de comportement indéfini reproductible a été publié sur reddit aujourd'hui:

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

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

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

(voir aire de jeux )

Je suppose que ce dernier commentaire était destiné à @ AaronM04 , en référence à leur commentaire précédent .

"Oh, c'est assez facile alors."

  • @pcwalton , 2014

Désolé, j'ai lu très attentivement toute cette histoire de 6 ans de bonnes intentions. Mais, sérieusement, 6 longues années sur 10 !!! Si cela avait été un forum de politiciens, on aurait prévu un sabotage flamboyant ici.

Alors, s'il vous plaît, quelqu'un peut-il expliquer, en termes simples, en quoi le processus de recherche d'une solution est-il plus intéressant que la solution elle-même?

Parce que c'est plus difficile qu'il n'y paraissait au départ et qu'il faut des changements LLVM.

Ok, mais ce n'est pas Dieu qui a créé cette LLVM lors de sa deuxième semaine, et en allant dans la même direction, il faudra peut-être encore 15 ans pour résoudre ce problème fondamental.

Vraiment, je n'ai aucune attention à blesser quelqu'un, et je suis nouveau dans l'infrastructure Rust pour aider soudainement, mais quand j'ai appris ce cas, j'ai été stupéfait.

Cet outil de suivi des problèmes permet de discuter de la manière de résoudre ce problème et de déclarer que l'évidence ne fait aucun progrès dans cette direction. Donc, si vous voulez aider à résoudre le problème ou avoir de nouvelles informations à contribuer, faites-le, mais sinon vos commentaires ne feront pas apparaître le correctif par magie. :)

Je pense que l'hypothèse selon laquelle cela nécessite des changements dans LLVM est prématurée.

Je pense que nous pouvons le faire dans la langue avec un coût de performance minimal. Serait-ce un changement radical * * oui, mais cela pourrait être fait et devrait être fait.

Ma solution serait de définir float to int cast comme unsafe puis de fournir des fonctions d'aide dans la bibliothèque standard pour fournir des résultats liés dans Result Types.

C'est un correctif non sexy et c'est un changement radical, mais en fin de compte, c'est ce que chaque développeur doit coder lui-même pour contourner déjà l'UB existant. C'est la bonne approche contre la rouille.

Merci, @RalfJung , de me le faire comprendre. Je n'avais aucune intention d'insulter qui que ce soit ou d'intervenir avec mépris dans le processus productif de brainstorming. Étant nouveau dans la rouille, c'est vrai, je ne peux pas faire grand chose. Néanmoins, cela m'aide, ainsi que peut-être d'autres, qui essaient de pénétrer dans la rouille, d'en savoir plus sur ses défauts non résolus et de faire le résultat pertinent: vaut-il la peine de creuser plus profondément ou il vaut mieux choisir autre chose pour l'instant. Mais je suis déjà heureux que la suppression de "mes commentaires inutiles" soit beaucoup plus facile.

Comme indiqué précédemment dans le fil de discussion, cela est lentement mais sûrement corrigé de la bonne façon en fixant llvm pour prendre en charge la sémantique nécessaire, comme les équipes concernées l'ont depuis longtemps convenu.

Rien de plus ne peut vraiment être ajouté à cette discussion.

https://reviews.llvm.org/D54749

@nikic On dirait que les progrès du côté de LLVM sont au point mort, pourriez-vous donner une brève mise à jour si possible? Merci.

La distribution saturante peut-elle être implémentée en tant que fonction de bibliothèque dans laquelle les utilisateurs pourraient opter s'ils sont prêts à prendre une régression de préférence pour obtenir un son? Je lis l'implémentation du compilateur mais cela semble assez subtil:

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

Nous pourrions exposer un intrinsèque qui génère l'IR LLVM pour la saturation (que ce soit l'IR à code ouvert actuel ou llvm.fpto[su]i.sat dans le futur) indépendamment de l'indicateur -Z . Ce n'est pas du tout difficile à faire.

Cependant, je me demande si c'est le meilleur plan d'action. Quand (si?) La saturation devient la sémantique par défaut de as casts, une telle API devient redondante. Il semble également déplaisant de dire aux utilisateurs qu'ils doivent choisir eux-mêmes s'ils veulent de la solidité ou des performances, même si ce n'est que temporaire.

Dans le même temps, la situation actuelle est clairement encore pire. Si nous envisageons d'ajouter des API de bibliothèque, je préviens de plus en plus d'activer simplement la saturation par défaut et d'offrir un unsafe intrinsèques qui a UB sur NaN et des nombres hors de portée (et diminue à un simple fpto[su]i ). Cela offrirait toujours fondamentalement le même choix, mais par défaut, la nouvelle API ne deviendrait probablement pas redondante à l'avenir.

Le passage au son par défaut sonne bien. Je pense que nous pouvons offrir l'intrinsèque paresseusement sur demande plutôt que dès le départ. Aussi, est-ce que const eval fera également la saturation dans ce cas? (cc @RalfJung @eddyb @ oli-obk)

Const eval nous faisons déjà la saturation et cela depuis des lustres, je pense même avant miri (je me souviens distinctement de l'avoir changé dans l'ancien évaluateur basé sur llvm::Constant ).

@rkruppe Génial! Puisque vous connaissez le code en question, aimeriez-vous être le fer de lance du changement des valeurs par défaut?

@rkruppe

Nous pourrions exposer un intrinsèque qui génère le LLVM IR pour la saturation

Cela peut nécessiter 10 ou 12 éléments intrinsèques distincts, pour chaque combinaison de type source et destination.

@Centril

Le passage au son par défaut sonne bien. Je pense que nous pouvons offrir l'intrinsèque paresseusement sur demande plutôt que dès le départ.

Je suppose que contrairement à d'autres commentaires, «l'intrinsèque» dans votre commentaire signifie quelque chose qui aurait moins de régression préférentielle lorsque as saturation.

Je ne pense pas que ce soit une bonne approche pour traiter les régressions importantes connues . Pour certains utilisateurs, la perte de performances peut être un réel problème, tandis que leur algorithme garantit que l'entrée est toujours à portée. S'ils ne sont pas abonnés à ce fil, ils pourraient se rendre compte qu'ils ne sont affectés que lorsque le changement atteint le canal stable. À ce stade, ils peuvent être bloqués pendant 6 à 12 semaines, même si nous obtenons une API non sécurisée immédiatement sur demande.

Je préfère de loin que nous suivions le modèle déjà établi pour les avertissements d'obsolescence: n'effectuez le changement dans Nightly qu'une fois l'alternative disponible dans Stable pendant un certain temps.

Cela peut nécessiter 10 ou 12 éléments intrinsèques distincts, pour chaque combinaison de type source et destination.

Bien, tu m'as, mais je ne vois pas en quoi c'est pertinent? Soit 30 intrinsèques, il est toujours trivial de les ajouter. Mais en réalité, il est encore plus facile d'avoir un seul intrinsèque générique utilisé par N wrappers minces. Le nombre ne change pas non plus si nous choisissons l'option "make as et introduisons une API unsafe cast".

Je ne pense pas que ce soit une bonne approche pour traiter les régressions importantes _connues_. Pour certains utilisateurs, la perte de performances peut être un réel problème, tandis que leur algorithme garantit que l'entrée est toujours à portée. S'ils ne sont pas abonnés à ce fil, ils pourraient se rendre compte qu'ils ne sont affectés que lorsque le changement atteint le canal stable. À ce stade, ils peuvent être bloqués pendant 6 à 12 semaines, même si nous obtenons une API non sécurisée immédiatement sur demande.

+1

Je ne sais pas si la procédure pour les avertissements d'obsolescence (obsolète uniquement la nuit une fois que le remplacement est stable) est nécessaire car il semble moins important de rester sans régression sur tous les canaux de publication que de rester sans avertissement sur tous les canaux de publication. , mais là encore, attendre 12 semaines de plus est essentiellement une erreur d'arrondi avec la durée de ce problème.

Nous pouvons également laisser le -Zsaturating-float-casts autour (en changeant simplement la valeur par défaut), ce qui signifie que tous les utilisateurs nocturnes peuvent toujours désactiver la cange pendant un certain temps.

(Oui, le nombre d'intrinsèques n'est qu'un détail d'implémentation et n'a pas été conçu comme un argument pour ou contre quoi que ce soit.)

@rkruppe Je ne peux pas prétendre avoir digéré tous les commentaires ici, mais je suis sous l'impression que LLVM maintenant a une instruction de gel, qui était l'élément bloquant le « chemin le plus court » à éliminer UB ici, non?

Bien que je suppose que freeze est si nouveau qu'il n'est peut-être pas disponible dans notre propre version de LLVM, n'est-ce pas? Pourtant, cela semble être quelque chose sur lequel nous devrions explorer le développement, peut-être au cours du premier semestre de 2020?

Nomination pour discussion lors de la réunion du T-compilateur, pour essayer d'obtenir un consensus approximatif sur notre chemin souhaité à ce stade.

L'utilisation de freeze est toujours problématique pour toutes les raisons mentionnées ici . Je ne sais pas dans quelle mesure ces préoccupations sont réalistes avec l'utilisation du gel pour ces moulages, mais en principe, elles s'appliquent. En gros, attendez-vous freeze ce que

Et de toute façon, même renvoyer des ordures aléatoires semble plutôt mauvais pour un casting as . Il est logique d'avoir des opérations plus rapides pour la vitesse là où c'est nécessaire, similaire à unchecked_add , mais faire que la valeur par défaut semble assez fortement contraire à l'esprit de Rust.

@SimonSapin, vous avez d'abord proposé l'approche opposée (par défaut, la sémantique est mauvaise / "bizarre" et fournissez une méthode explicitement saine); Je ne peux pas dire à partir de vos commentaires ultérieurs si vous pensez que le défaut de validité (après une période de transition appropriée) est également raisonnable / meilleur?

@pnkfelix

J'ai l'impression que LLVM a maintenant une instruction de gel, qui était l'élément bloquant le "chemin le plus court" pour éliminer UB ici, non?

Il y a quelques mises en garde. Plus important encore, même si tout ce qui nous importe est de nous débarrasser d'UB et que nous mettons à jour notre LLVM fourni pour inclure freeze (ce que nous pourrions faire à tout moment), nous prenons en charge plusieurs versions plus anciennes (retour à LLVM 6 à la moment) et nous aurions besoin d'une implémentation de secours pour que ceux-ci se débarrassent de l'UB pour tous les utilisateurs.

Deuxièmement, bien sûr, est la question de savoir si «tout simplement pas UB» est tout ce qui nous importe pendant que nous y sommes. En particulier, je tiens à souligner à nouveau qu'un freeze(fptosi %x) se comporte @RalfJung l'a dit) à chaque fois qu'il est exécuté. Je ne veux pas en débattre à nouveau maintenant, mais cela vaut la peine d'être considéré lors de la réunion si nous préférons faire un peu plus de travail pour faire de la saturation la valeur par défaut et des conversions non cochées (soit dangereuses, soit freeze -using) l'option non par défaut.

@RalfJung Ma position est qu'il vaut mieux éviter as indépendamment de ce problème, car il peut avoir une sémantique très différente (tronquer, saturer, arrondir,…) en fonction du type d'entrée et de sortie, et ce ne sont pas toujours évident lors de la lecture du code. (Même ce dernier peut être déduit avec foo as _ .) J'ai donc un projet de pré-RFC pour proposer diverses méthodes de conversion explicitement nommées qui couvrent les cas que as fait aujourd'hui (et peut-être plus) .

Je pense que as ne devrait certainement pas avoir UB, car il peut être utilisé en dehors de unsafe . Le retour des ordures ne sonne pas non plus. Mais nous devrions probablement avoir une sorte d'atténuation / de transition / d'alternative pour les cas connus de régression des performances causés par une saturation de la distribution. J'ai seulement posé des questions sur une implémentation de bibliothèque de distribution saturante afin de ne pas bloquer ce projet de RFC sur cette transition.

@SimonSapin

Ma position est qu'il vaut mieux éviter complètement ce problème, car il peut avoir une sémantique très différente (tronquer, saturer, arrondir,…)

D'accord. Mais cela ne nous aide pas vraiment avec ce problème.

(De plus, je suis heureux que vous travailliez à rendre as inutile. J'attends cela avec impatience.: D)

Je pense que cela ne devrait certainement pas avoir UB, car il peut être utilisé en dehors de dangereux. Le retour des ordures ne sonne pas non plus. Mais nous devrions probablement avoir une sorte d'atténuation / de transition / d'alternative pour les cas connus de régression des performances causés par une saturation de la distribution. J'ai seulement posé des questions sur une implémentation de bibliothèque de distribution saturante afin de ne pas bloquer ce projet de RFC sur cette transition.

Nous semblons donc convenir que l'état final devrait être que float-to-int as sature? Je suis satisfait de tout plan de transition tant que c'est l'objectif final vers lequel nous nous dirigeons.

Cet objectif final me semble bon.

Je ne pense pas que ce soit une bonne approche pour traiter les régressions importantes _connues_. Pour certains utilisateurs, la perte de performances peut être un réel problème, tandis que leur algorithme garantit que l'entrée est toujours à portée. S'ils ne sont pas abonnés à ce fil, ils pourraient se rendre compte qu'ils ne sont affectés que lorsque le changement atteint le canal stable. À ce stade, ils peuvent être bloqués pendant 6 à 12 semaines, même si nous obtenons une API non sécurisée immédiatement sur demande.

À mon avis, ce ne serait pas la fin du monde si ces utilisateurs attendent de mettre à niveau leur rustc pendant ces 6 à 12 semaines - ils pourraient ne pas avoir besoin de quoi que ce soit des versions à venir dans les deux cas, ou leurs bibliothèques peuvent avoir des contraintes MSRV pour soutenir.

Pendant ce temps, les utilisateurs, qui ne sont pas non plus abonnés au thread, peuvent subir des erreurs de compilation tout comme ils risquent de subir des pertes de performances. Lequel devrions-nous prioriser? Nous donnons des garanties sur la stabilité et nous donnons des garanties sur la sécurité - mais à ma connaissance, aucune garantie de ce type n'est donnée sur les performances (par exemple, la RFC 1122 ne mentionne pas du tout la performance).

Je préfère de loin que nous suivions le modèle déjà établi pour les avertissements d'obsolescence: n'effectuez le changement dans Nightly qu'une fois l'alternative disponible dans Stable pendant un certain temps.

Dans le cas des avertissements de dépréciation, la conséquence d'attendre avec la dépréciation jusqu'à ce qu'il y ait une alternative stable n'est pas, du moins pour autant que je sache, des trous de solidité pendant la période d'attente. (De plus, bien que les intrinsèques puissent être fournis ici, dans le cas général, nous pourrions ne pas être en mesure de proposer raisonnablement des alternatives lors de la correction des trous de solidité. Donc je ne pense pas qu'avoir des alternatives sur stable puisse être une exigence difficile.)

Bien, tu m'as, mais je ne vois pas en quoi c'est pertinent? Soit 30 intrinsèques, il est toujours trivial de les ajouter. Mais en réalité, il est encore plus facile d'avoir un seul intrinsèque générique utilisé par N wrappers minces. Le nombre ne change pas non plus si nous choisissons l'option "make as et introduisons une API unsafe cast".

Cet intrinsèque générique unique ne nécessitera-t-il pas des implémentations séparées dans le compilateur pour ces instanciations monomorphes spécifiques 12/30?

Il peut être trivial d'ajouter des éléments intrinsèques au compilateur, car LLVM a déjà effectué la plupart du travail, mais c'est également loin du coût total. De plus, il y a l'implémentation dans Miri, Cranelift, ainsi que le travail éventuel nécessaire dans une spécification. Je ne pense donc pas que nous devrions ajouter des éléments intrinsèques au cas où quelqu'un en aurait besoin.

Je ne suis cependant pas opposé à exposer plus d'intrinsèques, mais si quelqu'un en a besoin, il devrait faire une proposition (par exemple en tant que PR avec une description élaborée), et justifier l'ajout avec des chiffres de référence ou d'autres.

Nous pouvons également laisser le -Zsaturating-float-casts autour (en changeant simplement la valeur par défaut), ce qui signifie que tous les utilisateurs nocturnes peuvent toujours désactiver la cange pendant un certain temps.

Cela me semble correct, mais je suggérerais de renommer le drapeau en -Zunsaturating-float-casts pour éviter de changer la sémantique vers un défaut de validité pour ceux qui utilisent déjà ce drapeau.

@Centril

Cet intrinsèque générique unique ne nécessitera-t-il pas des implémentations séparées dans le compilateur pour ces instanciations monomorphes spécifiques 12/30?

Non, la plupart des implémentations peuvent être et sont déjà partagées en paramétrant les largeurs de bits source et destination. Seuls quelques bits nécessitent des distinctions de cas. La même chose s'applique à l'implémentation dans miri et très probablement aussi à d'autres implémentations et aux spécifications.

(Edit: pour être clair, ce partage peut se produire même s'il y a N intrinsèques distincts, mais un seul intrinsèque générique réduit le passe-partout requis par intrinsèque.)

Je ne pense donc pas que nous devrions ajouter des éléments intrinsèques au cas où quelqu'un en aurait besoin.

Je ne suis cependant pas opposé à exposer plus d'intrinsèques, mais si quelqu'un en a besoin, il devrait faire une proposition (par exemple en tant que PR avec une description élaborée), et justifier l'ajout avec des chiffres de référence ou d'autres. Je ne pense pas que cela devrait bloquer la réparation du trou de solidité entre-temps.

Nous avons déjà des chiffres de référence. Nous savons d'après l'appel à des benchmarks il y a longtemps que l' encodage JPEG est nettement plus lent sur x86_64 avec des castes saturantes. Quelqu'un pourrait les réexécuter, mais je suis confiant en prédisant que cela n'a pas changé (bien que les chiffres spécifiques ne soient bien sûr pas identiques) et ne vois aucune raison pour laquelle des changements futurs dans la façon dont la saturation est implémentée (comme le passage à l'asm en ligne ou le LLVM intrinsics @nikic a travaillé sur) changerait fondamentalement cela. Bien qu'il soit difficile d'être sûr de l'avenir, je suppose que le seul moyen plausible de récupérer ces performances est d'utiliser quelque chose qui génère du code sans vérification de plage, comme une conversion unsafe ou quelque chose utilisant freeze .

OK, donc à partir des chiffres de référence existants, il semble qu'il y ait un désir actif pour lesdits intrinsèques. Dans l'affirmative, je proposerais le plan d'action suivant:

  1. Simultanément:

    • Présentez les intrinsèques exposés tous les soirs grâce aux fonctions #[unstable(...)] .

    • Supprimez -Zsaturating-float-casts et introduisez -Zunsaturating-float-casts .

    • Remplacez la valeur par défaut par ce que fait -Zsaturating-float-casts .

  2. Nous stabilisons les intrinsèques après un certain temps; nous pouvons accélérer un peu.
  3. Supprimez -Zunsaturating-float-casts après un certain temps.

Ça m'a l'air bien. Sauf que les intrinsèques sont les détails d'implémentation de certaines API publiques, probablement des méthodes sur f32 et f64 . Ils pourraient être soit:

  • Méthodes d'un trait générique (avec un paramètre pour le type de retour entier de la conversion), éventuellement dans le prélude
  • Méthodes inhérentes avec un trait de soutien (similaire à str::parse et FromStr ) afin de prendre en charge différents types de retour
  • Plusieurs méthodes inhérentes non génériques avec le type de cible dans le nom

Oui, je voulais dire exposer les intrinsèques via des méthodes ou d'autres.

Plusieurs méthodes inhérentes non génériques avec le type de cible dans le nom

Cela ressemble à la chose habituelle que nous faisons - des objections à cette option?

Vraiment? J'ai le sentiment que lorsque le nom d'un type (de la signature) fait partie du nom d'une méthode, ce sont des conversions ad hoc "uniques en leur genre" (comme Vec::as_slice et [T]::to_vec ) , ou une série de conversions où la différence n'est pas un type (comme to_ne_bytes , to_be_bytes , to_le_bytes ). Mais une partie de la motivation des traits de std::convert était d'éviter des dizaines de méthodes séparées comme u8::to_u16 , u8::to_u32 , u8::to_u64 , etc.

Je me demande si cela serait naturellement généralisable à un trait étant donné que les méthodes doivent être unsafe fn . Si nous ajoutons des méthodes inhérentes, vous pouvez toujours déléguer à celles des implémentations de trait et ainsi de suite.

Il me semble étrange d'ajouter des traits pour les conversions non sécurisées, mais je suppose que Simon pense probablement au fait que nous aurions peut-être besoin d'une méthode différente pour chaque combinaison de type virgule flottante et entier (par exemple f32::to_u8_unsaturated , f32::to_u16_unsaturated , etc.).

Ne pas peser sur un long fil de discussion que je n'ai pas lu dans l'ignorance totale, mais est-ce souhaité ou est-ce suffisant d'avoir par exemple f32::to_integer_unsaturated qui se transforme en u32 ou quelque chose? Existe-t-il un choix évident pour le type de cible pour la conversion non sécurisée?

Fournir des conversions non sécurisées uniquement à i32 / u32 (par exemple) exclut complètement tous les types d'entiers dont la plage de valeurs n'est pas strictement plus petite, ce qui est certainement parfois nécessaire. Aller plus petit (jusqu'à u8, comme dans le codage JPEG) est également souvent nécessaire, mais peut être émulé en convertissant vers un type entier plus large et en tronquant avec as (ce qui est bon marché mais généralement pas gratuit).

Mais nous ne pouvons pas très bien fournir uniquement la conversion vers la plus grande taille entière. Ceux-ci ne sont pas toujours supportés nativement (donc, lents) et les optimisations ne peuvent pas résoudre cela: il n'est pas judicieux d'optimiser «convertir en grand int, puis tronquer» en «convertir directement en int plus petit» car ce dernier a UB (dans LLVM IR) / résultats différents (au niveau du code machine, sur la plupart des architectures) dans les cas où le résultat de la conversion d'origine aurait été bouclé lors de la troncature.

Notez que même exclure pragmatiquement les entiers 128 bits et se concentrer sur les entiers 64 bits sera toujours mauvais pour les cibles 32 bits communes.

Je suis nouveau dans cette conversation mais pas dans la programmation. Je suis curieux de savoir pourquoi les gens pensent que les conversions saturantes et la conversion de NaN à zéro sont des comportements par défaut raisonnables. Je comprends que Java fait cela (bien que l'enroulement semble beaucoup plus courant), mais il n'y a pas de valeur entière pour laquelle NaN peut vraiment être considéré comme une conversion correcte. De même, convertir 1000000.0 en 65535 (u16), par exemple, semble incorrect. Il n'y a tout simplement pas de U16 qui est clairement la bonne réponse. Au moins, je ne le vois pas comme étant meilleur que le comportement actuel de le convertir en 16960, qui est au moins un comportement partagé avec C / C ++, C #, go et autres, et donc au moins quelque peu surprenant.

Diverses personnes ont commenté la similitude avec la vérification des débordements, et je suis d'accord avec elles. C'est également similaire à la division entière par zéro. Je pense que les conversions invalides devraient paniquer, tout comme l'arithmétique invalide. Se fier à NaN -> 0 et 1000000.0 -> 65535 (ou 16960) semble tout aussi sujet aux erreurs que se fier à un dépassement d'entier ou à un hypothétique n / 0 == 0. C'est le genre de chose qui devrait produire une erreur par défaut. (Dans les versions de version, rust peut éluder la vérification des erreurs, tout comme il le fait avec l'arithmétique des nombres entiers.) Et dans les rares cas où vous _voulez_ convertir NaN en zéro ou avoir une saturation en virgule flottante, vous devriez avoir à y participer, tout comme vous devez opter pour le dépassement d'entier.

En ce qui concerne les performances, il semble que les performances générales les plus élevées proviendraient d'une conversion simple et de l'utilisation de défauts matériels. Les deux x86 et ARM, par exemple, lèvent des exceptions matérielles lorsqu'une conversion virgule flottante en entier ne peut pas être représentée correctement (y compris les cas NaN et hors limites). Cette solution est sans coût, sauf pour les conversions invalides, sauf lors de la conversion directe de types à virgule flottante en petits entiers dans les versions de débogage - un cas rare - où elle devrait encore être relativement bon marché. (Sur le matériel théorique qui ne prend pas en charge ces exceptions, il peut alors être émulé dans le logiciel, mais encore une fois uniquement dans les versions de débogage.) J'imagine que les exceptions matérielles sont exactement comment la détection de la division entière par zéro est implémentée aujourd'hui. J'ai beaucoup parlé de LLVM, alors peut-être que vous êtes contraint ici, mais il serait malheureux d'avoir une émulation logicielle dans chaque conversion en virgule flottante, même dans les versions de version afin de fournir des comportements alternatifs douteux pour des conversions intrinsèquement invalides.

@admilazz Nous sommes contraints par ce que LLVM peut faire, et actuellement LLVM n'expose pas de méthode pour convertir efficacement les flottants en entiers sans risque de comportement indéfini.

La saturation est due au fait que le langage définit as casts pour toujours réussir, et nous ne pouvons donc pas changer l'opérateur pour paniquer à la place.

De même, convertir 1000000.0 en 65535 (u16), par exemple, semble incorrect. Il n'y a tout simplement pas de U16 qui est clairement la bonne réponse. Au moins, je ne le vois pas comme étant meilleur que le comportement actuel de le convertir en 16960,

Ce n'était pas évident pour moi, alors je pense que cela vaut la peine de le souligner: 16960 est le résultat de la conversion de 1000000.0 en un entier suffisamment large, puis de la troncature pour conserver les 16 bits inférieurs.

Ce n'est ~ pas une option qui a été suggérée auparavant dans ce fil, et c'est ~ (Edit: je me suis trompé ici, désolé je ne l'ai pas trouvé) pas non plus le comportement actuel. Le comportement actuel dans Rust est que la conversion de virgule flottante en entier hors plage est un comportement indéfini. En pratique, cela conduit souvent à une valeur de garbage, en principe cela pourrait entraîner des erreurs de compilation et des vulnérabilités. Ce fil a pour but de résoudre ce problème. Lorsque j'exécute le programme ci-dessous dans Rust 1.39.0, j'obtiens une valeur différente à chaque fois:

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

Aire de jeux . Exemple de sortie:

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

Personnellement, je pense que la troncature de type entier n'est ni meilleure ni pire que la saturation, elles sont toutes deux numériquement fausses pour les valeurs hors plage. Une conversion infaillible a sa place, du moment qu'elle est déterministe et non UB. Vous savez peut-être déjà à partir de votre algorithme que les valeurs sont comprises dans la plage ou ne vous souciez pas de tels cas.

Je pense que nous devrions également ajouter des API de conversion faillibles qui renvoient un Result , mais j'ai encore besoin de finir d'écrire ce brouillon pré-RFC :)

La sémantique "convertir en entier mathématique, puis tronquer à la largeur cible" ou "wraparound" a déjà été suggérée dans ce fil de discussion (https://github.com/rust-lang/rust/issues/10184#issuecomment-299229143). Je n'aime pas particulièrement ça:

  • Je pense que c'est un peu moins sensible que la saturation. La saturation ne donne généralement pas de résultats raisonnables pour des nombres très éloignés de la plage, mais:

    • il se comporte plus raisonnablement que le bouclage lorsque les nombres sont légèrement hors de portée (par exemple en raison d'une erreur d'arrondi accumulée). En revanche, une conversion qui s'enroule peut amplifier une légère erreur d'arrondi dans le calcul du flottant à l'erreur maximale possible dans le domaine entier.

    • il est un peu couramment utilisé dans le traitement du signal numérique, il y a donc au moins certaines applications où cela est réellement souhaité. En revanche, je ne connais pas un seul algorithme qui bénéficie d'une sémantique enveloppante.

  • AFAIK la seule raison de préférer la sémantique enveloppante est l'efficacité de l'émulation logicielle, mais cela me semble une hypothèse non prouvée. Je serais heureux d'avoir tort, mais à un coup d'œil rapide, le wraparound semble nécessiter une si longue chaîne d'instructions ALU (plus des branches pour gérer les infinis et les NaN séparément) que je n'ai pas l'impression qu'il est clair que l'un sera clairement meilleur pour performance que l’autre.
  • Alors que la question de savoir quoi faire pour NaN est un vilain problème pour toute conversion en entier, la saturation au moins ne nécessite aucune casse particulière (ni dans la sémantique ni dans la plupart des implémentations) pour l'infini. Mais pour wraparound, quel est l'équivalent entier est +/- infini supposé être? JavaScript dit que c'est 0, et je suppose que si nous faisons paniquer as sur NaN, cela pourrait aussi paniquer à l'infini, mais dans tous les cas, cela semble rendre le bouclage plus difficile à faire rapidement que de regarder des nombres normaux et dénormaux seul suggérerait.

Je soupçonne que la plupart du code régressé par la sémantique de saturation pour la conversion serait mieux d'utiliser SIMD. Ainsi, bien que malheureux, ce changement n'empêchera pas l'écriture de code haute performance (surtout si des éléments intrinsèques avec une sémantique différente sont fournis), et pourrait même pousser certains projets vers une implémentation plus rapide (bien que moins portable).

Si tel est le cas, de légères régressions de performances ne doivent pas être utilisées comme justification pour éviter de fermer un trou de solidité.

https://github.com/rust-lang/rust/pull/66841 ajoute les méthodes unsafe fn qui convertissent avec les fptoui et fptosi LLVM, pour les cas où les valeurs sont connues être dans la plage et saturer est une régression de performance mesurable.

Après cela, je pense que c'est bien de changer la valeur par défaut pour as (et peut-être ajouter un autre drapeau -Z pour se désinscrire?), Bien que cela devrait probablement être une décision formelle de l'équipe Lang.

Après cela, je pense que c'est bien de changer la valeur par défaut pour as (et peut-être ajouter un autre drapeau -Z pour se désinscrire?), Bien que cela devrait probablement être une décision formelle de l'équipe Lang.

Nous (l'équipe linguistique, avec les personnes qui étaient là au moins) en avons donc discuté sur https://github.com/rust-lang/lang-team/blob/master/minutes/2019-11-21.md et nous avons pensé ajouter de nouveaux éléments intrinsèques + ajouter -Zunsaturated-float-casts serait de bonnes premières étapes.

Je pense qu'il serait bon de changer la valeur par défaut dans le cadre de cela ou peu de temps après, éventuellement avec FCP si nécessaire.

Je suppose que par nouveaux intrinsèques, vous entendez quelque chose comme https://github.com/rust-lang/rust/pull/66841

Que signifie ajouter -Z unsaturated-float-casts sans changer la valeur par défaut? Acceptez-le comme no-op plutôt que d'émettre "erreur: option de débogage inconnue"?

Je suppose que par nouveaux intrinsèques, vous voulez dire quelque chose comme # 66841

Oui 👍 - merci d'avoir été le fer de lance.

Que signifie ajouter -Z unsaturated-float-casts sans changer la valeur par défaut? Acceptez-le comme no-op plutôt que d'émettre "erreur: option de débogage inconnue"?

Ouais en gros. Alternativement, nous supprimons -Z saturated-float-casts en faveur de -Z unsaturated-float-casts et changeons la valeur par défaut directement, mais cela devrait conduire au même résultat sur moins de PR.

Je ne comprends vraiment pas la suggestion «non saturée». Si l'objectif est simplement de fournir un bouton pour désactiver la nouvelle valeur par défaut, il est plus facile de simplement modifier la valeur par défaut de l'indicateur existant et de ne rien faire de plus. Si le but est de choisir un nouveau nom qui soit plus clair sur le compromis (non-sens), alors "insaturé" est horrible à cela - je suggérerais plutôt un nom qui inclut "unsafe" ou "UB" ou similaire mot effrayant, par exemple -Z fix-float-cast-ub .

unchecked est le terme avec un précédent dans les noms d'API.

@admilazz Nous sommes contraints par ce que LLVM peut faire, et actuellement LLVM n'expose pas de méthode pour convertir efficacement les flottants en entiers sans risque de comportement indéfini.

Mais vraisemblablement, vous ne pouvez ajouter des vérifications d'exécution que dans les versions de débogage, comme vous le faites pour le dépassement d'entier.

AFAIK la seule raison de préférer la sémantique enveloppante est l'efficacité de l'émulation logicielle

Je ne pense pas que nous devrions préférer le wraparound ou la saturation, car les deux sont faux, mais wraparound a au moins l'avantage d'être la méthode utilisée par de nombreux langages similaires à rust: C / C ++, C #, go, probablement D, et sûrement plus, et d'être aussi le comportement actuel de la rouille (au moins parfois). Cela dit, je pense que "paniquer sur les conversions invalides (éventuellement dans les versions de débogage uniquement)" est idéal, tout comme nous le faisons pour le débordement d'entiers et l'arithmétique invalide comme la division par zéro.

(Fait intéressant, j'ai eu 16960 dans le terrain de jeu . Mais je vois dans d'autres exemples postés que parfois la rouille le fait différemment ...)

La saturation est due au fait que le langage définit comme casts pour toujours réussir, et nous ne pouvons donc pas changer l'opérateur pour paniquer à la place.

Changer ce que l'opération évalue est déjà un changement radical, dans la mesure où nous nous soucions des résultats des personnes qui le font déjà. Ce comportement sans panique pourrait également changer.

Je suppose que si nous paniquons sur NaN, cela pourrait aussi paniquer à l'infini, mais dans tous les cas, cela semble rendre le bouclage plus difficile à faire rapidement

S'il n'est vérifié que dans les versions de débogage, comme le débordement d'entier l'est, alors je pense que nous pouvons obtenir le meilleur des deux mondes: les conversions sont garanties d'être correctes (dans les versions de débogage), les erreurs utilisateur sont plus susceptibles d'être détectées, vous pouvez vous inscrire à des comportements étranges comme enveloppement et / ou saturation si vous le souhaitez, et les performances sont aussi bonnes que possible.

De plus, il semble étrange de contrôler ces éléments via un commutateur de ligne de commande. C'est un gros marteau. Le comportement souhaité d'une conversion hors plage dépend sûrement des spécificités de l'algorithme, c'est donc quelque chose qui doit être contrôlé sur une base par conversion. Je suggère f.to_u16_sat () et f.to_u16_wrap () ou similaire aux opt-ins, et ne pas avoir d'option de ligne de commande qui change la sémantique du code. Cela rendrait difficile le mélange et la correspondance de différents morceaux de code, et vous ne pouvez pas comprendre ce que fait quelque chose en le lisant ...

Et, s'il est vraiment inacceptable de faire de "paniquer si invalide" le comportement par défaut, il serait bien d'avoir une méthode intrinsèque qui l'implémente mais n'effectue le contrôle de validité que dans les versions de débogage afin que nous puissions nous assurer que nos conversions sont correctes dans le (vaste majorité des?) cas où nous prévoyons d'obtenir le même nombre après la conversion, mais sans payer de pénalité dans les versions de version.

Fait intéressant, j'ai obtenu 16960 dans la cour de récréation.

Voici comment fonctionne le comportement non défini: en fonction de la formulation exacte du programme et de la version exacte du compilateur et des indicateurs de compilation exacts, vous pouvez obtenir un comportement déterministe, ou une valeur de garbage qui change à chaque exécution, ou des erreurs de compilation. Le compilateur est autorisé à faire n'importe quoi.

wraparound a au moins l'avantage d'être la méthode utilisée par de nombreux langages similaires à rust: C / C ++, C #, go, probablement D, et sûrement plus,

Vraiment? Du moins pas en C et C ++, ils ont le même comportement indéfini que Rust. Ce n'est pas un hasard, nous utilisons LLVM qui est principalement construit pour clang implémentant C et C ++. Êtes-vous sûr de C # et c'est parti?

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

Lorsqu'une valeur finie de type flottant réel est convertie en un type entier autre que _Bool, la partie fractionnaire est rejetée (c'est-à-dire que la valeur est tronquée vers zéro). Si la valeur de la partie intégrale ne peut pas être représentée par le type entier, le comportement n'est pas défini.

L'opération restante exécutée lorsqu'une valeur de type entier est convertie en type non signé n'a pas besoin d'être effectuée lorsqu'une valeur de type flottant réel est convertie en type non signé. Ainsi, la plage des valeurs flottantes réelles portables est (-1, Utype_MAX + 1).

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

Une prvalue d'un type à virgule flottante peut être convertie en une prvalue d'un type entier. La conversion est tronquée, c'est-à-dire que la partie fractionnaire est ignorée. Le comportement n'est pas défini si la valeur tronquée ne peut pas être représentée dans le type de destination.

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

Lorsque vous convertissez une valeur double ou flottante en un type intégral, cette valeur est arrondie vers zéro à la valeur intégrale la plus proche. Si la valeur intégrale résultante est en dehors de la plage du type de destination, le résultat dépend du contexte de vérification de dépassement de capacité. Dans un contexte vérifié, une OverflowException est levée, tandis que dans un contexte non vérifié, le résultat est une valeur non spécifiée du type de destination.

Ce n'est donc pas UB, juste une "valeur non spécifiée".

@admilazz Il y a une énorme différence entre cela et le débordement d'entier: le débordement d'entier est indésirable mais bien défini . Les casts en virgule flottante sont un comportement indéfini .

Ce que vous demandez est similaire à désactiver la vérification des limites Vec en mode release, mais ce serait faux car cela permettrait un comportement indéfini.

Autoriser un comportement indéfini dans un code sécurisé n'est pas acceptable, même si cela ne se produit qu'en mode version. Ainsi, tout correctif doit s'appliquer à la fois au mode version et au mode de débogage.

Bien sûr, il est possible d'avoir un correctif plus restrictif en mode débogage, mais le correctif pour le mode de publication doit toujours être bien défini.

@admilazz Il y a une énorme différence entre cela et le débordement d'entier: le débordement d'entier est indésirable mais bien défini. Les casts en virgule flottante sont un comportement indéfini.

Bien sûr, mais ce fil concerne la définition du comportement. S'il était défini comme produisant «une valeur non spécifiée du type de destination», comme dans la spécification C # qu'Amanieu a utilement référencée ci-dessus, alors il ne serait plus indéfini (de manière dangereuse). Vous ne pouvez pas facilement utiliser la nature bien définie du débordement d'entier dans les programmes pratiques, car il paniquera toujours dans les versions de débogage. De même, la valeur produite par une conversion non valide dans les versions de version n'a pas besoin d'être prévisible ou particulièrement utile car les programmes ne pourraient pratiquement pas l'utiliser de toute façon si elle paniquait dans les versions de débogage. Cela donne en fait une portée maximale au compilateur pour les optimisations, alors que le choix d'un comportement comme la saturation contraint le compilateur et pourrait être beaucoup plus lent sur le matériel sans instructions de conversion saturantes natives. (Et ce n'est pas comme si la saturation était clairement correcte.)

Ce que vous demandez est similaire à désactiver la vérification des limites Vec en mode de libération, mais ce serait faux car cela permettrait un comportement indéfini. Autoriser un comportement indéfini dans un code sécurisé n'est pas acceptable ...

Tous les comportements non définis ne se ressemblent pas. Un comportement non défini signifie simplement que c'est à l'implémenteur du compilateur de décider de ce qui se passe. Tant qu'il n'y a aucun moyen de violer les garanties de sécurité de rust en jetant un float sur un int, je ne pense pas que ce soit similaire à permettre aux gens d'écrire dans des emplacements de mémoire arbitraires. Néanmoins, bien entendu, je conviens qu'il devrait être défini dans le sens d'une garantie de sécurité, même si ce n'est pas nécessairement prévisible.

Vraiment? Au moins pas en C et C ++, ils ont le même comportement indéfini que Rust ... Êtes-vous sûr de C # et c'est parti?

C'est suffisant. Je n'ai pas lu toutes leurs spécifications; Je viens de tester divers compilateurs. Vous avez raison de dire que "tous les compilateurs que j'ai essayés le font de cette façon" est différent de dire "les spécifications du langage le définissent comme tel". Mais je ne suis de toute façon pas en faveur du débordement, soulignant seulement qu'il semble être le plus courant. Je suis vraiment en faveur d'une conversion qui 1) protège contre les «mauvais» résultats comme 1000000.0 devenant 65535 ou 16960 pour la même raison que nous nous protégeons contre le débordement d'entiers - c'est probablement un bogue, les utilisateurs devraient donc y adhérer et 2) permet des performances maximales dans les versions de version.

Tous les comportements non définis ne se ressemblent pas. Un comportement non défini signifie simplement que c'est à l'implémenteur du compilateur de décider de ce qui se passe. Tant qu'il n'y a aucun moyen de violer les garanties de sécurité de rust en jetant un float sur un int, je ne pense pas que ce soit similaire à permettre aux gens d'écrire dans des emplacements de mémoire arbitraires. Néanmoins, bien sûr, je suis d'accord pour dire qu'il devrait être défini: défini, mais pas nécessairement prévisible.

Un comportement non défini signifie que les optimiseurs (qui sont fournis par les développeurs LLVM axés sur C et C ++) sont libres de supposer que cela ne peut jamais arriver et de transformer le code en fonction de cette hypothèse, y compris la suppression de morceaux de code qui ne sont accessibles qu'en passant par le cast non défini. ou, comme le montre cet exemple , en supposant qu'une affectation doit avoir été appelée, même si elle ne l'a pas été en fait, car invoquer du code qui est appelé sans l'appeler au préalable serait un comportement indéfini.

Même s'il était raisonnable de prouver que la composition des différentes passes d'optimisation ne produit pas de comportements émergents dangereux, les développeurs LLVM ne feront aucun effort conscient pour préserver cela.

Je dirais que tous les comportements non définis se ressemblent sur cette base.

Même s'il était raisonnable de prouver que la composition des différentes passes d'optimisation ne produit pas de comportements émergents dangereux, les développeurs LLVM ne feront aucun effort conscient pour préserver cela.

Eh bien, il est malheureux que LLVM empiète sur la conception de rust de cette manière, mais je viens de lire une partie de la référence des instructions LLVM et cela mentionne l'opération "gel" mentionnée ci-dessus ("... une autre consiste à attendre que LLVM ajoute un gel concept… ") qui empêcherait un comportement indéfini au niveau LLVM. La rouille est-elle liée à une ancienne version de LLVM? Sinon, nous pourrions l'utiliser. Cependant, leur documentation n'est pas claire sur le comportement exact.

Si l'argument est undef ou poison, 'freeze' renvoie une valeur arbitraire, mais fixe, de type 'ty'. Sinon, cette instruction est un no-op et renvoie l'argument d'entrée. Toutes les utilisations d'une valeur retournée par la même instruction 'freeze' sont garanties d'observer toujours la même valeur, tandis que différentes instructions 'freeze' peuvent donner des valeurs différentes.

Je ne sais pas ce qu'ils veulent dire par «valeur fixe» ou «la même instruction« gel »». Je pense que idéalement, il compilerait en un no-op et donnerait un entier imprévisible, mais il semble que cela pourrait éventuellement faire quelque chose de cher. Quelqu'un a-t-il essayé cette opération de gel?

Eh bien, il est dommage que LLVM empiète sur la conception de la rouille de cette manière

Ce n'est pas seulement que les développeurs LLVM écrivent les optimiseurs. C'est que, même si les développeurs rustc ont écrit les optimiseurs, flirter avec l'indéfini est intrinsèquement un énorme footgun en raison des propriétés émergentes de l'enchaînement des optimiseurs. Le cerveau humain n'a tout simplement pas évolué pour «comprendre l'ampleur potentielle de l'erreur d'arrondi» lorsque l'arrondi en question est un comportement émergent construit par des passes d'optimisation de chaînage.

Je ne vais pas être en désaccord avec vous ici. :-) J'espère que cette instruction LLVM "freeze" fournira un moyen sans frais d'éviter ce comportement indéfini.

Cela a été discuté ci-dessus et la conclusion était que si la coulée puis la congélation est un comportement défini , ce n'est pas du tout un comportement as .

IMO une telle sémantique serait une mauvaise conception de langage que nous préférerions éviter.

Ma position est qu'il vaut mieux éviter as indépendamment de ce problème, car il peut avoir une sémantique très différente (tronquer, saturer, arrondir, ...) en fonction du type d'entrée et de sortie, et celles-ci ne sont pas toujours évidentes lorsque lecture du code. (Même ce dernier peut être déduit avec foo as _ .) J'ai donc un projet de pré-RFC pour proposer diverses méthodes de conversion explicitement nommées qui couvrent les cas que as fait aujourd'hui (et peut-être plus) .

J'ai terminé ce brouillon! https://internals.rust-lang.org/t/pre-rfc-add-explicitly-named-numeric-conversion-apis/11395

Tout commentaire est le bienvenu, mais veuillez le donner dans les fils internes plutôt qu'ici.

En mode release, de tels casts renverraient des résultats arbitraires pour les entrées hors limites (dans un code entièrement sûr). Ce n'est pas une bonne sémantique pour quelque chose d'aussi innocent que.

Désolé de me répéter, mais je pense que ce même argument s'applique au débordement d'entier. Si vous multipliez certains nombres et que le résultat déborde, vous obtiendrez un résultat complètement faux qui invalidera presque certainement le calcul que vous tentiez d'effectuer, mais cela panique dans les versions de débogage et donc le bogue est susceptible d'être détecté. Je dirais qu'une conversion numérique qui donne des résultats complètement faux devrait également paniquer car il y a de très fortes chances qu'elle représente un bogue dans le code de l'utilisateur. (Le cas de l'inexactitude en virgule flottante typique est déjà traité. Si un calcul produit 65535.3, il est déjà valide de le convertir en u16. Pour obtenir une conversion hors limites, vous avez généralement besoin d'un bogue dans votre code, et si j'ai un bug Je veux être notifié pour pouvoir le corriger.)

La capacité des versions de version à donner des résultats arbitraires mais définis pour les conversions non valides permet également des performances maximales, ce qui est important, à mon avis, pour quelque chose d'aussi fondamental que les conversions numériques. Toujours saturer a un impact significatif sur les performances, masque les bugs et effectue rarement un calcul qui le rencontre de manière inattendue donne le bon résultat.

Désolé de me répéter, mais je pense que ce même argument s'applique au débordement d'entier. Si vous multipliez certains nombres et que le résultat déborde, vous obtiendrez un résultat extrêmement faux qui invalidera presque certainement le calcul que vous tentiez d'effectuer.

Nous ne parlons pas de multiplication cependant, nous parlons de moulages. Et oui, la même chose s'applique au débordement d'entier: les castes int-to-int ne paniquent jamais, même en cas de débordement. C'est parce que as , de par sa conception, ne panique jamais, même pas dans les versions de débogage. S'écarter de cela pour les lancers en virgule flottante est au mieux surprenant et dangereux au pire, car l'exactitude et la sécurité d'un code non sécurisé peuvent dépendre de certaines opérations sans paniquer.

Si vous voulez affirmer que la conception de as est imparfaite car elle fournit une conversion infaillible entre les types où une conversion correcte n'est pas toujours possible, je pense que la plupart d'entre nous seront d'accord. Mais cela est totalement hors de portée pour ce thread, qui consiste à corriger les conversions float-int dans le cadre existant de as cast . Celles-ci doivent être infaillibles, elles ne doivent pas paniquer, même pas dans les versions de débogage. Alors s'il vous plaît, proposez une sémantique raisonnable (n'impliquant pas freeze ), non-panique pour les lancers float-int, ou essayez de démarrer une nouvelle discussion sur la refonte de as pour permettre la panique lorsque le casting est avec perte (et faites-le systématiquement pour les cast int-to-int et float-to-int) - mais ce dernier est hors sujet dans ce numéro, veuillez donc ouvrir un nouveau thread (style pré-RFC) pour ça.

Que diriez-vous de commencer par implémenter simplement la sémantique freeze maintenant pour corriger l'UB, et ensuite nous pouvons avoir tout le temps du monde pour nous mettre d'accord sur la sémantique que nous voulons réellement puisque toute sémantique que nous choisirons sera rétrocompatible avec freeze sémantique.

Que diriez-vous de commencer par implémenter simplement freeze sémantique _now_ pour corriger l'UB, et ensuite nous pouvons avoir tout le temps du monde pour nous mettre d'accord sur la sémantique que nous voulons réellement puisque toute sémantique que nous choisirons sera rétrocompatible avec freeze sémantique.

  1. La panique n'est pas rétrocompatible avec le gel, nous aurions donc besoin de rejeter au moins toutes les propositions qui impliquent de paniquer. Passer de UB à paniquer est moins évidemment incompatible, bien que, comme discuté ci-dessus, il existe d'autres raisons pour ne pas faire paniquer as .
  2. Comme je l'ai écrit auparavant ,
    > nous prenons en charge plusieurs versions plus anciennes (retour à LLVM 6 pour le moment) et nous aurions besoin d'une implémentation de secours pour que celles-ci se débarrassent réellement de l'UB pour tous les utilisateurs.

Je suis d'accord avec @RalfJung pour as panique est hautement indésirable, mais cela mis à part, je ne pense pas que ce point soulevé par @admilazz soit évidemment correct:

(Le cas de l'inexactitude en virgule flottante typique est déjà traité. Si un calcul produit 65535.3, il est déjà valide de le convertir en u16. Pour obtenir une conversion hors limites, vous avez généralement besoin d'un bogue dans votre code, et si j'ai un bug Je veux être notifié pour pouvoir le corriger.)

Pour f32-> u16, il peut être vrai que vous ayez besoin d'une erreur d'arrondi extrêmement importante pour sortir de la plage u16 juste à cause d'une erreur d'arrondi, mais pour les conversions de f32 en entiers 32 bits, ce n'est pas si évidemment vrai. i32::MAX n'est pas représentable exactement en f32, le nombre représentable le plus proche est 47 de i32::MAX . Donc, si vous avez un calcul qui devrait aboutir mathématiquement à un nombre jusqu'à i32::MAX , toute erreur> = 1 ULP loin de zéro vous mettra hors limites. Et cela devient bien pire une fois que nous considérons des flotteurs de précision inférieure (IEEE 754 binary16, ou le bfloat16 non standard).

On ne parle pas de multiplication, on parle de lancers

Eh bien, les conversions de virgule flottante en nombre entier sont utilisées presque exclusivement dans le même contexte que la multiplication: les calculs numériques, et je pense qu'il existe un parallèle utile avec le comportement du débordement d'entiers.

Et oui, il en va de même pour le débordement d'entier: les cast int-to-int ne paniquent jamais, même lorsqu'ils débordent ... S'écarter de cela pour les conversions en virgule flottante est au mieux surprenant et dangereux au pire, comme l'exactitude et la sécurité du code non sécurisé peut dépendre de certaines opérations sans paniquer.

Je dirais que l'incohérence ici est justifiée par la pratique courante et ne serait pas si surprenante. Tronquer et découper des nombres entiers avec des décalages, des masques et des transtypages - en utilisant efficacement les transtypages comme une forme de bit ET plus un changement de taille - est très courant et a une longue histoire dans la programmation de systèmes. C'est quelque chose que je fais au moins plusieurs fois par semaine. Mais au cours des 30 dernières années, je ne me souviens pas avoir jamais espéré obtenir un résultat raisonnable de la conversion de NaN, Infinity ou d'une valeur à virgule flottante hors plage en entier. (Chaque instance dont je me souviens a été un bogue dans le calcul qui a produit la valeur.) Donc, je ne pense pas que le cas des entiers -> entiers casts et flottants -> entiers casts doit être traité de la même manière. Cela dit, je peux comprendre que certaines décisions sont déjà gravées dans le marbre.

s'il vous plaît… proposez une sémantique raisonnable (n'impliquant pas de gel) et sans panique pour les lancers float-int

Eh bien, ma proposition est:

  1. N'utilisez pas de commutateurs de compilation globaux qui modifient considérablement la sémantique. (Je suppose que -Zsaturating-float-casts est un paramètre de ligne de commande ou similaire.) Le code qui dépend du comportement de saturation, par exemple, serait cassé s'il était compilé sans lui. Vraisemblablement, du code avec des attentes différentes ne pourrait pas être mélangé dans le même projet. Il devrait y avoir un moyen local pour un calcul de spécifier la sémantique souhaitée, probablement quelque chose comme ce pré-RFC .
  2. Faire en sorte que les cast as aient des performances maximales par défaut, comme on pourrait s'y attendre d'un casting.

    • Je pense que cela devrait être fait via un gel sur les versions LLVM qui le prennent en charge et toute autre sémantique de conversion sur les versions LLVM qui ne le font pas (par exemple la troncature, la saturation, etc.). Je m'attends à ce que l'affirmation selon laquelle «le gel pourrait fuir des valeurs de la mémoire sensible» est purement hypothétique. (Ou, si y = freeze(fptosi(x)) laisse simplement y inchangé, faisant ainsi fuir la mémoire non initialisée, cela pourrait être corrigé en effaçant d'abord y .)

    • Si as sera relativement lent par défaut (par exemple parce qu'il sature), fournissez un moyen d'obtenir des performances maximales (par exemple une méthode - peu sûre si nécessaire - qui utilise le gel).

  1. N'utilisez pas de commutateurs de compilation globaux qui modifient considérablement la sémantique. (Je suppose que -Zsaturating-float-casts est un paramètre de ligne de commande ou similaire.)

Pour être clair, je ne pense pas que quiconque soit en désaccord. Cet indicateur n'a jamais été proposé comme un outil à court terme pour mesurer et contourner plus facilement les régressions de performances pendant que les bibliothèques sont mises à jour pour corriger ces régressions.

Pour f32-> u16, il peut être vrai que vous ayez besoin d'une erreur d'arrondi extrêmement importante pour sortir de la plage u16 juste à cause d'une erreur d'arrondi, mais pour les conversions de f32 en entiers 32 bits, ce n'est pas si évidemment vrai. i32 :: MAX n'est pas représentable exactement dans f32, le nombre représentable le plus proche est 47 de i32 :: MAX. Donc, si vous avez un calcul qui devrait aboutir mathématiquement à un nombre allant jusqu'à i32 :: MAX, toute erreur> = 1 ULP éloignée de zéro vous mettra hors limites

Cela devient un peu hors sujet, mais disons que vous avez cet algorithme hypothétique qui est censé produire mathématiquement des f32 jusqu'à 2 ^ 31-1 (mais ne devrait _pas_ produire 2 ^ 31 ou plus, sauf peut-être en raison d'une erreur d'arrondi). Cela semble déjà défectueux.

  1. Je pense que le i32 représentable le plus proche est en fait 127 en dessous de i32 :: MAX, donc même dans un monde parfait sans imprécision en virgule flottante, l'algorithme que vous prévoyez de produire des valeurs jusqu'à 2 ^ 31-1 ne peut en fait produire que (légal ) valeurs jusqu'à 2 ^ 31-128. C'est peut-être déjà un bug. Je ne suis pas sûr qu'il soit logique de parler d'erreur mesurée à partir de 2 ^ 31-1 lorsque ce nombre n'est pas possible de représenter. Vous devrez être éloigné de 64 du nombre représentable le plus proche (compte tenu de l'arrondissement) pour sortir des limites. Certes, ce n'est pas beaucoup en pourcentage lorsque vous êtes près de 2 ^ 32.
  2. Vous ne devriez pas vous attendre à une discrimination des valeurs séparées de 1 (c'est-à-dire 2 ^ 31-1 mais pas 2 ^ 31) lorsque les valeurs représentables les plus proches sont séparées de 128. De plus, seuls 3,5% des i32 sont représentables en f32 (et <2% des u32). Vous ne pouvez pas obtenir ce genre de plage tout en ayant ce genre de précision avec un f32. L'algorithme semble utiliser le mauvais outil pour le travail.

Je suppose que tout algorithme pratique qui fait ce que vous décrivez sera intimement lié aux nombres entiers. Par exemple, si vous convertissez un i32 aléatoire en f32 et inversement, il peut échouer s'il est supérieur à i32 :: MAX-64. Mais cela dégrade gravement votre précision et je ne sais pas pourquoi vous feriez une telle chose. Presque tous les calculs i32 -> f32 -> i32 qui sortent la gamme i32 complète peuvent être exprimés plus rapidement et plus précisément avec des nombres entiers, et sinon il y a f64.

Quoi qu'il en soit, même si je suis sûr qu'il est possible de trouver des cas où les algorithmes qui effectuent des conversions hors limites seraient corrigés par saturation, je pense qu'ils sont rares - assez rares pour que nous ne devrions pas ralentir _toutes_ les conversions pour les accueillir . Et je dirais que de tels algorithmes sont probablement encore défectueux et devraient être corrigés. Et si un algorithme ne peut pas être corrigé, il peut toujours faire une vérification des limites avant la conversion éventuellement hors limites (ou appeler une fonction de conversion saturante). De cette façon, les frais liés à la délimitation du résultat ne sont payés qu'en cas de besoin.

PS tardif joyeux Thanksgiving à tous.

Pour être clair, je ne pense pas que quiconque soit en désaccord. Ce drapeau n'a jamais été proposé que comme outil à court terme ...

Je faisais principalement référence à la proposition de remplacer -Zsatured-float-castts par -Zinsatured-float-casts. Même si la saturation devient la valeur par défaut, des indicateurs comme -Zunsatured-float-casts semblent mauvais pour la compatibilité, mais si elle est également destinée à être temporaire, alors d'accord, peu importe. :-)

Quoi qu'il en soit, je suis sûr que tout le monde espère que j'en ai assez dit sur cette question - moi y compris. Je sais que l'équipe de Rust a toujours cherché à fournir plusieurs façons de faire les choses afin que les gens puissent faire leurs propres choix entre la performance et la sécurité. J'ai partagé mon point de vue et je suis convaincu que vous trouverez une bonne solution à la fin. Prends soin de toi!

J'ai supposé que -Zunsaturated-float-casts n'existerait que temporairement et serait supprimé à un moment donné. Que c'est une option -Z (uniquement disponible sur Nightly) plutôt que -C suggère au moins.

Pour ce que ça vaut, la saturation et l'UB ne sont pas les seuls choix. Une autre possibilité est de changer LLVM pour ajouter une variante de fptosi qui utilise le comportement de débordement natif du processeur - c'est-à-dire que le comportement en cas de débordement ne serait pas portable entre les architectures, mais il serait bien défini sur toute architecture donnée ( par exemple, renvoyer 0x80000000 sur x86), et il ne renverrait jamais de poison ou de mémoire non initialisée. Même si la valeur par défaut devient saturante, ce serait bien de l'avoir en option. Après tout, alors que les transtypages saturants ont une surcharge inhérente aux architectures où ils ne sont pas le comportement par défaut, «faire ce que fait le CPU» n'a une surcharge que si cela inhibe une optimisation spécifique du compilateur. Je ne suis pas sûr, mais je soupçonne que toutes les optimisations activées en traitant le débordement float-int comme UB sont de niche et ne s'appliquent pas à la plupart du code.

Cela dit, un problème peut être si une architecture a plusieurs instructions float-to-int qui renvoient des valeurs différentes en cas de débordement. Dans ce cas, le compilateur choisissant l'un ou l'autre affecterait le comportement observable, ce qui n'est pas un problème en soi, mais pourrait le devenir si un seul fptosi est dupliqué et que les deux copies finissent par se comporter différemment. Mais je ne suis pas sûr que ce genre de divergence existe réellement sur les architectures populaires. Et le même problème s'applique aux autres optimisations en virgule flottante , y compris la

const fn (miri) a déjà choisi le comportement de conversion saturée depuis Rust 1.26 (en supposant que nous voulons que les résultats CTFE et RTFE soient cohérents) (avant 1.26, le cast de compilation débordant renvoie 0)

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 utilise les méthodes to_u128 / to_i128 apfloat pour effectuer la conversion. Mais je ne suis pas sûr qu'il s'agisse d'une garantie stable - étant donné en particulier qu'elle semble avoir changé auparavant (ce dont nous n'étions pas au courant lors de l'implémentation de ces éléments dans Miri).

Je pense que nous pourrions ajuster cela à tout ce que codegen finit par choisir. Mais le fait que l'apfloat de LLVM (dont la version Rust est un port direct) utilise la saturation est un bon indicateur qu'il s'agit d'une sorte de "défaut raisonnable".

Une solution au comportement observable pourrait être de choisir au hasard l'une des méthodes disponibles au moment de la construction du compilateur ou du binaire résultant.
Ensuite, ayez des fonctions comme a.saturating_cast::<i32>() pour les utilisateurs qui nécessitent un comportement spécifique.

@ dns2utf8

Le mot "aléatoirement" irait à l'encontre de l'effort pour obtenir des constructions reproductibles et, si c'est prévisible dans une version de compilateur, vous savez que quelqu'un décidera de ne pas changer.

OMI ce que @comex a décrit (pas nouveau pour ce thread IIRC, tout ce qui est ancien est nouveau) c'est la meilleure option suivante si nous ne voulons pas de saturation. Notez que nous n'avons même pas besoin de changements LLVM pour tester cela, nous pouvons utiliser inline asm (sur les architectures où de telles instructions existent).

Cela dit, un problème peut être si une architecture a plusieurs instructions float-to-int qui renvoient des valeurs différentes en cas de débordement. Dans ce cas, le compilateur choisissant l'un ou l'autre affecterait le comportement observable, ce qui n'est pas un problème en soi, mais pourrait le devenir si un seul fptosi est dupliqué et que les deux copies finissent par se comporter différemment.

OMI un tel non-déterminisme renoncerait à presque tous les avantages pratiques par rapport à freeze . Si nous faisons cela, nous devrions choisir une instruction par architecture et nous y tenir, à la fois pour le déterminisme et pour que les programmes puissent réellement s'appuyer sur le comportement de l'instruction lorsque cela a du sens pour eux. Si cela n'est pas possible sur certaines architectures, alors nous pourrions recourir à une implémentation logicielle (mais comme vous le dites, c'est tout à fait hypothétique).

C'est plus simple si nous ne déléguons pas cette décision à LLVM mais implémentons l'opération avec inline asm à la place. Ce qui serait d'ailleurs beaucoup plus facile que de changer LLVM pour ajouter de nouveaux intrinsèques et les abaisser dans chaque backend.

@rkruppe

[...] Ce qui serait d'ailleurs beaucoup plus facile que de changer LLVM pour ajouter de nouveaux intrinsèques et de les abaisser dans chaque backend.

De plus, LLVM n'est pas vraiment satisfait des intrinsèques avec une sémantique dépendante de la cible:

Cependant, si vous souhaitez que les castes soient bien définies, vous devez définir leur comportement. «Faire quelque chose de rapide» n'est pas vraiment une définition, et je ne pense pas que nous devrions donner aux constructions indépendantes de la cible un comportement dépendant de la cible.

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

Je vais retag # 10184 comme T-lang uniquement: Je pense que les problèmes à résoudre , il y a des choix sémantiques sur ce que float as int moyen

(c'est-à-dire que nous soyons disposés à le laisser avoir une sémantique panique ou non, que nous soyons disposés à lui laisser une sous-spécification basée sur freeze ou non, etc.)

ce sont des questions qui s'adressent mieux à l'équipe T-lang, pas à T-compilateur, du moins pour la discussion initiale, IMO

Je viens de rencontrer ce problème produisant des résultats qui sont _irreproductibles entre les exécutions_ même sans recompilation. L'opérateur as semble récupérer des déchets de la mémoire dans de tels cas.

Je suggère simplement d'interdire complètement l'utilisation de as pour "float as int" et de me fier à des méthodes d'arrondi spécifiques à la place. Raisonnement: as n'est pas avec perte pour les autres types.

Raisonnement: comme ce n'est pas avec perte pour les autres types.

C'est ça?

Basé sur Rust Book, je peux supposer qu'il est sans perte uniquement dans certains cas (à savoir dans les cas où le From<X> est défini pour un type Y), c'est-à-dire que vous pouvez convertir u8 en u32 utilisant From , mais pas l'inverse.

Par «pas avec perte», j'entends la distribution de valeurs suffisamment petites pour s'adapter. Exemple: 1_u64 as u8 n'est pas avec perte, donc u8 as u64 as u8 n'est pas avec perte. Pour les flottants, il n'y a pas de définition simple de «fit» puisque 20000000000000000000000000000_u128 as f32 n'est pas avec perte alors que 20000001_u32 as f32 est, donc ni float as int ni int as float sont sans perte.

256u64 as u8 est cependant avec perte.

Mais <anything>_u8 as u64 as u8 ne l'est pas.

Je pense que la perte est normale et attendue avec les moulages, et pas un problème. Tronquer des entiers avec des casts (par exemple u32 as u8 ) est une opération courante avec une signification bien comprise qui est cohérente dans tous les langages de type C que je connais (au moins sur les architectures qui utilisent des représentations entières complémentaires est fondamentalement tous ces jours). Les conversions à virgule flottante valides (c'est-à-dire où la partie intégrale s'inscrit dans la destination) ont également une sémantique bien comprise et convenue. 1.6 as u32 est avec perte, mais tous les langages de type C que je connais conviennent que le résultat devrait être 1. Ces deux cas découlent du consensus entre les fabricants de matériel sur la façon dont ces conversions devraient fonctionner et la convention en C -comme les langages qui transtypent devraient être de haute performance, des types d'opérateurs "Je sais ce que je fais".

Je ne pense donc pas que nous devrions considérer ces problèmes de la même manière que les conversions à virgule flottante invalides, car elles n'ont pas de sémantique convenue dans les langages de type C ou dans le matériel (mais elles entraînent généralement des états d'erreur ou des exceptions matérielles) et indiquent presque toujours des bogues (d'après mon expérience) et sont donc généralement inexistants dans le code correct.

Je viens de rencontrer ce problème, produisant des résultats irréproductibles entre les exécutions, même sans recompilation. L'opérateur as semble récupérer des déchets de la mémoire dans de tels cas.

Personnellement, je pense que c'est bien tant que cela ne se produit que lorsque la conversion est invalide et qu'elle n'a aucun effet secondaire en plus de produire une valeur de déchets. Si vous avez vraiment besoin d'une conversion autrement invalide dans un morceau de code, vous pouvez gérer vous-même la casse invalide avec la sémantique que vous pensez qu'elle devrait avoir.

et il n'a aucun effet secondaire en plus de produire une valeur de déchets

L'effet secondaire est que la valeur des déchets provient de quelque part dans la mémoire et révèle des données (éventuellement sensibles). Renvoyer une valeur "aléatoire" calculée uniquement à partir de float lui-même serait bien, mais le comportement actuel ne l'est pas.

Les conversions à virgule flottante valides (c'est-à-dire où la partie intégrale s'inscrit dans la destination) ont également une sémantique bien comprise et convenue.

Existe-t-il des cas d'utilisation de conversions float-int non accompagnées de trunc() , round() , floor() ou ceil() ? La stratégie d'arrondi actuelle de as est "indéfinie", ce qui rend as peine utilisable pour les nombres non arrondis. Je crois que dans la plupart des cas, celui qui écrit x as u32 veut en fait x.round() as u32 .

Je pense que la perte est normale et attendue avec les moulages, et pas un problème.

Je suis d'accord, mais seulement si la perte est facilement prévisible. Pour les entiers, les conditions de conversion avec perte sont évidentes. Pour les flotteurs, ils sont obscurs. Ils sont sans perte pour certains très gros nombres mais avec perte pour certains plus petits, même s'ils sont ronds. Ma préférence personnelle est d'avoir deux opérateurs différents pour les conversions avec perte et sans perte pour éviter d'introduire une conversion avec perte par erreur, mais je suis également d'accord avec un seul opérateur à condition que je puisse dire s'il est avec perte ou non.

L'effet secondaire est que la valeur des déchets provient de quelque part dans la mémoire et révèle des données (éventuellement sensibles).

Je m'attendrais à ce qu'il laisse la destination inchangée ou autre, mais si c'est vraiment un problème, il pourrait être remis à zéro en premier.

Existe-t-il des cas d'utilisation de conversions float-int non accompagnées de trunc (), round (), floor () ou ceil () explicites? La stratégie d'arrondi actuelle de as est "indéfinie", ce qui la rend à peine utilisable pour les nombres non arrondis.

Si la stratégie d'arrondi est vraiment indéfinie, ce serait une surprise pour moi, et je conviens que l'opérateur est à peine utile à moins que vous ne lui donniez déjà un entier. Je m'attendrais à ce qu'il tronque vers zéro.

Je crois que dans la plupart des cas, celui qui écrit x as u32 veut en fait x.round() as u32 .

Je suppose que cela dépend du domaine, mais je pense que x.trunc() as u32 est également assez généralement souhaité.

Je suis d'accord, mais seulement si la perte est facilement prévisible.

Je suis tout à fait d’accord. Que 1.6 as u32 devienne 1 ou 2 ne doit pas être indéfini, par exemple.

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

La conversion d'un float à un entier arrondira le float vers zéro
REMARQUE: actuellement, cela entraînera un comportement non défini si la valeur arrondie ne peut pas être représentée par le type entier cible. Cela inclut Inf et NaN. Ceci est un bogue et sera corrigé.

Les liens de note ici.

L'arrondi des valeurs qui «correspondent» est bien défini, ce n'est pas de cela qu'il s'agit. Ce fil est déjà long, ce serait bien de ne pas se lancer dans la spéculation de tangentes sur des faits déjà établis et documentés. Merci.

Reste à décider comment définir f as $Int dans les cas suivants:

  • f.trunc() > $Int::MAX (y compris l'infini positif)
  • f.trunc() < $Int::MIN (y compris l'infini négatif)
  • f.is_nan()

Une option déjà implémentée et disponible dans Nightly avec l'indicateur de compilateur -Z saturating-casts consiste à les définir pour renvoyer respectivement: $Int::MAX , $Int::MIN et zéro. Mais il est toujours possible de choisir un autre comportement.

Mon avis est que le comportement devrait certainement être déterministe et renvoyer une valeur entière (plutôt que de paniquer par exemple), mais la valeur exacte n'est pas trop importante et les utilisateurs qui se soucient de ces cas devraient plutôt utiliser des méthodes de conversion que je propose séparément. ajouter: https://internals.rust-lang.org/t/pre-rfc-add-explicitly-named-numeric-conversion-apis/11395

Je suppose que cela dépend du domaine, mais je pense que x.trunc() as u32 est également assez souvent souhaité.

Correct. En général, x.anything() as u32 , très probablement round() , mais pourrait aussi être trunc() , floor() , ceil() . Juste x as u32 sans spécifier la procédure d'arrondi concrète est très probablement une erreur.

Mon avis est que le comportement doit définitivement être déterministe et renvoyer une valeur entière (plutôt que paniquer par exemple), mais la valeur exacte n'est pas trop importante

Personnellement, je me débrouille bien même avec une valeur "indéfinie" à condition qu'elle ne dépende de rien d'autre que du flottement et, plus important encore, n'expose aucun registre et contenu de mémoire sans rapport.

Une option déjà implémentée et disponible dans Nightly avec l'indicateur de compilateur -Z saturating-casts consiste à les définir pour renvoyer respectivement: $Int::MAX , $Int::MIN et zéro. Mais il est toujours possible de choisir un autre comportement.

Le comportement que je m'attendrais à obtenir pour f.trunc() > $Int::MAX et f.trunc() < $Int::MIN est le même que lorsque le nombre à virgule flottante imaginaire est converti en un nombre entier de taille infinie, puis les bits les plus significatifs de celui-ci sont renvoyés ( comme dans la conversion de types entiers). Techniquement, il s'agirait de quelques bits du significatif décalés vers la gauche en fonction de l'exposant (pour les nombres positifs, les nombres négatifs ont besoin d'une inversion en fonction du complément à deux).

Donc, par exemple, je m'attendrais à ce que de très gros nombres se convertissent en 0 .

Il semble être plus difficile / plus arbitraire de définir vers quoi l'infini et NaN convertissent.

@CryZe donc si je lis cela correctement, cela correspond à -Z saturating-casts (et ce que Miri implémente déjà)?

@RalfJung C'est exact.

Génial, je vais copier https://github.com/WebAssembly/testsuite/blob/master/conversions.wast (avec les traps remplacés par les résultats spécifiés) dans la suite de tests de Miri. :)

@RalfJung Veuillez mettre à jour vers la dernière version de conversions.wast, qui vient d'être mise à jour pour inclure des tests pour les nouveaux opérateurs de conversion saturants. Les nouveaux opérateurs ont "_sat" dans leurs noms, et ils n'ont pas de recouvrement donc vous ne devriez pas avoir besoin de remplacer quoi que ce soit.

@sunfishcode merci pour la mise à jour! De toute façon, je dois traduire les tests en Rust donc je dois encore remplacer beaucoup de choses. ;)

Les tests _sat sont-ils différents en termes de valeurs testées? (EDIT: il y a un commentaire là-bas disant que les valeurs sont les mêmes.) Pour les lancers saturants de Rust, j'ai pris plusieurs de ces valeurs et les ai ajoutées à https://github.com/rust-lang/miri/pull/1321. J'étais trop paresseux pour le faire pour tous ... mais je pense que cela signifie qu'il n'y a rien à changer pour le moment avec le fichier mis à jour.

Pour l'UB intrinsèque, les pièges du côté wasm devraient alors devenir des tests d'échec de compilation dans Miri je pense.

Les valeurs d'entrée sont toutes les mêmes, la seule différence est que les opérateurs _sat ont des valeurs de sortie attendues sur les entrées où les opérateurs de recouvrement ont prévu des interruptions.

Des tests pour Miri (et donc aussi le moteur Rust CTFE) ont été ajoutés dans https://github.com/rust-lang/miri/pull/1321. J'ai vérifié localement que rustc -Zmir-opt-level=0 -Zsaturating-float-casts passe également les tests dans ce fichier.
J'ai maintenant également implémenté l'intrinsèque non vérifiée dans Miri, voir https://github.com/rust-lang/miri/pull/1325.

J'ai publié https://github.com/rust-lang/rust/pull/71269#issuecomment -615537137 qui documente l'état actuel tel que je l'ai compris et que PR se déplace également pour stabiliser le comportement du drapeau saturant -Z.

Compte tenu de la longueur de ce fil, je pense que si les gens sentent que j'ai manqué quelque chose dans ce commentaire, je dirigerais le commentaire vers le PR, ou, si c'est mineur, n'hésitez pas à me cingler sur Zulip ou Discord (simulacre) et je peux réparer les choses pour éviter les bruits inutiles sur le fil PR.

Je m'attends à ce qu'un membre de l'équipe linguistique commence bientôt une proposition de FCP sur ce PR, et sa fusion clôturera automatiquement ce problème :)

Existe-t-il des plans pour les conversions vérifiées? Quelque chose comme fn i32::checked_from(f64) -> Result<i32, DoesntFit> ?

Vous devrez considérer ce que i32::checked_from(4.5) retourner.

Cette page vous a été utile?
0 / 5 - 0 notes