Conversión de coma flotante IEEE 754 en Haskell Word32/64 ay desde Haskell Float/Double
floating-point ghc (4)
Simon Marlow menciona otro enfoque en GHC bug 2209 (también vinculado a la respuesta de Bryan O''Sullivan)
Puede lograr el efecto deseado utilizando castSTUArray, por cierto (esta es la forma en que lo hacemos en GHC).
He utilizado esta opción en algunas de mis bibliotecas para evitar la unsafePerformIO
requerida para el método de clasificación 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
Inlineé la función de conversión porque al hacerlo, GHC genera un núcleo mucho más ajustado. Después de la wordToFloat
, wordToFloat
se traduce en una llamada a runSTRep y tres primops ( newByteArray#
, writeWord32Array#
, readFloatArray#
).
No estoy seguro de cómo es el rendimiento en comparación con el método de clasificación FFI, pero solo por diversión comparé el núcleo generado por ambas opciones .
Hacer la clasificación FFI es un poco más complicado en este sentido. Llama a inseguroDupablePerformIO y 7 primop ( noDuplicate#
, newAlignedPinnedByteArray#
, unsafeFreezeByteArray#
, byteArrayContents#
, writeWord32OffAddr#
, readFloatOffAddr#
, touch#
).
Acabo de comenzar a aprender a analizar el núcleo, tal vez alguien con más experiencia puede comentar sobre el costo de estas operaciones.
Pregunta
En Haskell, las bibliotecas base
y los paquetes de Hackage proporcionan varios medios para convertir datos de punto flotante binarios IEEE-754 hacia y desde los tipos Float
y Double
elevados. Sin embargo, la precisión, el rendimiento y la portabilidad de estos métodos no están claros.
Para una biblioteca dirigida a GHC destinada a (de) serializar un formato binario en todas las plataformas, ¿cuál es el mejor enfoque para manejar datos de punto flotante IEEE-754?
Enfoques
Estos son los métodos que he encontrado en bibliotecas existentes y recursos en línea.
FFI Marshaling
Este es el enfoque utilizado por el paquete data-binary-ieee754
. Como Float
, Double
, Word32
y Word64
son instancias de Storable
, se puede poke
un valor del tipo de fuente en un búfer externo y luego peek
un valor del tipo de destino:
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
En mi máquina esto funciona, pero me estremezco al ver que se realiza la asignación solo para cumplir la coacción. Además, aunque no es exclusivo de esta solución, hay una suposición implícita aquí de que IEEE-754 es realmente la representación en memoria. Las pruebas que acompañan al paquete le otorgan el sello de aprobación "funciona en mi máquina", pero esto no es ideal.
unsafeCoerce
Con la misma suposición implícita de la representación en memoria IEEE-754, el siguiente código también obtiene el sello "funciona en mi máquina":
toFloat :: Word32 -> Float
toFloat = unsafeCoerce
Esto tiene el beneficio de no realizar una asignación explícita como el enfoque anterior, pero la documentación dice "es su responsabilidad asegurarse de que los tipos antiguo y nuevo tengan representaciones internas idénticas". Esa suposición implícita sigue haciendo todo el trabajo, y es aún más extenuante cuando se trata de tipos levantados.
unsafeCoerce#
Extender los límites de lo que podría considerarse "portátil":
toFloat :: Word -> Float
toFloat (W# w) = F# (unsafeCoerce# w)
Esto parece funcionar, pero no parece práctico ya que está limitado a los tipos GHC.Exts
. Es bueno pasar por alto los tipos levantados, pero eso es todo lo que se puede decir.
encodeFloat
y decodeFloat
Este enfoque tiene la buena propiedad de eludir todo lo que unsafe
sea unsafe
en el nombre, pero no parece que IEEE-754 tenga la razón. Una respuesta SO anterior a una pregunta similar ofrece un enfoque conciso, y el paquete ieee754-parser
utilizó un enfoque más general antes de ser desaprobado a favor de data-binary-ieee754
.
Hay bastante atractivo para tener un código que no necesita suposiciones implícitas sobre la representación subyacente, pero estas soluciones se basan en encodeFloat
y decodeFloat
, que aparentemente están llenas de inconsistencias . Todavía no he encontrado una forma de solucionar estos problemas.
Soy el autor de data-binary-ieee754
. En algún momento usó cada una de las tres opciones.
encodeFloat
y decodeFloat
funcionan lo suficientemente bien para la mayoría de los casos, pero el código de accesorio requerido para usarlos agrega una tremenda sobrecarga. No reaccionan bien ante NaN
o Infinity
, por lo que se requieren algunas suposiciones específicas de GHC para los lanzamientos basados en ellos.
unsafeCoerce
fue un intento de reemplazo, para obtener un mejor rendimiento. Fue realmente rápido, pero los informes de otras bibliotecas que tenían problemas importantes me hicieron finalmente decidir evitarlo.
El código FFI hasta ahora ha sido el más confiable y tiene un rendimiento decente. La sobrecarga de la asignación no es tan mala como parece, probablemente debido al modelo de memoria GHC. Y en realidad no depende del formato interno de flotantes, simplemente en el comportamiento de la instancia Storable
. El compilador puede usar cualquier representación que quiera, siempre que Storable
sea IEEE-754. GHC usa internamente IEEE-754 de todos modos, y ya no me preocupan más los compiladores que no son de GHC, así que es un punto discutible.
Hasta que los desarrolladores de GHC consideren oportuno otorgarnos palabras de ancho fijo no elevadas, con funciones de conversión asociadas, FFI parece ser la mejor opción.
Todas las CPU modernas usan IEEE754 para coma flotante, y esto parece muy poco probable que cambie en el transcurso de nuestra vida. Así que no te preocupes por el código que hace esa suposición.
Definitivamente no es libre de usar unsafeCoerce
o unsafeCoerce#
para convertir entre tipos integrales y de punto flotante, ya que esto puede causar fallas de compilación y fallas en el tiempo de ejecución. Ver error GHC 2209 para más detalles.
Hasta que se solucione la falla 4092 de GHC , que aborda la necesidad de coerciones int↔fp, el único enfoque seguro y confiable es a través del FFI.
Usaría el método FFI para la conversión. Pero asegúrese de usar la alineación cuando asigne memoria para obtener memoria que sea aceptable para cargar / almacenar tanto para el número de punto flotante como para el entero. También debe poner en claro que los tamaños del flotador y la palabra son los mismos para que pueda detectar si algo sale mal.
Si la asignación de memoria te hace temblar, no deberías usar Haskell. :)