File 0015-vis-improve-performance-by-reducing-allocations.patch of Package go-mtree
From 47086b06548d97e3feb9db1f3802b16639e274de Mon Sep 17 00:00:00 2001
From: Aleksa Sarai <cyphar@cyphar.com>
Date: Mon, 22 Sep 2025 02:51:50 +1000
Subject: [PATCH 15/25] vis: improve performance by reducing allocations
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
By avoiding lots of small string allocations and reallocations when
appending to the output buffer, we can get a pretty decent performance
improvement (~6x for strings that do not require escaping, and ~2x for
most other multi-byte utf8 strings).
goos: linux
goarch: amd64
pkg: github.com/vbatts/go-mtree/pkg/govis
cpu: AMD Ryzen 7 7840U w/ Radeon 780M Graphics
│ before │ after │
│ sec/op │ sec/op vs base │
Vis/NoChange-16 2372.5n ± 2% 379.1n ± 1% -84.02% (p=0.000 n=10)
Vis/Binary-16 2.104µ ± 8% 1.319µ ± 8% -37.35% (p=0.000 n=10)
Vis/ASCII-16 2070.0n ± 1% 737.3n ± 0% -64.38% (p=0.000 n=10)
Vis/German-16 3.380µ ± 1% 1.181µ ± 2% -65.04% (p=0.000 n=10)
Vis/Russian-16 10.927µ ± 2% 5.293µ ± 2% -51.56% (p=0.000 n=10)
Vis/Japanese-16 7.489µ ± 1% 3.990µ ± 0% -46.72% (p=0.000 n=10)
geomean 3.767µ 1.447µ -61.58%
In theory we could get more performance if switch away from fmt.Sprintf,
but the %.N handling would be a little annoying to implement and so we
can punt on that for now.
Signed-off-by: Aleksa Sarai <cyphar@cyphar.com>
---
pkg/govis/vis.go | 66 +++++++++++++++++++++++++------------------
pkg/govis/vis_test.go | 50 ++++++++++++++++++++++++++++++++
2 files changed, 88 insertions(+), 28 deletions(-)
diff --git a/pkg/govis/vis.go b/pkg/govis/vis.go
index 620b49a1db5b..99bb0920bcbe 100644
--- a/pkg/govis/vis.go
+++ b/pkg/govis/vis.go
@@ -20,6 +20,7 @@ package govis
import (
"fmt"
+ "strings"
"unicode"
)
@@ -59,14 +60,15 @@ func isgraph(ch rune) bool {
// the plus side this is actually a benefit on the encoding side (it will
// always work with the simple unvis(3) implementation). It also means that we
// don't have to worry about different multi-byte encodings.
-func vis(b byte, flag VisFlag) string {
+func vis(output *strings.Builder, b byte, flag VisFlag) {
// Treat the single-byte character as a rune.
ch := rune(b)
// XXX: This is quite a horrible thing to support.
if flag&VisHTTPStyle == VisHTTPStyle {
if !ishttp(ch) {
- return "%" + fmt.Sprintf("%.2X", ch)
+ _, _ = fmt.Fprintf(output, "%%%.2X", ch)
+ return
}
}
@@ -86,35 +88,44 @@ func vis(b byte, flag VisFlag) string {
(flag&VisNewline != VisNewline && ch == '\n') ||
(flag&VisSafe != 0 && isunsafe(ch)) {
- encoded := string(ch)
if ch == '\\' && flag&VisNoSlash == 0 {
- encoded += "\\"
+ _ = output.WriteByte('\\')
}
- return encoded
+ _ = output.WriteByte(b)
+ return
}
// Try to use C-style escapes first.
if flag&VisCStyle == VisCStyle {
switch ch {
case ' ':
- return "\\s"
+ _, _ = output.WriteString("\\s")
+ return
case '\n':
- return "\\n"
+ _, _ = output.WriteString("\\n")
+ return
case '\r':
- return "\\r"
+ _, _ = output.WriteString("\\r")
+ return
case '\b':
- return "\\b"
+ _, _ = output.WriteString("\\b")
+ return
case '\a':
- return "\\a"
+ _, _ = output.WriteString("\\a")
+ return
case '\v':
- return "\\v"
+ _, _ = output.WriteString("\\v")
+ return
case '\t':
- return "\\t"
+ _, _ = output.WriteString("\\t")
+ return
case '\f':
- return "\\f"
+ _, _ = output.WriteString("\\f")
+ return
case '\x00':
// Output octal just to be safe.
- return "\\000"
+ _, _ = output.WriteString("\\000")
+ return
}
}
@@ -123,7 +134,8 @@ func vis(b byte, flag VisFlag) string {
// encoded as octal.
if flag&VisOctal == VisOctal || isgraph(ch) || ch&0x7f == ' ' {
// Always output three-character octal just to be safe.
- return fmt.Sprintf("\\%.3o", ch)
+ _, _ = fmt.Fprintf(output, "\\%.3o", ch)
+ return
}
// Now we have to output meta or ctrl escapes. As far as I can tell, this
@@ -131,30 +143,28 @@ func vis(b byte, flag VisFlag) string {
// copied from the original vis(3) implementation. Hopefully nobody
// actually relies on this (octal and hex are better).
- encoded := ""
if flag&VisNoSlash == 0 {
- encoded += "\\"
+ _ = output.WriteByte('\\')
}
// Meta characters have 0x80 set, but are otherwise identical to control
// characters.
if b&0x80 != 0 {
b &= 0x7f
- encoded += "M"
+ _ = output.WriteByte('M')
}
if unicode.IsControl(rune(b)) {
- encoded += "^"
+ _ = output.WriteByte('^')
if b == 0x7f {
- encoded += "?"
+ _ = output.WriteByte('?')
} else {
- encoded += fmt.Sprintf("%c", b+'@')
+ _ = output.WriteByte(b + '@')
}
} else {
- encoded += fmt.Sprintf("-%c", b)
+ _ = output.WriteByte('-')
+ _ = output.WriteByte(b)
}
-
- return encoded
}
// Vis encodes the provided string to a BSD-compatible encoding using BSD's
@@ -164,10 +174,10 @@ func Vis(src string, flags VisFlag) (string, error) {
if unknown := flags &^ visMask; unknown != 0 {
return "", unknownVisFlagsError{flags: flags}
}
-
- output := ""
+ var output strings.Builder
+ output.Grow(len(src)) // vis() will always take up at least len(src) bytes
for _, ch := range []byte(src) {
- output += vis(ch, flags)
+ vis(&output, ch, flags)
}
- return output, nil
+ return output.String(), nil
}
diff --git a/pkg/govis/vis_test.go b/pkg/govis/vis_test.go
index 96c367264943..f7b7a32488cb 100644
--- a/pkg/govis/vis_test.go
+++ b/pkg/govis/vis_test.go
@@ -19,6 +19,7 @@
package govis
import (
+ "crypto/rand"
"fmt"
"testing"
@@ -125,3 +126,52 @@ func TestVisChanged(t *testing.T) {
})
}
}
+
+func BenchmarkVis(b *testing.B) {
+ doBench := func(b *testing.B, text string) {
+ _, err := Vis(text, DefaultVisFlags)
+ require.NoErrorf(b, err, "vis(%q)", text)
+
+ for b.Loop() {
+ _, _ = Vis(text, DefaultVisFlags)
+ }
+ }
+
+ b.Run("NoChange", func(b *testing.B) {
+ text := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
+ doBench(b, text)
+ })
+
+ b.Run("Binary", func(b *testing.B) {
+ var data [32]byte
+ n, err := rand.Read(data[:])
+ require.NoError(b, err, "rand.Read")
+ require.Equal(b, len(data), n, "rand.Read len return")
+
+ text := string(data[:])
+ doBench(b, text)
+ })
+
+ // The rest of these test strings come from a set of test strings collated
+ // in <https://www.w3.org/2001/06/utf-8-test/quickbrown.html>.
+
+ b.Run("ASCII", func(b *testing.B) {
+ text := "The quick brown fox jumps over the lazy dog."
+ doBench(b, text)
+ })
+
+ b.Run("German", func(b *testing.B) {
+ text := "Falsches Üben von Xylophonmusik quält jeden größeren Zwerg"
+ doBench(b, text)
+ })
+
+ b.Run("Russian", func(b *testing.B) {
+ text := "В чащах юга жил бы цитрус? Да, но фальшивый экземпляр!"
+ doBench(b, text)
+ })
+
+ b.Run("Japanese", func(b *testing.B) {
+ text := "いろはにほへとちりぬるをイロハニホヘトチリヌルヲ"
+ doBench(b, text)
+ })
+}
--
2.51.0