Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement floating point conversion with ryu #365

Merged
merged 45 commits into from
Dec 4, 2021

Conversation

la-wu
Copy link
Contributor

@la-wu la-wu commented Feb 22, 2021

This PR implements floatDec and doubleDec using the Ryu algorithm described in Ulf Adams' paper. Compared to #222, this is a native Haskell implementation based on https://github.com/Lumaere/ryu and the performance difference is relatively minimal.

However, this does add dependencies on template-haskell (which can probably be removed but will result in the hardcoding of large tables) and array.

Relevant benchmarks for this implementation on my i7-8700k

      foldMap floatDec (10000):                               OK (0.28s)
        1.0 ms ±  42 μs
      foldMap doubleDec (10000):                              OK (0.15s)
        1.1 ms ±  91 μs

And current

      foldMap floatDec (10000):                               OK (0.15s)
        9.5 ms ± 861 μs
      foldMap doubleDec (10000):                              OK (0.12s)
         17 ms ± 1.6 ms

@Bodigrim
Copy link
Contributor

Wonderful!

We will drop older GHCs soon (see #265), do not worry about correspondent CI failures.

Could you please submit tests as a separate PR?

@la-wu
Copy link
Contributor Author

la-wu commented Feb 23, 2021

Moving off of those older GHCs is great. Any thoughts on the windows build failure

C:\Users\RUNNER~1\AppData\Local\Temp\ghcCE41.o.tmp: renameFile:renamePath:MoveFileEx "\\\\?\\C:\\Users\\RUNNER~1\\AppData\\Local\\Temp\\ghcCE41.o.tmp" Just "\\\\?\\C:\\Users\\RUNNER~1\\AppData\\Local\\Temp\\ghcCE41.o": does not exist (The system cannot find the file specified.)

Some light searching found https://gitlab.haskell.org/ghc/ghc/-/issues/18274 which suggests that there is some issue with process but I'm not very familiar with windows builds

@sjakobi
Copy link
Member

sjakobi commented Feb 23, 2021

Some light searching found https://gitlab.haskell.org/ghc/ghc/-/issues/18274 which suggests that there is some issue with process but I'm not very familiar with windows builds

IIUC we could work around this by using a different cabal executable. Maybe we could simply upgrade to using cabal-install-3.4?! Unfortunately I'm not very familiar with our use of GitHub Actions.

@Bodigrim
Copy link
Contributor

An issue with a temporary file used to happen before, but became much more prominent when Windows build switched to GHC 9.0. FIled as haskell/actions#36.

Not much can be done on our side, I believe, except just rerunning tests twice or thrice cabal test || cabal test || cabal test. I'll raise a PR.

@Bodigrim
Copy link
Contributor

Let's wait for haskell/actions#39 to land, maybe it will alleviate our issues with Windows build.

@Bodigrim
Copy link
Contributor

Bodigrim commented Feb 25, 2021

However, this does add dependencies on template-haskell (which can probably be removed but will result in the hardcoding of large tables) and array.

template-haskell is OK, #330 adds it to dependencies as well.
array could be OKish, but I'd prefer to avoid. Could you please point me where it is used? I wonder if we can use Addr# instead. Not only it eliminates a dependency, but also is usually significantly faster.

@la-wu
Copy link
Contributor Author

la-wu commented Feb 25, 2021

The precomputed tables of multipliers and the formatting code both use arrays. Since they aren't exposed anywhere, I'll take a look at converting to raw address lookups

@la-wu
Copy link
Contributor Author

la-wu commented Feb 28, 2021

@Bodigrim the array dependency has been removed but the build ran into the windows issue again.

@Bodigrim
Copy link
Contributor

Bodigrim commented Mar 1, 2021

@Lumaere I rerun, all green now :)

I'm a bit confused: f65dde3 had tests, but they disappeared after push force. Could you please submit tests as a separate PR to be merged before this one?

@la-wu
Copy link
Contributor Author

la-wu commented Mar 1, 2021

Can do. Some of the tests are more specific to internals (checking e.g round-trip and rounding). Should I stub those to the reference implementations or bring them back to this PR?

@hsyl20
Copy link
Contributor

hsyl20 commented Mar 1, 2021

template-haskell is OK, #330 adds it to dependencies as well.

Is it OK? bytestring is built with the stage1 compiler which doesn't support template-haskell.

@Bodigrim
Copy link
Contributor

Bodigrim commented Mar 1, 2021

@hsyl20 Thanks, good to know. How does template-haskell work for other boot libraries? I see that text and exceptions depend on it.

We can review this with template-haskell, and replace it with generated splices as the very last step.

@hsyl20
Copy link
Contributor

hsyl20 commented Mar 2, 2021

@Bodigrim The issue is that we can't link/execute generated code with GHC stage1, hence no TH splice.

  • exceptions depends on template-haskell package but only to define MonadThrow for the Q monad, i.e. it doesn't use the TemplateHaskell extension
  • text only uses TH quotes to define the Lift instances for {Lazy.}Text.

We can review this with template-haskell, and replace it with generated splices as the very last step.

Indeed. Or perhaps write a little utility program to generate the tables?

Also I think we could improve the generated tables by avoiding creating ByteArray at runtime. We can't create static ByteArray but we could generate primitive strings (which have type Addr#) and directly use indexWord64OffAddr#. For example double_pow5_inv_split would look like

double_pow5_inv_split :: Addr#
double_pow5_inv_split = "\0\1\123\45..."#

We have to be careful about endianness though. Perhaps always generate little-endian tables and perform a byte-swap at runtime if the host is big-endian.

@Bodigrim
Copy link
Contributor

Can do. Some of the tests are more specific to internals (checking e.g round-trip and rounding). Should I stub those to the reference implementations or bring them back to this PR?

@Lumaere sorry, missed your question. Do you mean that some tests from your branch fail under existing, show-based implementation (because of its infelicities)? In such case still submit them, wrapped into expectFail.

I'm keen to get this merged into the next release (which is September).

@Bodigrim Bodigrim mentioned this pull request May 27, 2021
@la-wu
Copy link
Contributor Author

la-wu commented Jun 17, 2021

@Bodigrim The tests were largely ported from the original C implementation of Ryu by Ulf Adams. I made the modifications to them so that they agree with show based implementation regarding rounding, etc. However, they still look at a new internal function (exposed for testing... they don't handle precision for all formats) called formatFloat / formatDouble that is based loosely on formatRealFloat. I can make the test PR expose these as

formatFloat :: FFFormat -> Maybe Int -> Float -> Builder
formatFloat fmt prec = string7 . formatRealFloat fmt prec

formatDouble :: FFFormat -> Maybe Int -> Double -> Builder
formatDouble fmt prec = string7 . formatRealFloat fmt prec

or modify all the tests to only use floatDec / doubleDec

@Bodigrim
Copy link
Contributor

From my perspective, it is better to modify tests to use only floatDec / doubleDec.

@la-wu
Copy link
Contributor Author

la-wu commented Jun 17, 2021

Works for me. The PR is #402. Let me know if you wanted it in a different format. Thanks

@Bodigrim
Copy link
Contributor

Bodigrim commented Aug 1, 2021

@la-wu could you please rebase?

Copy link
Contributor

@Bodigrim Bodigrim left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@la-wu thanks for your astonishing work! It will take some time to review, here are my first suggestions.

Data/ByteString/Builder/ASCII.hs Outdated Show resolved Hide resolved
Data/ByteString/Builder/ASCII.hs Show resolved Hide resolved
Data/ByteString/Builder/RealFloat/Internal.hs Show resolved Hide resolved
Data/ByteString/Builder/RealFloat/Internal.hs Outdated Show resolved Hide resolved
Data/ByteString/Builder/RealFloat/Internal.hs Outdated Show resolved Hide resolved
-- doubles: 1 (sign) + 17 (mantissa) + 1 (.) + 1 (e) + 4 (exponent) = 24
--
maxEncodedLength :: Int
maxEncodedLength = 32
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So why it it 32 instead of 24?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIRC, I liked rounding to the nearest power of 2 for whatever reason

boundString s = boundedPrim maxEncodedLength $ const (pokeAll s)

-- Sign -> Exp -> Mantissa
special :: Bool -> Bool -> Bool -> BoundedPrim ()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we possibly define data Foo = Foo { fooSign :: !Bool, fooExpr :: !Bool, fooMantissa :: !Bool }, so that the meaning of arguments is self-explanatory, and their ordering is less error-prone?

Copy link
Contributor Author

@la-wu la-wu Aug 8, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Data/ByteString/Builder/RealFloat/Internal.hs Outdated Show resolved Hide resolved
Data/ByteString/Builder/RealFloat/Internal.hs Show resolved Hide resolved
Data/ByteString/Builder/RealFloat/Internal.hs Outdated Show resolved Hide resolved
Copy link
Member

@sjakobi sjakobi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First off, apologies for the late review! I hadn't realized that this PR had been waiting for a review for so long.

I've made a first pass over the code. So far I'm pretty impressed by the clean and well-documented internals! 👍

I should point out that I'm not very experienced with floating-point encodings, so I probably won't be able to comment on much of the internals. For correctness, I'll have to rely on the test suite, which I'm grateful you have improved so much! :)

