-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
Add FloatPrinter based on Grisu3 #4333
Conversation
This improves the speed of transforming floats to their string representation. It is based on the 2004 paper "Printing Floating-Point Numbers Quickly and Accurately with Integers" by Florian Loitsch[1]. Most of the code is a port from the BSD-licensed C++ project "double-conversion"[2], which was extracted from the V8 engine. The Grisu3 algorithm is fast because it deals only with fixed-sized integer arithmetic. It takes advantage extra bits leftover from the 53-bit significand in a 64 bit number to help find the optimal string representation. However this only works for 95.5% of floats and it rejects the remaining 0.5%. Rejected numbers still need to be printed with some other, slower method. 1: http://www.cs.tufts.edu/~nr/cs257/archive/florian-loitsch/printf.pdf 2: https://github.com/google/double-conversion
edit: This patch now has Float32 support, disregard the rest of this comment From #4308
I ran into the same thing. I'm going to look at Float32s can be added by using https://github.com/google/double-conversion/blob/9ed0dec708a18f0bbdf1a1edaee1b6b86be2043a/double-conversion/ieee.h#L263 but I'll do that in a separate PR if it works |
Almost there, there's just one failing formatter check:
|
@will what was the problem with I haven't review the code, I am asking just because commit message says |
@bcardiff it was working locally, but was failing on travis. I wasn't actually sure if If |
["nan", "-nan"].each {|s| a = s.to_f; puts [s, pointerof(a).as(UInt64*).value.to_s(16)]} on
on
It looks like crystal on linux is not setting the negative bit. I wonder though if this is a bug in |
Crystal The only way I was able to build a -NaN in osx and crystal 0.22.0 is to parse a -NaN as you did fs = [0.0 / 0.0,
-0.0 / 0.0,
0.0 / -0.0,
-0.0 / -0.0,
"NaN".to_f64,
"-NaN".to_f64]
fs.each do |f|
{f , f.to_s, pointerof(f).as(UInt64*).value.to_s(16), f.nan? }
# 5 times => nan.0 "nan.0" "7ff8000000000000" true
# 1 time => nan.0 "nan.0" "fff8000000000000" true
end From https://linux.die.net/man/3/snprintf it seems that -NaN's are not defined in SUSv2. I now notice that the |
I updated the test to use the memory representation of -nan and nan. This way, even if it's unlikely to come across a |
|
||
fp.exp.should eq -0x3FF - 52 + 1 | ||
# This is denormal, so no hidden bit | ||
fp.frac.should eq 1 | ||
end | ||
|
||
it "converst min f32" do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo: converst
-> converts
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks
Just for fun, wanted to bench this in a different way. This is just the most straightforward way in each lang. I wouldn't be surprised if there were fancy ways to eek out performance here and there, but that really wasn't the point This is the average speed it takes to generate 1Gb of floats with newlines to stdout
# crystal complied —release
a = 1.0
loop { puts a += 1.1 } // C
/// compiled with -O on LLVM version 8.1.0 (clang-802.0.38)
#include <stdio.h>
int main() {
double a = 1.0;
for (;;) {
printf("%.17g\n", a);
a = a + 1.1;
}
} # ruby v3.4.1p111
a = 1.0
loop { puts a += 1.1 } // go 1.8.1
package main
import "fmt"
func main() {
a := 1.0
for {
fmt.Println(a);
a += 1.1;
}
} // rust 1.17.0, built with -O
fn main() {
let mut a = 1.0;
loop {
println!("{}",a);
a += 1.1;
}
} I wanted to also do node because grisu3 is used in V8, but I had problems with it: a = 1.0
for (;;) {
console.log(a);
a += 1.1
} node f.js | pv -Ss 1g > /dev/null It was at 0B/s when I killed it there, and it would switch from like 200Kb/s to 0 after a while, something is obviously wrong here, but eh. |
Minor, but would it be a better file organization to have the class be nested under Float instead of a new top-level class? (i.e. |
@will Thank you so much for this!! After this I will do a few minor changes, like probably move FloatPrinter inside Float, and introduce an |
@asterite can you explain the bitcast name? It doesn't seem to fit to me. |
@RX14 LLVM uses bitcast. It casts an object to another by just using the same bits. C++ uses |
Any other suggestion, though? |
How about |
Hmmm... I think I like that name :-) For example you can't do |
@asterite yes, that was exactly what I was trying to get at. |
This improves the speed of transforming floats to their string
representation. It is based on the 2004 paper "Printing Floating-Point
Numbers Quickly and Accurately with Integers" by Florian Loitsch[1].
Most of the code is a port from the BSD-licensed C++ project
"double-conversion"[2], which was extracted from the V8 engine.
The Grisu3 algorithm is fast because it deals only with fixed-sized
integer arithmetic. It takes advantage extra bits leftover from the
53-bit significand in a 64 bit number to help find the optimal string
representation. However this only works for 95.5% of floats and it
rejects the remaining 0.5%. Rejected numbers still need to be printed
with some other, slower method.
1: http://www.cs.tufts.edu/~nr/cs257/archive/florian-loitsch/printf.pdf
2: https://github.com/google/double-conversion
Previous issues #4308 #2220