internal/strconv: work around escape analysis bug

For some reason, aggressive inlining with -l=4 during TestAbstractOriginSanity
does not properly analyze ftoa[F] and concludes that dst escapes to the heap.
An earlier CL disabled TestAbstractOriginSanity to fix the build.
This CL re-enables TestAbstractOriginSanity and manually
specializes ftoa[F] into ftoa32 and ftoa64, which avoids the
bad escape analysis and lets us re-enable the test.

For #79547.

Change-Id: I5a87ef5c95761781b9fea2b22d2bb161e37897d5
Reviewed-on: https://go-review.googlesource.com/c/go/+/781160
TryBot-Bypass: Russ Cox <rsc@golang.org>
Reviewed-by: David Chase <drchase@google.com>
Reviewed-by: Michael Pratt <mpratt@google.com>
Reviewed-by: Cherry Mui <cherryyz@google.com>
This commit is contained in:
Russ Cox 2026-05-21 07:10:28 -04:00
parent 04ed01963e
commit 661e0c610e
2 changed files with 142 additions and 8 deletions

View file

@ -828,9 +828,6 @@ func TestAbstractOriginSanity(t *testing.T) {
t.Skip("skipping test in short mode.")
}
// TODO(go.dev/issue/79547): -l=4 builds are temporarily broken.
t.Skip("-l=4 builds are currently broken because they introduce an allocation in runtime.printfloat64")
mustHaveDWARF(t)
abstractOriginSanity(t, "testdata/httptest", OptAllInl4)
}

View file

@ -58,10 +58,10 @@ const (
// for all formats other than 'b', it will be at least two digits.
func FormatFloat(f float64, fmt byte, prec, bitSize int) string {
if bitSize == 32 {
return string(ftoa(make([]byte, 0, max(prec+4, 24)), float32(f), fmt, prec))
return string(ftoa32(make([]byte, 0, max(prec+4, 24)), float32(f), fmt, prec))
}
if bitSize == 64 {
return string(ftoa(make([]byte, 0, max(prec+4, 24)), f, fmt, prec))
return string(ftoa64(make([]byte, 0, max(prec+4, 24)), f, fmt, prec))
}
panic("strconv: illegal FormatFloat bitSize")
}
@ -70,15 +70,152 @@ func FormatFloat(f float64, fmt byte, prec, bitSize int) string {
// as generated by [FormatFloat], to dst and returns the extended buffer.
func AppendFloat(dst []byte, f float64, fmt byte, prec, bitSize int) []byte {
if bitSize == 32 {
return ftoa(dst, float32(f), fmt, prec)
return ftoa32(dst, float32(f), fmt, prec)
}
if bitSize == 64 {
return ftoa(dst, f, fmt, prec)
return ftoa64(dst, f, fmt, prec)
}
panic("strconv: illegal AppendFloat bitSize")
}
func ftoa[F float32 | float64](dst []byte, val F, fmt byte, prec int) []byte {
// TODO(rsc): This should be ftoa[F float32 | float64](dst []byte, val F, ...),
// but due to some bad interaction between inlining, escape analysis, and generic functions,
// the result appears to escape dst to the heap with -l=4, which breaks
// TestAbstractOriginSanity. See go.dev/issue/79547.
// For now we make two manual specializations ftoa32 and ftoa64 instead.
func ftoa32(dst []byte, val float32, fmt byte, prec int) []byte {
type F = float32
var b uint64
var expBits, mantBits, bias int // parameterized constants
switch 8 * unsafe.Sizeof(val) {
case 32:
b = uint64(float32bits(float32(val)))
expBits = float32ExpBits
mantBits = float32MantBits
bias = float32Bias
case 64:
b = float64bits(float64(val))
expBits = float64ExpBits
mantBits = float64MantBits
bias = float64Bias
}
neg := b>>(expBits+mantBits) != 0
exp := int(b>>mantBits) & (1<<expBits - 1)
mant := b & (1<<mantBits - 1)
if exp == 1<<expBits-1 {
if mant != 0 {
return append(dst, "NaN"...)
}
if neg {
return append(dst, "-Inf"...)
}
return append(dst, "+Inf"...)
}
if exp == 0 {
exp++
} else {
mant |= 1 << mantBits
}
exp += bias
// Pick off easy binary, hex formats.
if fmt == 'b' {
return fmtB(dst, neg, mant, exp-mantBits)
}
if fmt == 'x' || fmt == 'X' {
return fmtX(dst, prec, fmt, neg, mant, exp, mantBits)
}
// Pick off zero.
if mant == 0 {
return fmtEFG(dst, neg, nil, 0, 0, prec, fmt, prec < 0)
}
// Negative precision means "only as much as needed to be exact."
if prec < 0 {
// Use fast unrounded scaling.
var buf [32]byte
s := 64 - bits.Len64(mant)
m := mant << s
e := exp - s
d, p := shortFloat[F](m, e-mantBits)
dp, nd := setDigits(buf[:], d, p, numDigits(d))
// Precision for shortest representation mode.
switch fmt {
case 'e', 'E':
prec = max(nd-1, 0)
case 'f':
prec = max(nd-dp, 0)
case 'g', 'G':
prec = nd
}
return fmtEFG(dst, neg, buf[:], dp, nd, prec, fmt, true)
}
if optimize {
// Fixed number of digits.
digits := prec
switch fmt {
case 'f':
// %f precision specifies digits after the decimal point.
// Estimate an upper bound on the total number of digits needed.
// ftoaFixed will shorten as needed according to prec.
if exp >= 0 {
digits = 1 + log10Pow2(1+exp) + prec
} else {
digits = 1 + prec - log10Pow2(-exp)
}
case 'e', 'E':
digits++
case 'g', 'G':
if prec == 0 {
prec = 1
}
digits = prec
default:
// Invalid mode.
digits = 1
}
if digits <= 18 {
// digits <= 0 happens for %f on very small numbers
// and means that we're guaranteed to print all zeros.
var buf [24]byte
var dp, nd int
if digits > 0 {
s := 64 - bits.Len64(mant)
m := mant << s
e := exp - s
d, p := fixedWidthFloat(m, e-mantBits, digits, prec, fmt)
if d != 0 {
dp, nd = setDigits(buf[:], d, p, numDigits(d))
}
}
return fmtEFG(dst, neg, buf[:], dp, nd, prec, fmt, false)
}
}
// Slow bignum case. Only for non-shortest results.
d := new(decimal)
d.Assign(mant)
d.Shift(exp - mantBits)
switch fmt {
case 'e', 'E':
d.Round(prec + 1)
case 'f':
d.Round(d.dp + prec)
case 'g', 'G':
if prec == 0 {
prec = 1
}
d.Round(prec)
}
return fmtEFG(dst, neg, d.d[:], d.dp, d.nd, prec, fmt, false)
}
func ftoa64(dst []byte, val float64, fmt byte, prec int) []byte {
type F = float64
var b uint64
var expBits, mantBits, bias int // parameterized constants
switch 8 * unsafe.Sizeof(val) {