Commits

Bryan O'Sullivan committed 783730e

Add a ByteString module, and benchmarks.

Interestingly, the ByteString code is *slower* than Text, by a
wide margin (2x or so). I assume this is due to the overhead of
using malloc for ByteString, versus pointer-bump allocation for
the MutableByteArray# used by Text.

Still a big win over plain old show, and over bytestring-show (which
is once again a surprise, being *slower* than plain old show!).

Comments (0)

Files changed (9)

Data/Double/Conversion.hs

-{-# LANGUAGE MagicHash, Rank2Types #-}
-
--- |
--- Module      : Data.Double.Conversion
--- Copyright   : (c) 2011 MailRank, Inc.
---
--- License     : BSD-style
--- Maintainer  : bos@mailrank.com
--- Stability   : experimental
--- Portability : GHC
---
--- Fast, efficient support for converting between double precision
--- floating point values and text.
-
-module Data.Double.Conversion
-    (
-      toExponential
-    , toFixed
-    , toPrecision
-    , toShortest
-    ) where
-
-import Control.Monad (when)
-import Control.Monad.ST (unsafeIOToST, runST)
-import Data.Double.Conversion.FFI
-import Data.Text.Internal (Text(Text))
-import Foreign.C.Types (CDouble, CInt)
-import GHC.Prim (MutableByteArray#)
-import qualified Data.Text.Array as A
-
--- | Compute a representation in exponential format with the requested
--- number of digits after the decimal point. The last emitted digit is
--- rounded.  If -1 digits are requested, then the shortest exponential
--- representation is computed.
-toExponential :: Int -> Double -> Text
-toExponential ndigits = convert "toExponential" len $ \val mba ->
-                        c_Text_ToExponential val mba (fromIntegral ndigits)
-  where len = c_ToExponentialLength
-        {-# NOINLINE len #-}
-
--- | Compute a decimal representation with a fixed number of digits
--- after the decimal point. The last emitted digit is rounded.
-toFixed :: Int -> Double -> Text
-toFixed ndigits = convert "toFixed" len $ \val mba ->
-                  c_Text_ToFixed val mba (fromIntegral ndigits)
-  where len = c_ToFixedLength
-        {-# NOINLINE len #-}
-
--- | Compute the shortest string of digits that correctly represent
--- the input number.
-toShortest :: Double -> Text
-toShortest = convert "toShortest" len c_Text_ToShortest
-  where len = c_ToShortestLength
-        {-# NOINLINE len #-}
-
--- | Compute @precision@ leading digits of the given value either in
--- exponential or decimal format. The last computed digit is rounded.
-toPrecision :: Int -> Double -> Text
-toPrecision ndigits = convert "toPrecision" len $ \val mba ->
-                      c_Text_ToPrecision val mba (fromIntegral ndigits)
-  where len = c_ToPrecisionLength
-        {-# NOINLINE len #-}
-
-convert :: String -> CInt
-        -> (forall s. CDouble -> MutableByteArray# s -> IO CInt)
-        -> Double -> Text
-convert func len act val = runST go
-  where
-    go = do
-      buf <- A.new (fromIntegral len)
-      size <- unsafeIOToST $ act (realToFrac val) (A.maBA buf)
-      when (size == -1) .
-        fail $ "Data.Double.Conversion." ++ func ++
-               ": conversion failed (invalid precision requested)"
-      frozen <- A.unsafeFreeze buf
-      return $ Text frozen 0 (fromIntegral size)

Data/Double/Conversion/ByteString.hs

+-- |
+-- Module      : Data.Double.Conversion.ByteString
+-- Copyright   : (c) 2011 MailRank, Inc.
+--
+-- License     : BSD-style
+-- Maintainer  : bos@mailrank.com
+-- Stability   : experimental
+-- Portability : GHC
+--
+-- Fast, efficient support for converting between double precision
+-- floating point values and text.
+
+module Data.Double.Conversion.ByteString
+    (
+      toExponential
+    , toFixed
+    , toPrecision
+    , toShortest
+    ) where
+
+import Control.Monad (when)
+import Foreign.ForeignPtr (withForeignPtr)
+import Data.Double.Conversion.FFI
+import Data.Word (Word8)
+import Data.ByteString.Internal (ByteString(..), mallocByteString)
+import Foreign.C.Types (CDouble, CInt)
+import Foreign.Ptr (Ptr)
+import System.IO.Unsafe (unsafePerformIO)
+
+-- | Compute a representation in exponential format with the requested
+-- number of digits after the decimal point. The last emitted digit is
+-- rounded.  If -1 digits are requested, then the shortest exponential
+-- representation is computed.
+toExponential :: Int -> Double -> ByteString
+toExponential ndigits = convert "toExponential" len $ \val mba ->
+                        c_ToExponential val mba (fromIntegral ndigits)
+  where len = c_ToExponentialLength
+        {-# NOINLINE len #-}
+
+-- | Compute a decimal representation with a fixed number of digits
+-- after the decimal point. The last emitted digit is rounded.
+toFixed :: Int -> Double -> ByteString
+toFixed ndigits = convert "toFixed" len $ \val mba ->
+                  c_ToFixed val mba (fromIntegral ndigits)
+  where len = c_ToFixedLength
+        {-# NOINLINE len #-}
+
+-- | Compute the shortest string of digits that correctly represent
+-- the input number.
+toShortest :: Double -> ByteString
+toShortest = convert "toShortest" len c_ToShortest
+  where len = c_ToShortestLength
+        {-# NOINLINE len #-}
+
+-- | Compute @precision@ leading digits of the given value either in
+-- exponential or decimal format. The last computed digit is rounded.
+toPrecision :: Int -> Double -> ByteString
+toPrecision ndigits = convert "toPrecision" len $ \val mba ->
+                      c_ToPrecision val mba (fromIntegral ndigits)
+  where len = c_ToPrecisionLength
+        {-# NOINLINE len #-}
+
+convert :: String -> CInt -> (CDouble -> Ptr Word8 -> IO CInt)
+        -> Double -> ByteString
+convert func len act val = unsafePerformIO $ do
+  fp <- mallocByteString (fromIntegral len)
+  size <- withForeignPtr fp $ act (realToFrac val)
+  when (size == -1) .
+    fail $ "Data.Double.Conversion.ByteString." ++ func ++
+           ": conversion failed (invalid precision requested)"
+  return $ PS fp 0 (fromIntegral size)

Data/Double/Conversion/FFI.hs

     , c_ToFixedLength
     , c_ToPrecisionLength
     , c_ToShortestLength
+    , c_ToExponential
+    , c_ToFixed
+    , c_ToPrecision
+    , c_ToShortest
     ) where
 
+import Data.Word (Word8)
 import Foreign.C.Types (CDouble, CInt)
+import Foreign.Ptr (Ptr)
 import GHC.Prim (MutableByteArray#)
 
 foreign import ccall unsafe "hs-double-conversion.h _hs_ToShortestLength"
 foreign import ccall unsafe "hs-double-conversion.h _hs_Text_ToShortest"
     c_Text_ToShortest :: CDouble -> MutableByteArray# s -> IO CInt
 
+foreign import ccall unsafe "hs-double-conversion.h _hs_ToShortest"
+    c_ToShortest :: CDouble -> Ptr Word8 -> IO CInt
+
 foreign import ccall unsafe "hs-double-conversion.h _hs_ToFixedLength"
     c_ToFixedLength :: CInt
 
 foreign import ccall unsafe "hs-double-conversion.h _hs_Text_ToFixed"
     c_Text_ToFixed :: CDouble -> MutableByteArray# s -> CInt -> IO CInt
 
+foreign import ccall unsafe "hs-double-conversion.h _hs_ToFixed"
+    c_ToFixed :: CDouble -> Ptr Word8 -> CInt -> IO CInt
+
 foreign import ccall unsafe "hs-double-conversion.h _hs_ToExponentialLength"
     c_ToExponentialLength :: CInt
 
 foreign import ccall unsafe "hs-double-conversion.h _hs_Text_ToExponential"
     c_Text_ToExponential :: CDouble -> MutableByteArray# s -> CInt -> IO CInt
 
+foreign import ccall unsafe "hs-double-conversion.h _hs_ToExponential"
+    c_ToExponential :: CDouble -> Ptr Word8 -> CInt -> IO CInt
+
 foreign import ccall unsafe "hs-double-conversion.h _hs_ToPrecisionLength"
     c_ToPrecisionLength :: CInt
 
 foreign import ccall unsafe "hs-double-conversion.h _hs_Text_ToPrecision"
     c_Text_ToPrecision :: CDouble -> MutableByteArray# s -> CInt -> IO CInt
+
+foreign import ccall unsafe "hs-double-conversion.h _hs_ToPrecision"
+    c_ToPrecision :: CDouble -> Ptr Word8 -> CInt -> IO CInt

Data/Double/Conversion/Text.hs

+{-# LANGUAGE MagicHash, Rank2Types #-}
+
+-- |
+-- Module      : Data.Double.Conversion.Text
+-- Copyright   : (c) 2011 MailRank, Inc.
+--
+-- License     : BSD-style
+-- Maintainer  : bos@mailrank.com
+-- Stability   : experimental
+-- Portability : GHC
+--
+-- Fast, efficient support for converting between double precision
+-- floating point values and text.
+
+module Data.Double.Conversion.Text
+    (
+      toExponential
+    , toFixed
+    , toPrecision
+    , toShortest
+    ) where
+
+import Control.Monad (when)
+import Control.Monad.ST (unsafeIOToST, runST)
+import Data.Double.Conversion.FFI
+import Data.Text.Internal (Text(Text))
+import Foreign.C.Types (CDouble, CInt)
+import GHC.Prim (MutableByteArray#)
+import qualified Data.Text.Array as A
+
+-- | Compute a representation in exponential format with the requested
+-- number of digits after the decimal point. The last emitted digit is
+-- rounded.  If -1 digits are requested, then the shortest exponential
+-- representation is computed.
+toExponential :: Int -> Double -> Text
+toExponential ndigits = convert "toExponential" len $ \val mba ->
+                        c_Text_ToExponential val mba (fromIntegral ndigits)
+  where len = c_ToExponentialLength
+        {-# NOINLINE len #-}
+
+-- | Compute a decimal representation with a fixed number of digits
+-- after the decimal point. The last emitted digit is rounded.
+toFixed :: Int -> Double -> Text
+toFixed ndigits = convert "toFixed" len $ \val mba ->
+                  c_Text_ToFixed val mba (fromIntegral ndigits)
+  where len = c_ToFixedLength
+        {-# NOINLINE len #-}
+
+-- | Compute the shortest string of digits that correctly represent
+-- the input number.
+toShortest :: Double -> Text
+toShortest = convert "toShortest" len c_Text_ToShortest
+  where len = c_ToShortestLength
+        {-# NOINLINE len #-}
+
+-- | Compute @precision@ leading digits of the given value either in
+-- exponential or decimal format. The last computed digit is rounded.
+toPrecision :: Int -> Double -> Text
+toPrecision ndigits = convert "toPrecision" len $ \val mba ->
+                      c_Text_ToPrecision val mba (fromIntegral ndigits)
+  where len = c_ToPrecisionLength
+        {-# NOINLINE len #-}
+
+convert :: String -> CInt
+        -> (forall s. CDouble -> MutableByteArray# s -> IO CInt)
+        -> Double -> Text
+convert func len act val = runST go
+  where
+    go = do
+      buf <- A.new (fromIntegral len)
+      size <- unsafeIOToST $ act (realToFrac val) (A.maBA buf)
+      when (size == -1) .
+        fail $ "Data.Double.Conversion.Text." ++ func ++
+               ": conversion failed (invalid precision requested)"
+      frozen <- A.unsafeFreeze buf
+      return $ Text frozen 0 (fromIntegral size)

benchmarks/Benchmarks.hs

 {-# LANGUAGE ForeignFunctionInterface, OverloadedStrings #-}
 
 import Criterion.Main
-import Data.Double.Conversion
+import qualified Data.Double.Conversion.ByteString as B
+import qualified Data.Double.Conversion.Text as T
 import Foreign.C.Types (CInt, CDouble)
 import qualified Data.Text as T
-
-showText :: Double -> T.Text
-showText d = T.pack (show d)
+import qualified Text.Show.ByteString as BS
 
 main = defaultMain [
          bgroup "haskell" [
-           bench "show" $ whnf showText pi
-         , bench "toShortest" $ whnf toShortest pi
-         , bench "toExponential" $ whnf (toExponential 3) pi
-         , bench "toPrecision" $ whnf (toExponential 8) pi
-         , bench "toFixed" $ whnf (toFixed 8) pi
+           bench "show" $ nf show (pi::Double)
+         , bench "bytestring-show" $ whnf BS.show (pi::Double)
+         , bgroup "text" [
+             bench "toShortest" $ whnf T.toShortest pi
+           , bench "toExponential" $ whnf (T.toExponential 3) pi
+           , bench "toPrecision" $ whnf (T.toExponential 8) pi
+           , bench "toFixed" $ whnf (T.toFixed 8) pi
+           ]
+         , bgroup "bytestring" [
+             bench "toShortest" $ whnf B.toShortest pi
+           , bench "toExponential" $ whnf (B.toExponential 3) pi
+           , bench "toPrecision" $ whnf (B.toExponential 8) pi
+           , bench "toFixed" $ whnf (B.toFixed 8) pi
+           ]
          ]
        , bgroup "sprintf" [
            bench "exact" $ whnf sprintf_exact pi

benchmarks/double-conversion-benchmarks.cabal

 
   build-depends:
     base,
+    bytestring-show,
     criterion >= 0.5.0.10,
     double-conversion,
     text >= 0.11.0.8

cbits/hs-double-conversion.cc

   return pos;
 }
 
+static int copy(uint16_t *buf, const char *cbuf, const int len)
+{
+  for (int i = 0; i < len; i++)
+    buf[i] = cbuf[i];
+  return len;
+}
+
 static inline const DoubleToStringConverter& defaultConverter(void)
 {
   const int flags = DoubleToStringConverter::UNIQUE_ZERO;
 }
 
 extern "C"
+int _hs_ToShortest(double value, char *buf)
+{
+  StringBuilder builder(buf, kToShortestLength);
+  return defaultConverter().ToShortest(value, &builder)
+    ? builder.position() : -1;
+}
+
+extern "C"
 int _hs_Text_ToShortest(double value, uint16_t *buf)
 {
   char cbuf[kToShortestLength];
-  StringBuilder builder(cbuf, kToShortestLength);
-  bool ok = defaultConverter().ToShortest(value, &builder);
+  return copy(buf, cbuf, _hs_ToShortest(value, cbuf));
+}
 
-  if (!ok)
-    return -1;
-
-  return copy(buf, builder, cbuf);
+extern "C"
+int _hs_ToFixed(double value, char *buf, const int ndigits)
+{
+  StringBuilder builder(buf, kToFixedLength);
+  return defaultConverter().ToFixed(value, ndigits, &builder)
+    ? builder.position() : -1;
 }
 
 extern "C"
 int _hs_Text_ToFixed(double value, uint16_t *buf, const int ndigits)
 {
   char cbuf[kToFixedLength];
-  StringBuilder builder(cbuf, kToFixedLength);
-  bool ok = defaultConverter().ToFixed(value, ndigits, &builder);
+  return copy(buf, cbuf, _hs_ToFixed(value, cbuf, ndigits));
+}
 
-  if (!ok)
-    return -1;
-  
-  return copy(buf, builder, cbuf);
+extern "C"
+int _hs_ToExponential(double value, char *buf, const int ndigits)
+{
+  StringBuilder builder(buf, kToExponentialLength);
+  return defaultConverter().ToExponential(value, ndigits, &builder)
+    ? builder.position() : -1;
 }
 
 extern "C"
 int _hs_Text_ToExponential(double value, uint16_t *buf, const int ndigits)
 {
   char cbuf[kToExponentialLength];
-  StringBuilder builder(cbuf, kToExponentialLength);
-  bool ok = defaultConverter().ToExponential(value, ndigits, &builder);
+  return copy(buf, cbuf, _hs_ToExponential(value, cbuf, ndigits));
+}
 
-  if (!ok)
-    return -1;
-  
-  return copy(buf, builder, cbuf);
+extern "C"
+int _hs_ToPrecision(double value, char *buf, const int precision)
+{
+  StringBuilder builder(buf, kToPrecisionLength);
+  return defaultConverter().ToPrecision(value, precision, &builder)
+    ? builder.position() : -1;
 }
 
 extern "C"
 int _hs_Text_ToPrecision(double value, uint16_t *buf, const int precision)
 {
   char cbuf[kToPrecisionLength];
-  StringBuilder builder(cbuf, kToPrecisionLength);
-  bool ok = defaultConverter().ToPrecision(value, precision, &builder);
-
-  if (!ok)
-    return -1;
-  
-  return copy(buf, builder, cbuf);
+  return copy(buf, cbuf, _hs_ToPrecision(value, cbuf, precision));
 }

double-conversion.cabal

 name:           double-conversion
-version:        0.1.1.0
+version:        0.2.0.0
 license:        BSD3
 license-file:   LICENSE
 homepage:       https://github.com/mailrank/double-conversion
     include
 
   exposed-modules:
-    Data.Double.Conversion
+    Data.Double.Conversion.ByteString
+    Data.Double.Conversion.Text
 
   other-modules:
     Data.Double.Conversion.FFI
 
   build-depends:
     base == 4.*,
+    bytestring,
     ghc-prim,
     text >= 0.11.0.8
 

include/hs-double-conversion.h

 
 int _hs_ToShortestLength(void);
 int _hs_Text_ToShortest(double value, uint16_t *buf);
+int _hs_ToShortest(double value, char *buf);
 int _hs_ToFixedLength(void);
 int _hs_Text_ToFixed(double value, uint16_t *buf, int ndigits);
+int _hs_ToFixed(double value, char *buf, int ndigits);
 int _hs_ToExponentialLength(void);
 int _hs_Text_ToExponential(double value, uint16_t *buf, int ndigits);
+int _hs_ToExponential(double value, char *buf, int ndigits);
 int _hs_ToPrecisionLength(void);
 int _hs_Text_ToPrecision(double value, uint16_t *buf, int ndigits);
+int _hs_ToPrecision(double value, char *buf, int ndigits);
 
 #ifdef __cplusplus
 }