strings, bytes: speed up TrimSpace for Unicode input
Avoid the generic TrimFunc fallback in TrimSpace after encountering
non-ASCII input. Instead, use dedicated Unicode whitespace trimming
helpers backed by a small internal White_Space predicate.
This keeps the existing ASCII fast path unchanged while avoiding
function predicate calls and general Unicode range table lookups on
the Unicode trimming path.
On linux/amd64:
name old time/op new time/op delta
strings/TrimSpace/NoTrim 1.54ns 1.51ns -1.8%
strings/TrimSpace/ASCII 2.26ns 2.28ns +0.6%
strings/TrimSpace/SomeNonASCII 48.7ns 22.5ns -53.8%
strings/TrimSpace/JustNonASCII 61.2ns 31.0ns -49.3%
bytes/TrimSpace/NoTrim 1.53ns 1.51ns -1.5%
bytes/TrimSpace/ASCII 2.46ns 2.47ns +0.5%
bytes/TrimSpace/SomeNonASCII 46.8ns 24.4ns -47.8%
bytes/TrimSpace/JustNonASCII 63.3ns 33.2ns -47.5%
All cases remain at 0 B/op and 0 allocs/op.
Add a test to keep the internal predicate in sync with unicode.IsSpace.
diff --git a/src/bytes/bytes.go b/src/bytes/bytes.go
index b0daa5d..669d5b0 100644
--- a/src/bytes/bytes.go
+++ b/src/bytes/bytes.go
@@ -8,6 +8,7 @@
import (
"internal/bytealg"
+ "internal/stringslite"
"math/bits"
"unicode"
"unicode/utf8"
@@ -1106,6 +1107,38 @@
return s
}
+func trimSpaceUnicode(s []byte) []byte {
+ for len(s) > 0 {
+ r, n := rune(s[0]), 1
+ if r >= utf8.RuneSelf {
+ r, n = utf8.DecodeRune(s)
+ }
+ if !stringslite.IsSpace(r) {
+ break
+ }
+ s = s[n:]
+ }
+ if len(s) == 0 {
+ // This is what we've historically done.
+ return nil
+ }
+ return trimRightSpaceUnicode(s)
+}
+
+func trimRightSpaceUnicode(s []byte) []byte {
+ for len(s) > 0 {
+ r, n := rune(s[len(s)-1]), 1
+ if r >= utf8.RuneSelf {
+ r, n = utf8.DecodeLastRune(s)
+ }
+ if !stringslite.IsSpace(r) {
+ break
+ }
+ s = s[:len(s)-n]
+ }
+ return s
+}
+
// TrimSpace returns a subslice of s by slicing off all leading and
// trailing white space, as defined by Unicode.
func TrimSpace(s []byte) []byte {
@@ -1114,7 +1147,7 @@
if c >= utf8.RuneSelf {
// If we run into a non-ASCII byte, fall back to the
// slower unicode-aware method on the remaining bytes.
- return TrimFunc(s[lo:], unicode.IsSpace)
+ return trimSpaceUnicode(s[lo:])
}
if asciiSpace[c] != 0 {
continue
@@ -1124,7 +1157,7 @@
for hi := len(s) - 1; hi >= 0; hi-- {
c := s[hi]
if c >= utf8.RuneSelf {
- return TrimFunc(s[:hi+1], unicode.IsSpace)
+ return trimRightSpaceUnicode(s[:hi+1])
}
if asciiSpace[c] == 0 {
// At this point, s[:hi+1] starts and ends with ASCII
diff --git a/src/internal/stringslite/strings.go b/src/internal/stringslite/strings.go
index 6c825df..830a05b 100644
--- a/src/internal/stringslite/strings.go
+++ b/src/internal/stringslite/strings.go
@@ -5,7 +5,7 @@
// Package stringslite implements a subset of strings,
// only using packages that may be imported by "os".
//
-// Tests for these functions are in the strings package.
+// Most tests for these functions are in the strings package.
package stringslite
import (
@@ -112,6 +112,28 @@
return s
}
+// IsSpace reports whether r is a space character as defined by
+// Unicode's White Space property.
+func IsSpace(r rune) bool {
+ if r <= ' ' {
+ return r == ' ' || '\t' <= r && r <= '\r'
+ }
+ if r < 0x85 {
+ return false
+ }
+ if r == 0x85 || r == 0xA0 || r == 0x1680 {
+ return true
+ }
+ if 0x2000 <= r && r <= 0x200A {
+ return true
+ }
+ switch r {
+ case 0x2028, 0x2029, 0x202F, 0x205F, 0x3000:
+ return true
+ }
+ return false
+}
+
func Clone(s string) string {
if len(s) == 0 {
return ""
diff --git a/src/internal/stringslite/strings_test.go b/src/internal/stringslite/strings_test.go
new file mode 100644
index 0000000..2f3df52
--- /dev/null
+++ b/src/internal/stringslite/strings_test.go
@@ -0,0 +1,20 @@
+// Copyright 2026 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package stringslite_test
+
+import (
+ "internal/stringslite"
+ "testing"
+ "unicode"
+ "unicode/utf8"
+)
+
+func TestIsSpace(t *testing.T) {
+ for r := rune(0); r <= utf8.MaxRune; r++ {
+ if stringslite.IsSpace(r) != unicode.IsSpace(r) {
+ t.Fatalf("IsSpace(%U) = %v, want %v", r, stringslite.IsSpace(r), unicode.IsSpace(r))
+ }
+ }
+}
diff --git a/src/strings/strings.go b/src/strings/strings.go
index 3e5de2f..d366d52 100644
--- a/src/strings/strings.go
+++ b/src/strings/strings.go
@@ -1083,6 +1083,34 @@
return s
}
+func trimSpaceUnicode(s string) string {
+ for len(s) > 0 {
+ r, n := rune(s[0]), 1
+ if r >= utf8.RuneSelf {
+ r, n = utf8.DecodeRuneInString(s)
+ }
+ if !stringslite.IsSpace(r) {
+ break
+ }
+ s = s[n:]
+ }
+ return trimRightSpaceUnicode(s)
+}
+
+func trimRightSpaceUnicode(s string) string {
+ for len(s) > 0 {
+ r, n := rune(s[len(s)-1]), 1
+ if r >= utf8.RuneSelf {
+ r, n = utf8.DecodeLastRuneInString(s)
+ }
+ if !stringslite.IsSpace(r) {
+ break
+ }
+ s = s[:len(s)-n]
+ }
+ return s
+}
+
// TrimSpace returns a slice (substring) of the string s,
// with all leading and trailing white space removed,
// as defined by Unicode.
@@ -1092,7 +1120,7 @@
if c >= utf8.RuneSelf {
// If we run into a non-ASCII byte, fall back to the
// slower unicode-aware method on the remaining bytes.
- return TrimFunc(s[lo:], unicode.IsSpace)
+ return trimSpaceUnicode(s[lo:])
}
if asciiSpace[c] != 0 {
continue
@@ -1102,7 +1130,7 @@
for hi := len(s) - 1; hi >= 0; hi-- {
c := s[hi]
if c >= utf8.RuneSelf {
- return TrimRightFunc(s[:hi+1], unicode.IsSpace)
+ return trimRightSpaceUnicode(s[:hi+1])
}
if asciiSpace[c] == 0 {
// At this point, s[:hi+1] starts and ends with ASCII
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
bytes/TrimSpace/JustNonASCII 63.3ns 33.2ns -47.5% if r <= ' ' {can this just be one big switch block?
the ranges aren't that big either.
and comment on the hex codes what those runes are.
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
please use benchstat https://pkg.go.dev/golang.org/x/perf/cmd/benchstat
Done
can this just be one big switch block?
the ranges aren't that big either.
and comment on the hex codes what those runes are.
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |