Goの浮動小数点数の扱いについて非常にどーでも良いことに気が付いたので晒しておきます。何故そうなるのかまでは調べていません。2点あります。
1. マイナスゼロ(-0.0)を定数にできない
Goのソースコード中に「-0.0」を記入しても、ビルドした際にコンパイラが気を利かせて「+0.0」にしてしまうようです。
IEEE754では「-0.0」を表現できるはずなので、定数で表現できても良いと思うのですが。
いちおう、浮動小数点数のbitを直接弄れば「-0.0」を作ることが出来るようで、無理やり作った「-0.0」を用いて計算した結果には符号が引き継がれるみたいです。
サンプルコード
package main
import (
"fmt"
"math"
"strconv"
)
func main() {
fmt.Println("1 => ", strconv.FormatFloat(0.0, 'f', -1, 64))
fmt.Println("2 => ", strconv.FormatFloat(-0.0, 'f', -1, 64))
fmt.Println("3 => ", strconv.FormatFloat(0.0*-1.0, 'f', -1, 64))
// -0.0
f := math.Float64frombits(1 << 63)
fmt.Println("4 => ", strconv.FormatFloat(f, 'f', -1, 64))
fmt.Println("5 => ", strconv.FormatFloat(f*0.0, 'f', -1, 64))
fmt.Println("6 => ", strconv.FormatFloat(f*-0.0, 'f', -1, 64))
fmt.Println("7 => ", strconv.FormatFloat(f*f, 'f', -1, 64))
fmt.Println("8 => ", strconv.FormatFloat(f*-1.0, 'f', -1, 64))
fmt.Println("9 => ", strconv.FormatFloat(f*1.0, 'f', -1, 64))
}
1 => 0
2 => 0
3 => 0
4 => -0
5 => -0
6 => -0
7 => 0
8 => 0
9 => -0
定数で表現できないというか、コンパイル時に評価が終わって結果がゼロだと符号が無くなるようですね。
※実行できます→https://play.golang.org/p/cNbP3bsPEL
ちなみに、適当にwandboxでC言語とC++言語を試したところ、「-0.0」を定数で表現出来るようでした。
#include "stdio.h"
int main(void) {
printf("%f\n", -0.0);
return 0;
}
-0.000000
#include <iostream>
int main() {
std::cout << -0.0 << std::endl;
}
-0
2. strconv.AppendFloatで小数点以下桁数を指定すると劇的に遅くなる
strconv.AppendFloatやstrconv.FormatFloat1の小数点以下桁数を指定する引数にゼロ以上の値を入れてしまうと場合によっては10倍程度遅くなります。
速度が必要な場合は有効桁がそんなに要らなくても0以下を渡して、小数点をすべて出力するようにした方が良いです。
サンプルコード
func BenchmarkStrconvAppendFloat1(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
strconv.AppendFloat(nil, 3.1415926535, 'f', -1, 64)
}
}
func BenchmarkStrconvAppendFloat2(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
strconv.AppendFloat(nil, 3.1415926535, 'f', 1, 64)
}
}
func BenchmarkStrconvAppendFloat3(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
strconv.AppendFloat(nil, 2.225073858507201e-308, 'f', -1, 64)
}
}
func BenchmarkStrconvAppendFloat4(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
strconv.AppendFloat(nil, 2.225073858507201e-308, 'f', 1, 64)
}
}
func BenchmarkStrconvAppendFloat5(b *testing.B) {
buf := make([]byte, 0, 1024)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
strconv.AppendFloat(buf, 2.225073858507201e-308, 'f', -1, 64)
}
}
func BenchmarkStrconvAppendFloat6(b *testing.B) {
buf := make([]byte, 0, 1024)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
strconv.AppendFloat(buf, 2.225073858507201e-308, 'f', 1, 64)
}
}
適当にベンチマークを実行すると以下のようになります。
BenchmarkStrconvAppendFloat1-6 5000000 316 ns/op 24 B/op 2 allocs/op
BenchmarkStrconvAppendFloat2-6 3000000 401 ns/op 8 B/op 1 allocs/op
BenchmarkStrconvAppendFloat3-6 1000000 1661 ns/op 1016 B/op 7 allocs/op
BenchmarkStrconvAppendFloat4-6 100000 15213 ns/op 8 B/op 1 allocs/op
BenchmarkStrconvAppendFloat5-6 2000000 994 ns/op 0 B/op 0 allocs/op
BenchmarkStrconvAppendFloat6-6 100000 15148 ns/op 0 B/op 0 allocs/op
アロケート回数とかアロケート量とか関係なく劇的に遅いです。もはやバグと言って良いレベルなんじゃないでしょうか。
気が付いた理由
rapidjsonの高速な浮動小数点数文字列変換機能をGoに移植する際にたまたま気が付きました。移植したやつ→dtoa
-
fmt.Printf系の書式指定の影響は確認してません。 ↩