38 votes

Conversion de la virgule flottante IEEE 754 dans Haskell Word32/64 vers et depuis Haskell Float/Double

Question

En Haskell, le base et les paquets Hackage fournissent plusieurs moyens de convertir des données binaires en virgule flottante IEEE-754 vers et à partir du format lifté. Float et Double types. Cependant, la précision, la performance et la portabilité de ces méthodes ne sont pas claires.

Pour une bibliothèque ciblant GHC et destinée à (dé)sérialiser un format binaire sur plusieurs plateformes, quelle est la meilleure approche pour gérer les données en virgule flottante IEEE-754 ?

Approches

Ce sont les méthodes que j'ai rencontrées dans les bibliothèques existantes et les ressources en ligne.

FFI Marshaling

C'est l'approche utilisée par le data-binary-ieee754 paquet. Depuis Float , Double , Word32 et Word64 sont chacune des instances de Storable on peut poke une valeur du type source dans un tampon externe, puis peek une valeur du type cible :

toFloat :: (F.Storable word, F.Storable float) => word -> float
toFloat word = F.unsafePerformIO $ F.alloca $ \buf -> do
    F.poke (F.castPtr buf) word
    F.peek buf

Sur ma machine, cela fonctionne, mais je crains de voir l'allocation effectuée juste pour accomplir la coercition. De plus, bien que cette solution ne soit pas unique, il y a une hypothèse implicite selon laquelle IEEE-754 est en fait la représentation en mémoire. Les tests accompagnant le paquet lui donnent le sceau d'approbation "fonctionne sur ma machine", mais ce n'est pas idéal.

unsafeCoerce

Avec la même hypothèse implicite de représentation IEEE-754 en mémoire, le code suivant obtient également le label "fonctionne sur ma machine" :

toFloat :: Word32 -> Float
toFloat = unsafeCoerce

Cette méthode présente l'avantage de ne pas effectuer d'allocation explicite comme l'approche ci-dessus, mais la documentation dit "il est de votre responsabilité de vous assurer que les anciens et les nouveaux types ont des représentations internes identiques". Cette hypothèse implicite fait toujours tout le travail, et elle est encore plus contraignante lorsqu'il s'agit de types levés.

unsafeCoerce#

Repousser les limites de ce qui peut être considéré comme "portable" :