I have a few requests:

  • Please add module header haddocks to all the added modules.
  • Since I have encountered several bugs that involved fromIntegral, I have a somewhat irrational fear of it. (I also dislike that I often can't tell the types involved just from looking at it.) Would it be possible to replace its use with specialized intToFloat-style helpers? Alternatively, we could consider using -XTypeApplications, but I think that would require dropping support for GHC < 8 a bit earlier than planned so far.
  • I would also prefer if the types (.>>) and (.<<) were constrained a bit, if that can be done ergonomically.

Data/ByteString/Builder/RealFloat.hs Outdated Show resolved Hide resolved
Data/ByteString/Builder/RealFloat.hs Outdated Show resolved Hide resolved
Data/ByteString/Builder/RealFloat.hs Outdated Show resolved Hide resolved
Data/ByteString/Builder/RealFloat.hs Outdated Show resolved Hide resolved
Data/ByteString/Builder/RealFloat.hs Outdated Show resolved Hide resolved
Data/ByteString/Builder/RealFloat.hs Outdated Show resolved Hide resolved
Data/ByteString/Builder/RealFloat/Internal.hs Outdated Show resolved Hide resolved
Data/ByteString/Builder/RealFloat/Internal.hs Outdated Show resolved Hide resolved
@la-wu
Copy link
Contributor Author

la-wu commented Sep 17, 2021

Thanks for the review. If you're interested in reading about the floating point string conversion, I think part 2 of the ryu paper has a good intro to it.

* [ ]  Please add module header haddocks to all the added modules.

Will do. What do you suggest I put in the fields Copyright, Maintainer, Stability?

On stability (and wrt some of your other comments that I'll get to individually as I start making edits), perhaps separating the current RealFloat.hs into a stable portion for just floatDec / doubleDec and a internal/experimental portion that has the formatFloat / formatDouble (which are also new to bytestring) could be clearer. What are your thoughts?

* [ ]  Since I have encountered several bugs that involved `fromIntegral`, I have a somewhat irrational fear of it. (I also dislike that I often can't tell the types involved just from looking at it.) Would it be possible to replace its use with specialized `intToFloat`-style helpers? Alternatively, we could consider using `-XTypeApplications`, but I think that would require dropping support for GHC < 8 a bit earlier than planned so far.

* [ ]  I would also prefer if the types `(.>>)` and `(.<<)` were constrained a bit, if that can be done ergonomically.

I think these can both be at least partially achieved. Most (all?) of the fromIntegral usages are actually just signed-to-unsigned or 32-to-64-bit conversion (and vice versa). Some of it can be elided by a more careful handling of integral types and others can get explicit annotations of the whole expression (like in Builder/Prim/Binary.hs)

@sjakobi
Copy link
Member

sjakobi commented Sep 17, 2021

Will do. What do you suggest I put in the fields Copyright, Maintainer, Stability?

Oh, I was mostly looking for brief module descriptions. I'm not sure we need all that other data. I usually skip it in other packages that I maintain. (CC @Bodigrim)

Do feel free to add your copyright in the modules that you've written of course!

On stability (and wrt some of your other comments that I'll get to individually as I start making edits), perhaps separating the current RealFloat.hs into a stable portion for just floatDec / doubleDec and a internal/experimental portion that has the formatFloat / formatDouble (which are also new to bytestring) could be clearer. What are your thoughts?

I do think that formatFloat / formatDouble are very desirable additions. If there is particular reason to believe that these functions will change in the near future, it might be better to to export them from an Internal module, so we don't have to do a major version bump. D.B.B.RealFloat.Internal would be the obvious candidate, but then you'd probably want to move the other contents of that module elsewhere…

I'm not overly concerned about breaking changes though. As long as there's a general sense of "improvement", I think we can do a major version bump every 2 or 3 years.

Data/ByteString/Builder/RealFloat.hs Outdated Show resolved Hide resolved
Data/ByteString/Builder/RealFloat.hs Outdated Show resolved Hide resolved
Data/ByteString/Builder/RealFloat.hs Outdated Show resolved Hide resolved
- More intuitive name for scientific notation since we're moving away
  from GHC.Float.FFFormat anyway
- while precision handling is not fully implemented
Copy link
Member

@sjakobi sjakobi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, @la-wu. I like this direction! Needs just a bit more polishing.

-- "12.345"
{-# INLINABLE formatFloat #-}
formatFloat :: FloatFormat -> Float -> Builder
formatFloat (MkFloatFormat fmt prec) f =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we might want this to inline when applied to only a single argument, roughly like this:

Suggested change
formatFloat (MkFloatFormat fmt prec) f =
formatFloat (MkFloatFormat fmt prec) = \f ->

@Bodigrim, what do you think?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it will be better for inlining this way.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you clarify what kind of problems you're concerned about? The easiest thing might be to take a look at the Core…

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just that running f2Intermediate is unnecessary work in case of FScientific. I took a look at the dump-simpl output but it wasn't obvious to me what I should be looking for...

Data/ByteString/Builder/RealFloat.hs Outdated Show resolved Hide resolved
Data/ByteString/Builder/RealFloat.hs Outdated Show resolved Hide resolved
Data/ByteString/Builder/RealFloat.hs Outdated Show resolved Hide resolved
Data/ByteString/Builder/RealFloat.hs Outdated Show resolved Hide resolved
Data/ByteString/Builder/RealFloat.hs Outdated Show resolved Hide resolved
Data/ByteString/Builder/RealFloat.hs Outdated Show resolved Hide resolved
Data/ByteString/Builder/RealFloat.hs Outdated Show resolved Hide resolved
Data/ByteString/Builder/RealFloat.hs Outdated Show resolved Hide resolved
- `fixed` was confusing terminology adopted from FFFormat
- `FormatFloat'` -> `FormatMode`
- some doc tweaks
Copy link
Member

@sjakobi sjakobi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a few more documentation tweaks.

Could you possibly generate the haddocks and check whether the markup comes out right? Especially that identifiers are hyperlinked and non-identifiers aren't accidentally rendered in mono-space font?

Data/ByteString/Builder/RealFloat.hs Outdated Show resolved Hide resolved
Comment on lines 15 to 18
-- NB: this implementation matches `show`'s output (specifically with respect
-- to default rounding and length). In particular, there are boundary cases
-- where the closest and 'shortest' string representations are not used.
-- Mentions of 'shortest' in the docs below are with this caveat.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This paragraph is a bit hard to understand I think. An example would be very helpful. Please also clarify what "this implementation" refers to. Maybe create a named chunk and refer to it from the relevant places.

Also note that single quotes (') are used for marking identifiers in Haddock. Escaping the quotes might work?! E.g. \'shortest\'.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added an example / handwavy explanation about the rounding / boundaries and pointed to the paper for details. TBH at this point I am far enough from the writing this code that I would need to redigest the exact semantics.

WRT quotes, thanks for pointing that out. I'm used to normal MD.

standard :: Int -> FloatFormat
standard n = MkFloatFormat FStandard (Just n)

-- | Standard notation with the default precision (number of decimal places)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"default precision" seems like another term that could be explained in a named chunk that can be referenced from here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It just means matching show / formatRealFloat. LMK if I should elaborate further...

in mk0 ls `mappend` mkDot rs
| otherwise ->
let (ei, is') = roundTo 10 p' (replicate (-e) 0 ++ ds)
(b:bs) = digitsToBuilder (if ei > 0 then is' else 0:is')
Copy link
Contributor

@Bodigrim Bodigrim Nov 22, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This raises Pattern match(es) are non-exhaustive with GHC 9.2.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm is there a way to convince GHC that the list is non-empty? Looking at the implementation of roundTo, the non-throwing return values are (0, _) and (1, 1:_). Before the call to digitsToZero, we prepend 0 to the former case's snd value so AFIACT this is 'correct'.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could also just mimic the case above or add some redundant matches e.g

diff --git a/Data/ByteString/Builder/RealFloat.hs b/Data/ByteString/Builder/RealFloat.hs
index cd0d39d..0dadf45 100644
--- a/Data/ByteString/Builder/RealFloat.hs
+++ b/Data/ByteString/Builder/RealFloat.hs
@@ -258,8 +258,8 @@ showStandard m e prec =
            in mk0 ls `mappend` mkDot rs
       | otherwise ->
           let (ei, is') = roundTo 10 p' (replicate (-e) 0 ++ ds)
-              (b:bs) = digitsToBuilder (if ei > 0 then is' else 0:is')
-           in b `mappend` mkDot bs
+              (ls, rs) = splitAt 1 $ digitsToBuilder (if ei > 0 then is' else 0:is')
+           in mk0 ls `mappend` mkDot rs
           where p' = max p 0
   where
     mk0 ls = case ls of [] -> char7 '0'; _ -> mconcat ls

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not saying that your code is wrong, I'm saying that we need to get rid of a warning. Boot packages cannot have warnings. Throwing an error for an empty list would be fine, for instance.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that this warning needs to be silenced. Maybe this would be a good fit for Data.List.NonEmpty?!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added the snippet above. A lot of the code in GHC.Float makes the same assumption though so do they just turn the warning off?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. That module uses the pragma

{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-}

@Bodigrim
Copy link
Contributor

Bodigrim commented Nov 22, 2021

@sjakobi sorry, I lost track of the discussion. Is there anything left unresolved?

Copy link
Member

@sjakobi sjakobi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm happy with the API at this stage.

The haddocks could use some editing and more examples, but I don't think this needs to happen within this PR.

The GHC warning should be addressed before merging though.

-- "9.999999999999999e22"
--
-- Simplifying, we can build a shorter, lossless representation by just using
-- @"1.0e23"@ since the floating point values that are 1 ULP away are
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is "ULP"?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good to know! Could you link to the article?

Note BTW that haddock supports Markdown-style hyperlinks, e.g. [ULP](https://en.wikipedia.org/wiki/Unit_in_the_last_place).

Comment on lines +15 to +55
-- NB: The float-to-string conversions exposed by this module match `show`'s
-- output (specifically with respect to default rounding and length). In
-- particular, there are boundary cases where the closest and \'shortest\'
-- string representations are not used. Mentions of \'shortest\' in the docs
-- below are with this caveat.
--
-- For example, for fidelity, we match `show` on the output below.
--
-- >>> show (1.0e23 :: Float)
-- "1.0e23"
-- >>> show (1.0e23 :: Double)
-- "9.999999999999999e22"
-- >>> floatDec 1.0e23
-- "1.0e23"
-- >>> doubleDec 1.0e23
-- "9.999999999999999e22"
--
-- Simplifying, we can build a shorter, lossless representation by just using
-- @"1.0e23"@ since the floating point values that are 1 ULP away are
--
-- >>> showHex (castDoubleToWord64 1.0e23) []
-- "44b52d02c7e14af6"
-- >>> castWord64ToDouble 0x44b52d02c7e14af5
-- 9.999999999999997e22
-- >>> castWord64ToDouble 0x44b52d02c7e14af6
-- 9.999999999999999e22
-- >>> castWord64ToDouble 0x44b52d02c7e14af7
-- 1.0000000000000001e23
--
-- In particular, we could use the exact boundary if it is the shortest
-- representation and the original floating number is even. To experiment with
-- the shorter rounding, refer to
-- `Data.ByteString.Builder.RealFloat.Internal.acceptBounds`. This will give us
--
-- >>> floatDec 1.0e23
-- "1.0e23"
-- >>> doubleDec 1.0e23
-- "1.0e23"
--
-- For more details, please refer to the
-- <https://dl.acm.org/doi/10.1145/3192366.3192369 Ryu paper>.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this section is a bit too technical for most users. If we want to keep it, we should probably move it out of the module header and to the bottom of the page.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there such a thing as a footer? This was added per one of our discussions above about clarifying 'shortest'.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can insert chunks of documentation at selected places in the export list. See e.g. Data.ByteString.Lazy.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in mk0 ls `mappend` mkDot rs
| otherwise ->
let (ei, is') = roundTo 10 p' (replicate (-e) 0 ++ ds)
(b:bs) = digitsToBuilder (if ei > 0 then is' else 0:is')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that this warning needs to be silenced. Maybe this would be a good fit for Data.List.NonEmpty?!

- add redundant pattern matching. now symmetrical with the e >= 0 case
@Bodigrim
Copy link
Contributor

Bodigrim commented Dec 4, 2021

@sjakobi thanks for extensive reviews!

@la-wu if you get inspiration to fix documentation suggestions in a separate PR, it would be awesome. Otherwise thank you very much for all your heroic efforts to make this state-of-the-art algorithm available to Haskell users. This is a long-awaited improvement.

@Bodigrim Bodigrim merged commit 36874c3 into haskell:master Dec 4, 2021
Bodigrim pushed a commit to Bodigrim/bytestring that referenced this pull request Dec 4, 2021
* Implement floating point conversion with ryu

* Use manual strictness

- Strict extension is from ghc >= 8.0.1

* Use checked shifts

- ghc 9.0.1 doesn't shiftL on negative unsafeShiftR and this is more
  straightforward regardless

* Use builtin float-to-word conversion functions

* Use builtin conversion to Bool

* Remove dependency on array package

* Handle non-exhaustive patterns

* Try using prim conversions directly

* Revert "Try using prim conversions directly"

This reverts commit 10809d3.

* Dispatch to slow cast when builtin unavailable

- GHC.Float exports them starting from base-4.10.0.0 which starts with
  with ghc 8.2.1

* Try bumping min version to 8.4.x

* Fix log10pow5 approximation and add unit test

- expose RealFloat.Internal so that tests can import and verify that the
  approximations match in the expected ranges

* Re-export floatDec and doubleDec to maintain public API

* Improve documentation and fixes for initial code review

- make double-polymorphic functions singly-polymorphic for clarity
- use canonical Control.Arrow.first
- name boolean values in special string formatting and add explanation
- explain magic numbers in logarithm approximations and reciprocal
  multiplication
- other misc comments

* Improve table generation documentation and clean-up

- add general overview of floating point conversion algorithm and idea
  behind ryu algorithm
- remove unused Num Word128 instance

* Improve documentation of f2s and d2s and cleanup

- deduplicate shortest and rounding handling
- add some comments and explanations of algorithm steps inline

* Use stricter integral types and annotate fromIntegral usages

- a closer inspection of `fromIntegral` usages shows that a lot of that
  conversion scaffolding is unnecessary if types are chose correctly
- also fixes a delayed to-signed-int conversion that caused unsigned
  wraparound on subtraction

* Add module descriptions and fix typos

* Use internal FloatFormat instead of GHC.Float.FFFormat

- avoids dependency especially while we don't actually support the full
  API of GHC.Float.formatRealFloat

* Use monomorphic helpers for remaining integral conversions used by RealFloat

* Remove usage of TemplateHaskell in RealFloat

* Fix LUT usage on big-endian systems

- Do a runtime byteswap. bswap64 is ~1 cycle on most architectures
- NB: multiline string literals are parsed differently with language CPP

* Add header for endianness detection

* Fix big-endian word16 packing in fast digit formatting

* Fix big-endian word128 read from raw addr

* Clean up unused functions

- finv and fnorm kept as comments for reference to table computation

* Fix incorrect reciprocal function usage

- Doesn't actually affect correctness vs show since round-even is not
  implemented (acceptBounds is always False)
- Adds a couple explorative test cases and a comment anyways

* Add more test coverage and fix doc example

- Fixed-format fixed-precision tests
- Re-exports TableGenerator constants to allow sanity checks for
  computed bit-range constants

* Use quickcheck equality property in tests

* Format haddock headers more similarly to existing ones

* Use simpler reciprocal math for 32-bit words

- Clarity and marginal performance improvement

* Use boxed arithmetic in logic flow

- more portable wrt internal ghc prim and 32- vs 64-bit
- more readable (less syntax cruft in hashes and verbose operation
  names)
- not much of a performance difference measured

* Support ghc 9.2 prim word changes

- Data.Word wraps fixed-sized word types
- array operations now use fixed-sized word types

* Fix 32-bit support

- Removes most of the raw Word# usage to facilitate support of
  fixed-size sub-word types and 32-bit systems. Benchmarking shows
  little difference (<5%)
- Implements manual multiplication of 64-bit words for 32-bit systems

* Skip conversion to Double before fixed Float formatting

- otherwise produces unnecessarily long results since imprecise
  representations get much more significance with Double precision

* Tweak doc wording and add examples

- per sjakobi suggestions

* Rename FExponent to FScientific

- More intuitive name for scientific notation since we're moving away
  from GHC.Float.FFFormat anyway

* Use an opaque FloatFormat type for compatibility

- while precision handling is not fully implemented

* Rename float fixed-format to standard-format and other naming tweaks

- `fixed` was confusing terminology adopted from FFFormat
- `FormatFloat'` -> `FormatMode`
- some doc tweaks

* Encourage inlining by removing partial application

* Fix some haddock links and accidental monospacing

* Add explanation about difference between implementation and reference paper

* Clarify default precision

* Point to ryu paper for more details

* Fix non-exhaustive warning for ghc 9.2

- add redundant pattern matching. now symmetrical with the e >= 0 case
@sjakobi
Copy link
Member

sjakobi commented Dec 4, 2021

@la-wu Thanks for enduring this marathon of a review! :)

You may want to add your implementation to the list at https://github.com/ulfjack/ryu#ryu, BTW!

noughtmare pushed a commit to noughtmare/bytestring that referenced this pull request Dec 12, 2021
* Implement floating point conversion with ryu

* Use manual strictness

- Strict extension is from ghc >= 8.0.1

* Use checked shifts

- ghc 9.0.1 doesn't shiftL on negative unsafeShiftR and this is more
  straightforward regardless

* Use builtin float-to-word conversion functions

* Use builtin conversion to Bool

* Remove dependency on array package

* Handle non-exhaustive patterns

* Try using prim conversions directly

* Revert "Try using prim conversions directly"

This reverts commit 10809d3.

* Dispatch to slow cast when builtin unavailable

- GHC.Float exports them starting from base-4.10.0.0 which starts with
  with ghc 8.2.1

* Try bumping min version to 8.4.x

* Fix log10pow5 approximation and add unit test

- expose RealFloat.Internal so that tests can import and verify that the
  approximations match in the expected ranges

* Re-export floatDec and doubleDec to maintain public API

* Improve documentation and fixes for initial code review

- make double-polymorphic functions singly-polymorphic for clarity
- use canonical Control.Arrow.first
- name boolean values in special string formatting and add explanation
- explain magic numbers in logarithm approximations and reciprocal
  multiplication
- other misc comments

* Improve table generation documentation and clean-up

- add general overview of floating point conversion algorithm and idea
  behind ryu algorithm
- remove unused Num Word128 instance

* Improve documentation of f2s and d2s and cleanup

- deduplicate shortest and rounding handling
- add some comments and explanations of algorithm steps inline

* Use stricter integral types and annotate fromIntegral usages

- a closer inspection of `fromIntegral` usages shows that a lot of that
  conversion scaffolding is unnecessary if types are chose correctly
- also fixes a delayed to-signed-int conversion that caused unsigned
  wraparound on subtraction

* Add module descriptions and fix typos

* Use internal FloatFormat instead of GHC.Float.FFFormat

- avoids dependency especially while we don't actually support the full
  API of GHC.Float.formatRealFloat

* Use monomorphic helpers for remaining integral conversions used by RealFloat

* Remove usage of TemplateHaskell in RealFloat

* Fix LUT usage on big-endian systems

- Do a runtime byteswap. bswap64 is ~1 cycle on most architectures
- NB: multiline string literals are parsed differently with language CPP

* Add header for endianness detection

* Fix big-endian word16 packing in fast digit formatting

* Fix big-endian word128 read from raw addr

* Clean up unused functions

- finv and fnorm kept as comments for reference to table computation

* Fix incorrect reciprocal function usage

- Doesn't actually affect correctness vs show since round-even is not
  implemented (acceptBounds is always False)
- Adds a couple explorative test cases and a comment anyways

* Add more test coverage and fix doc example

- Fixed-format fixed-precision tests
- Re-exports TableGenerator constants to allow sanity checks for
  computed bit-range constants

* Use quickcheck equality property in tests

* Format haddock headers more similarly to existing ones

* Use simpler reciprocal math for 32-bit words

- Clarity and marginal performance improvement

* Use boxed arithmetic in logic flow

- more portable wrt internal ghc prim and 32- vs 64-bit
- more readable (less syntax cruft in hashes and verbose operation
  names)
- not much of a performance difference measured

* Support ghc 9.2 prim word changes

- Data.Word wraps fixed-sized word types
- array operations now use fixed-sized word types

* Fix 32-bit support

- Removes most of the raw Word# usage to facilitate support of
  fixed-size sub-word types and 32-bit systems. Benchmarking shows
  little difference (<5%)
- Implements manual multiplication of 64-bit words for 32-bit systems

* Skip conversion to Double before fixed Float formatting

- otherwise produces unnecessarily long results since imprecise
  representations get much more significance with Double precision

* Tweak doc wording and add examples

- per sjakobi suggestions

* Rename FExponent to FScientific

- More intuitive name for scientific notation since we're moving away
  from GHC.Float.FFFormat anyway

* Use an opaque FloatFormat type for compatibility

- while precision handling is not fully implemented

* Rename float fixed-format to standard-format and other naming tweaks

- `fixed` was confusing terminology adopted from FFFormat
- `FormatFloat'` -> `FormatMode`
- some doc tweaks

* Encourage inlining by removing partial application

* Fix some haddock links and accidental monospacing

* Add explanation about difference between implementation and reference paper

* Clarify default precision

* Point to ryu paper for more details

* Fix non-exhaustive warning for ghc 9.2

- add redundant pattern matching. now symmetrical with the e >= 0 case
@hsyl20
Copy link
Contributor

hsyl20 commented Dec 14, 2021

@la-wu Out of curiosity, did you make benchmarks against the double-conversion package? [We have had troubles using this package with GHCJS recently (it needs to be linked with libstdc++...) so a pure Haskell solution is very appealing if performance is comparable]

@la-wu
Copy link
Contributor Author

la-wu commented May 1, 2022

@la-wu Out of curiosity, did you make benchmarks against the double-conversion package? [We have had troubles using this package with GHCJS recently (it needs to be linked with libstdc++...) so a pure Haskell solution is very appealing if performance is comparable]

Hey @hsyl20, not sure if this is still relevant but I just ran a quick bench for this and I got

foldMap floatDec (10000):                               OK (0.15s)
  1.17 ms ± 114 μs
foldMap doubleDec (10000):                              OK (0.37s)
  1.42 ms ±  45 μs
foldMap floatShow (10000):                              OK (0.33s)
  10.5 ms ± 541 μs
foldMap doubleShow (10000):                             OK (0.27s)
  18.0 ms ± 1.0 ms

And with double-conversion,

foldMap floatDC (10000):    OK (0.16s)
  5.17 ms ± 450 μs
foldMap doubleDC (10000):   OK (0.21s)
  6.55 ms ± 494 μs

With a setup of

import qualified Data.Double.Conversion.Convertable    as DC

...
, benchB "foldMap floatShow"       floatData          $ foldMap (string7 . show)
, benchB "foldMap doubleShow"      doubleData         $ foldMap (string7 . show)
, benchB "foldMap floatDC"         floatData          $ foldMap DC.toShortest
, benchB "foldMap doubleDC"        doubleData         $ foldMap DC.toShortest

Not sure if I was using the library correctly... but directly converting with double-conversion to Bytestring (instead of builder) was much slower fwiw.

@hsyl20
Copy link
Contributor

hsyl20 commented May 2, 2022

@la-wu Thanks a lot for this report! We should definitely use your code instead of having to deal with double-conversion. I've added it to our task list.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Improve doubleDec/floatDec
4 participants