Skip to content

Commit 9cd1054

Browse files
authored
perf: Speed up UTF-16 decoding (#138)
* speed up UTF-16 decoding * fix UTF-16 slice length
1 parent d75de9b commit 9cd1054

3 files changed

Lines changed: 109 additions & 5 deletions

File tree

pkg/kevent/kparams/readers.go

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,7 @@
2222
package kparams
2323

2424
import (
25-
"syscall"
26-
"unicode/utf16"
25+
"github.com/rabbitstack/fibratus/pkg/syscall/utf16"
2726
"unsafe"
2827
)
2928

@@ -79,7 +78,7 @@ func ReadUTF16String(buf uintptr, offset, length uint16) (string, uint16) {
7978
return "", 0
8079
}
8180
s := (*[1<<30 - 1]uint16)(unsafe.Pointer(buf + uintptr(offset)))[: length-offset : length-offset]
82-
return syscall.UTF16ToString(s), uint16(len(s) + 2)
81+
return utf16.Decode(s[:len(s)/2-1-2]), uint16(len(s) + 2)
8382
}
8483

8584
// ConsumeUTF16String reads the byte slice with UTF16-encoded string
@@ -89,13 +88,13 @@ func ConsumeUTF16String(buf uintptr, offset, length uint16) string {
8988
return ""
9089
}
9190
s := (*[1<<30 - 1]uint16)(unsafe.Pointer(buf + uintptr(offset)))[: length-offset : length-offset]
92-
return string(utf16.Decode(s[:len(s)/2-1]))
91+
return utf16.Decode(s[:len(s)/2-1])
9392
}
9493

9594
// ReadSID reads the security identifier from the provided buffer.
9695
func ReadSID(buf uintptr, offset uint16) ([]byte, uint16) {
9796
// this is a Security Token which can be null and takes 4 bytes.
98-
// Otherwise it is an 8 byte structure (TOKEN_USER) followed by SID,
97+
// Otherwise, it is an 8 byte structure (TOKEN_USER) followed by SID,
9998
// which is variable size depending on the 2nd byte in the SID
10099
sid := ReadUint32(buf, offset)
101100
if sid == 0 {

pkg/syscall/utf16/string.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"reflect"
2626
"syscall"
2727
"unicode/utf16"
28+
"unicode/utf8"
2829
"unsafe"
2930
)
3031

@@ -74,3 +75,37 @@ func PtrToString(p unsafe.Pointer) string {
7475
// Remove trailing NUL and decode into a Go string.
7576
return string(utf16.Decode(s[:len(s)-1]))
7677
}
78+
79+
const (
80+
// 0xd800-0xdc00 encodes the high 10 bits of a pair.
81+
surr1 = 0xd800
82+
// 0xdc00-0xe000 encodes the low 10 bits of a pair.
83+
surr2 = 0xdc00
84+
)
85+
86+
func isHighSurrogate(r rune) bool { return r >= surr1 && r <= 0xdbff }
87+
func isLowSurrogate(r rune) bool { return r >= surr2 && r <= 0xdfff }
88+
89+
// Decode decodes the UTF16-encoded string to UTF-8 string. This function
90+
// exhibits much better performance than the standard library counterpart.
91+
// All credits go to: https://gist.github.com/skeeto/09f1410183d246f9b18cba95c4e602f0
92+
func Decode(p []uint16) string {
93+
s := make([]byte, 0, 2*len(p))
94+
for i := 0; i < len(p); i++ {
95+
r := rune(0xfffd)
96+
r1 := rune(p[i])
97+
if isHighSurrogate(r1) {
98+
if i+1 < len(p) {
99+
r2 := rune(p[i+1])
100+
if isLowSurrogate(r2) {
101+
i++
102+
r = 0x10000 + (r1-surr1)<<10 + (r2 - surr2)
103+
}
104+
}
105+
} else if !isLowSurrogate(r) {
106+
r = r1
107+
}
108+
s = utf8.AppendRune(s, r)
109+
}
110+
return string(s)
111+
}

pkg/syscall/utf16/string_test.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
* Copyright 2021-2022 by Nedim Sabic Sabic
3+
* https://www.fibratus.io
4+
* All Rights Reserved.
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
package utf16
20+
21+
import (
22+
"github.com/stretchr/testify/require"
23+
"math/rand"
24+
"testing"
25+
"unicode/utf16"
26+
)
27+
28+
func TestDecode(t *testing.T) {
29+
for i := 0; i < 24; i++ {
30+
buf := genbuf(1 << i)
31+
w := string(utf16.Decode(buf))
32+
g := Decode(buf)
33+
if w != g {
34+
t.Errorf("mismatch on 1<<%d", i)
35+
}
36+
}
37+
s := []rune("Do you want café?")
38+
encoded := utf16.Encode(s)
39+
require.Equal(t, "Do you want café?", Decode(encoded))
40+
}
41+
42+
func BenchmarkStdlibDecode(b *testing.B) {
43+
b.ReportAllocs()
44+
b.StopTimer()
45+
buf := genbuf(b.N)
46+
b.StartTimer()
47+
_ = string(utf16.Decode(buf))
48+
}
49+
50+
func BenchmarkDecode(b *testing.B) {
51+
b.ReportAllocs()
52+
b.StopTimer()
53+
buf := genbuf(b.N)
54+
b.StartTimer()
55+
_ = Decode(buf)
56+
}
57+
58+
func genbuf(n int) []uint16 {
59+
r := rand.New(rand.NewSource(int64(n)))
60+
buf := make([]rune, n)
61+
for i := 0; i < n; i++ {
62+
// simulate mostly-ASCII
63+
if r.Intn(100) == 0 {
64+
buf[i] = rune(r.Intn(0x10ffff + 1))
65+
} else {
66+
buf[i] = rune(r.Intn(1 << 7))
67+
}
68+
}
69+
return utf16.Encode(buf)
70+
}

0 commit comments

Comments
 (0)