toFloat :: Word -> Float
toFloat (W# w) = F# (unsafeCoerce# w)

Cela semble fonctionner, mais n'est pas du tout pratique puisque cela se limite à l'écran de l'ordinateur. GHC.Exts types. Il est agréable de contourner les types levés, mais c'est à peu près tout ce qu'on peut dire.

encodeFloat et decodeFloat

Cette approche a l'agréable propriété de contourner tout ce qui a trait à l'utilisation de la fonction unsafe dans le nom, mais ne semble pas obtenir IEEE-754 tout à fait juste. A réponse précédente du SO à une question similaire offre une approche concise, et la ieee754-parser utilisait une approche plus générale avant d'être déprécié en faveur du paquet data-binary-ieee754 .

L'intérêt de disposer d'un code qui ne nécessite pas d'hypothèses implicites sur la représentation sous-jacente n'est pas négligeable, mais ces solutions s'appuient sur encodeFloat et decodeFloat qui sont apparemment plein d'incohérences . Je n'ai pas encore trouvé le moyen de contourner ces problèmes.

18voto

Bryan O'Sullivan Points 375

Tous les processeurs modernes utilisent IEEE754 pour la virgule flottante, et il est très peu probable que cela change au cours de notre vie. Ne vous inquiétez donc pas si un code fait cette supposition.

Vous êtes très certainement pas gratuit à utiliser unsafeCoerce ou unsafeCoerce# pour convertir les types à virgule flottante en types intégraux, car cela peut provoquer des échecs de compilation et des plantages à l'exécution. Voir Bogue 2209 de GHC pour les détails.

Jusqu'à Bogue GHC 4092 Si l'on ne parvient pas à régler le problème de l'int↔fp coercition, la seule approche sûre et fiable est celle de la FFI.

18voto

Jacob Stanley Points 2067

Simon Marlow mentionne une autre approche dans Bogue 2209 de GHC (également lié à la réponse de Bryan O'Sullivan)

Vous pouvez obtenir l'effet désiré en utilisant castSTUArray, d'ailleurs (c'est la façon dont nous le faisons dans GHC).

J'ai utilisé cette option dans certaines de mes bibliothèques afin d'éviter l'utilisation de l'option unsafePerformIO requis pour la méthode de marshalling FFI.

{-# LANGUAGE FlexibleContexts #-}

import Data.Word (Word32, Word64)
import Data.Array.ST (newArray, castSTUArray, readArray, MArray, STUArray)
import GHC.ST (runST, ST)

wordToFloat :: Word32 -> Float
wordToFloat x = runST (cast x)

floatToWord :: Float -> Word32
floatToWord x = runST (cast x)

wordToDouble :: Word64 -> Double
wordToDouble x = runST (cast x)

doubleToWord :: Double -> Word64
doubleToWord x = runST (cast x)

{-# INLINE cast #-}
cast :: (MArray (STUArray s) a (ST s),
         MArray (STUArray s) b (ST s)) => a -> ST s b
cast x = newArray (0 :: Int, 0) x >>= castSTUArray >>= flip readArray 0

J'ai intégré la fonction cast parce que cela permet à GHC de générer un noyau beaucoup plus serré. Après l'inlining, wordToFloat est traduit par un appel à runSTRep et trois primops ( newByteArray# , writeWord32Array# , readFloatArray# ).

Je ne suis pas sûr des performances par rapport à la méthode de marshalling FFI, mais juste pour le plaisir j'ai comparé le noyau généré par les deux options .

Le marshalling FFI est un peu plus compliqué à cet égard. Elle appelle unsafeDupablePerformIO et 7 primops ( noDuplicate# , newAlignedPinnedByteArray# , unsafeFreezeByteArray# , byteArrayContents# , writeWord32OffAddr# , readFloatOffAddr# , touch# ).

Je viens tout juste d'apprendre à analyser les carottes, peut-être que quelqu'un de plus expérimenté peut commenter le coût de ces opérations ?

7voto

John Millikin Points 86775

Je suis l'auteur de data-binary-ieee754 . Il a utilisé à un moment donné chacune des trois options.

encodeFloat et decodeFloat fonctionnent assez bien dans la plupart des cas, mais le code accessoire requis pour les utiliser ajoute une énorme surcharge. Ils ne réagissent pas bien à NaN ou Infinity Par conséquent, certaines hypothèses spécifiques à GHC sont nécessaires pour tous les calculs basés sur ces données.

unsafeCoerce était une tentative de remplacement, pour obtenir de meilleures performances. Elle était vraiment rapide, mais les rapports d'autres bibliothèques ayant des problèmes importants m'ont finalement décidé à l'éviter.

Le code FFI s'est avéré jusqu'à présent le plus fiable, et ses performances sont décentes. L'overhead de l'allocation n'est pas aussi mauvais qu'il n'y paraît, probablement en raison du modèle de mémoire GHC. Et en fait n'a pas ne dépendent pas du format interne des flottants, mais simplement du comportement de la fonction Storable instance. Le compilateur peut utiliser la représentation qu'il veut tant que Storable est IEEE-754. GHC utilise IEEE-754 en interne de toute façon, et je ne m'inquiète plus beaucoup des compilateurs non-GHC, donc c'est un point discutable.

Jusqu'à ce que les développeurs de GHC jugent bon de nous confier des mots de largeur fixe, avec les fonctions de conversion associées, FFI semble être la meilleure option.

6voto

augustss Points 15750

J'utiliserais la méthode FFI pour la conversion. Mais assurez-vous d'utiliser l'alignement lors de l'allocation de mémoire afin d'obtenir une mémoire acceptable pour le chargement/stockage à la fois pour le nombre à virgule flottante et l'entier. Vous devriez également mettre une certaine assertion sur les tailles du flottant et du mot étant les mêmes afin que vous puissiez détecter si quelque chose va mal.

Si l'allocation de mémoire vous fait frémir, vous ne devriez pas utiliser Haskell :)

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X