Conversion IEEE 754 floating point in Haskell Word32 / 64 en et de Haskell Float / Double
Question
dans Haskell, les bibliothèques base
et les paquets" Hackage "fournissent plusieurs moyens de convertir les données binaires IEEE-754 en points flottants vers et à partir des types Float
et Double
. Toutefois, l'exactitude, la performance et la portabilité de ces méthodes ne sont pas claires.
pour une bibliothèque GHC-ciblée destinée à (de)sérialiser un format binaire à travers les plates-formes, Quelle est la meilleure approche pour traiter IEEE-754 des données en virgule flottante?
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
. 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 tremble de voir la répartition effectuée juste pour accomplir la coercition. En outre, bien que pas unique à cette solution, Il ya une hypothèse implicite ici que L'IEEE-754 est en fait la représentation en mémoire. Les essais qui accompagnent le colis lui confèrent le label d'agrément" travaux sur ma machine", mais ce n'est pas idéal.
unsafeCoerce
avec la même supposition implicite de représentation in-memory IEEE-754, le code suivant obtient le sceau" fonctionne sur ma machine "aussi bien:
toFloat :: Word32 -> Float
toFloat = unsafeCoerce
cela a l'avantage de ne pas effectuer une répartition explicite comme l'approche ci-dessus, mais la documentation dit "il est de votre responsabilité de s'assurer que les anciens et les nouveaux types ont des représentations internes identiques". Cette hypothèse implicite est fait encore tout le travail, et est encore plus difficile quand il s'agit de types levés.
unsafeCoerce#
qui s'étend des limites de ce qui peut être considéré comme "portable":
toFloat :: Word -> Float
toFloat (W# w) = F# (unsafeCoerce# w)
cela semble fonctionner, mais ne semble pas pratique du tout, car il est limité aux types GHC.Exts
. Il est agréable de contourner les types levés, mais c'est à peu près tout ce qui peut être dit.
encodeFloat
et decodeFloat
cette approche a la propriété agréable de contourner n'importe quoi avec unsafe
dans le nom, mais ne semble pas obtenir IEEE-754 tout à fait raison. Une précédente réponse SO à une question similaire offre une approche concise, et le paquet ieee754-parser
utilisé une approche plus générale avant d'être déprécié en faveur de data-binary-ieee754
.
il y a beaucoup d'appel à avoir un code qui n'a pas besoin d'hypothèses implicites sur la représentation sous-jacente , mais ces solutions reposent sur encodeFloat
et decodeFloat
, qui sont apparemment chargé d'incohérences . Je n'ai pas encore trouvé un moyen de contourner ces problèmes.
4 réponses
Simon Marlow mentionne une autre approche dans GHC bug 2209 (également lié à la réponse de Bryan O'Sullivan)
vous pouvez obtenir l'effet désiré en utilisant castSTUArray, soit dit en passant (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 le 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 simplifié la fonction cast parce que cela provoque GHC à générer un noyau beaucoup plus serré. Après l'écriture, wordToFloat
est traduit par un appel à runSTRep et trois primops ( newByteArray#
, writeWord32Array#
, readFloatArray#
).
Je ne suis pas sûr de ce qu'est la performance par rapport à la méthode de formation FFI, mais juste pour le plaisir, j'ai comparé le noyau généré par les deux options .
faire FFI marshalling est un peu plus compliqué à cet égard. Il appelle unsafeDupablePerformIO et 7 primops ( noDuplicate#
, newAlignedPinnedByteArray#
, unsafeFreezeByteArray#
, byteArrayContents#
, writeWord32OffAddr#
, readFloatOffAddr#
, touch#
).
je viens juste de commencer à apprendre comment analyser core, peut-être quelqu'un avec plus d'expérience peut commenter sur le coût de ces opérations?
tous les CPU modernes utilisent IEEE754 pour la virgule flottante, et cela semble très peu susceptible de changer au cours de notre vie. Alors ne vous inquiétez pas pour le code qui fait cette supposition.
Vous êtes très certainement pas libre d'utiliser unsafeCoerce
ou unsafeCoerce#
pour convertir entre intégrale et de types à virgule flottante, ce qui peut provoquer à la fois des erreurs de compilation et d'exécution des plantages. Voir GHC bogue 2209 pour plus de détails.
Jusqu'à ce que le GHC bug 4092 , qui traite du besoin de coercions int ∆ fp, soit corrigé, la seule approche sûre et fiable est via le FFI.
j'utiliserais la méthode FFI pour la conversion. Mais assurez-vous d'utiliser l'alignement lors de l'attribution de la mémoire de sorte que vous obtenez la mémoire qui est acceptable pour charger/stocker à la fois le nombre de virgule flottante et le nombre entier. Vous devriez également mettre dans certains affirment sur les tailles du flotteur et le mot étant le même de sorte que vous pouvez détecter si quelque chose va mal.
si l'allocation de la mémoire vous fait criner, vous ne devriez pas utiliser Haskell. :)
je suis l'auteur de data-binary-ieee754
. Il a utilisé chacun de ces trois options.
encodeFloat
et decodeFloat
fonctionnent assez bien pour la plupart des cas, mais le code accessoire nécessaire pour les utiliser ajoute énorme overhead. Ils ne réagissent pas bien à NaN
ou Infinity
, de sorte que certaines hypothèses propres au GHC sont requises pour tout moulage fondé sur eux.
unsafeCoerce
était une tentative de remplacement, pour obtenir une meilleure performance. Il était vraiment rapide, mais les rapports d'autres bibliothèques ayant des problèmes importants m'ont finalement fait décider de l'éviter.
le code FFI a été jusqu'à présent le plus fiable, et a des performances décentes. Les frais généraux d'allocation ne sont pas aussi mauvais qu'il y paraît, probablement en raison du modèle de mémoire GHC. Et en fait ne dépend pas du format interne des flotteurs, simplement du comportement de l'instance Storable
. Le compilateur peut utiliser n'importe quelle représentation il veut aussi longtemps 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 GHC jugent bon de nous accorder des mots de largeur fixe non soulevés, avec des fonctions de conversion associées, FFI semble la meilleure option.