Skip to content
This repository has been archived by the owner on Sep 1, 2021. It is now read-only.

Add non-scientific serialization for float64 #1

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 54 additions & 6 deletions ryu.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,17 +78,17 @@ func AppendFloat32(b []byte, f float32) []byte {
func FormatFloat64(f float64) string {
b := make([]byte, 0, 24)
b = AppendFloat64(b, f)
return byteSliceToString(b)
}

// Convert the output to a string without copying.
var s string
sh := (*reflect.StringHeader)(unsafe.Pointer(&s))
sh.Data = uintptr(unsafe.Pointer(&b[0]))
sh.Len = len(b)
return s
func byteSliceToString(b []byte) string {
// Zero alloc conversion following pattern found in stdlib strings.Builder.
return *(*string)(unsafe.Pointer(&b))
}

// AppendFloat64 appends the string form of the 64-bit floating point number f,
// as generated by FormatFloat64, to b and returns the extended buffer.
// It behaves like strconv.AppendFloat(b, f, 'e', -1, 64).
func AppendFloat64(b []byte, f float64) []byte {
// Step 1: Decode the floating-point number.
// Unify normalized and subnormal cases.
Expand Down Expand Up @@ -126,6 +126,54 @@ func appendSpecial(b []byte, neg, expZero, mantZero bool) []byte {
return append(b, "0e+00"...)
}

// FormatFloat64 converts a 64-bit floating point number f to a string.
// It behaves like strconv.FormatFloat(f, 'f', -1, 64).
func FormatFloat64f(f float64) string {
b := make([]byte, 0, 24)
b = AppendFloat64f(b, f)
return byteSliceToString(b)
}

// AppendFloat64 appends the string form of the 64-bit floating point number f,
// as generated by FormatFloat64, to b and returns the extended buffer.
// It behaves like strconv.AppendFloat(b, f, 'f', -1, 64).
func AppendFloat64f(b []byte, f float64) []byte {
// Step 1: Decode the floating-point number.
// Unify normalized and subnormal cases.
u := math.Float64bits(f)
neg := u>>(mantBits64+expBits64) != 0
mant := u & (uint64(1)<<mantBits64 - 1)
exp := (u >> mantBits64) & (uint64(1)<<expBits64 - 1)

// Exit early for easy cases.
if exp == uint64(1)<<expBits64-1 || (exp == 0 && mant == 0) {
return appendSpecialf(b, neg, exp == 0, mant == 0)
}

d, ok := float64ToDecimalExactInt(mant, exp)
if !ok {
d = float64ToDecimal(mant, exp)
}
return d.appendF(b, neg)
}

func appendSpecialf(b []byte, neg, expZero, mantZero bool) []byte {
if !mantZero {
return append(b, "NaN"...)
}
if !expZero {
if neg {
return append(b, "-Inf"...)
} else {
return append(b, "+Inf"...)
}
}
if neg {
b = append(b, '-')
}
return append(b, '0')
}

func assert(t bool, msg string) {
if !t {
panic(msg)
Expand Down
71 changes: 71 additions & 0 deletions ryu64.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,77 @@ func (d dec64) append(b []byte, neg bool) []byte {
return b
}

func sizeSlice(b []byte, bufLen int) []byte {
if cap(b)-len(b) >= bufLen {
// Avoid function call in the common case.
return b[:len(b)+bufLen]
}

return append(b, make([]byte, bufLen)...)
}

func (d dec64) appendF(b []byte, neg bool) []byte {
// Step 5: Print the decimal representation.
if neg {
b = append(b, '-')
}

out := d.m
outLen := decimalLen64(out)

dE := int(d.e)
if dE >= 0 {
// XYZ
n := len(b)
b = sizeSlice(b, dE+outLen)
for i := n; i < dE+n; i++ {
b[outLen+i] = '0'
}

for i := n + outLen - 1; i >= n; i-- {
b[i] = '0' + byte(out%10)
out /= 10
}

return b
}

ePos := -dE
if ePos >= outLen {
// 0.XYZ
b := append(b, "0."...)
n := len(b)
b = sizeSlice(b, ePos)
for i := n + ePos - 1; i >= n; i-- {
b[i] = '0' + byte(out%10)
out /= 10
}

return b
}

// Y.XZ
b = sizeSlice(b, outLen+1) // + "."
n := len(b)
i := n - 1
end := i - outLen
for ; ePos > 0; i-- {
b[i] = '0' + byte(out%10)
out /= 10
ePos--
}

b[i] = '.'
i--

for ; i >= end; i-- {
b[i] = '0' + byte(out%10)
out /= 10
}

return b
}

func float64ToDecimalExactInt(mant, exp uint64) (d dec64, ok bool) {
e := exp - bias64
if e > mantBits64 {
Expand Down
49 changes: 49 additions & 0 deletions ryu_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ var genericTestCases = []float64{
-0.3,
1000000,
123456.7,
10.01,
123e45,
-123.45,
1e23,
Expand Down Expand Up @@ -191,8 +192,10 @@ var benchCases = []float64{
0,
1,
0.3,
0.0000000003,
1000000,
-123.45,
-123456789.123456789,
}

func BenchmarkAppendFloat32(b *testing.B) {
Expand Down Expand Up @@ -319,3 +322,49 @@ func measureCall(b []byte, f float64, format func([]byte, float64) []byte, times
elapsed := time.Since(start)
return elapsed / time.Duration(times)
}

func TestFormatFloat64f(t *testing.T) {
for _, f := range append(genericTestCases, float64TestCases...) {
got := FormatFloat64f(f)
want := strconv.FormatFloat(f, 'f', -1, 64)
if got != want {
t.Errorf("FormatFloat64f(%g): got %q; want %q", f, got, want)
}
}
}

func TestAppendFloat64f(t *testing.T) {
for _, f := range append(genericTestCases, float64TestCases...) {
// Test with under sized and over sized buffer since the implementation
// differs a bit between the two cases.
bigBufResult := string(AppendFloat64f(make([]byte, 0, 1000), f))
smallBufResult := string(AppendFloat64f(make([]byte, 0), f))
if bigBufResult != smallBufResult {
t.Errorf("AppendFloat64f(%g): big %q; small %q", f, bigBufResult, smallBufResult)
}
}
}

func BenchmarkStrconvAppendFloat64f(b *testing.B) {
for _, f := range append(benchCases, benchCases64...) {
b.Run(FormatFloat64(f), func(b *testing.B) {
var buf []byte
for i := 0; i < b.N; i++ {
buf = strconv.AppendFloat(buf[:0], f, 'f', -1, 64)
}
sinkb = buf
})
}
}

func BenchmarkAppendFloat64f(b *testing.B) {
for _, f := range append(benchCases, benchCases64...) {
b.Run(FormatFloat64(f), func(b *testing.B) {
var buf []byte
for i := 0; i < b.N; i++ {
buf = AppendFloat64f(buf[:0], f)
}
sinkb = buf
})
}
}