Skip to content

Commit dc8fe66

Browse files
authored
Merge pull request #54 from klauspost/add-full-lut
Add full lookup table for single rune width.
2 parents 1ccc74d + acbd689 commit dc8fe66

File tree

3 files changed

+168
-28
lines changed

3 files changed

+168
-28
lines changed

benchmark_test.go

Lines changed: 72 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,42 @@ var benchSink int
1414
func benchRuneWidth(b *testing.B, eastAsianWidth bool, start, stop rune, want int) int {
1515
b.Helper()
1616
n := 0
17-
got := -1
18-
c := NewCondition()
19-
c.EastAsianWidth = eastAsianWidth
20-
for i := 0; i < b.N; i++ {
21-
got = n
22-
for r := start; r < stop; r++ {
23-
n += c.RuneWidth(r)
17+
b.Run("regular", func(b *testing.B) {
18+
got := -1
19+
c := NewCondition()
20+
c.EastAsianWidth = eastAsianWidth
21+
b.ReportAllocs()
22+
b.ResetTimer()
23+
for i := 0; i < b.N; i++ {
24+
got = n
25+
for r := start; r < stop; r++ {
26+
n += c.RuneWidth(r)
27+
}
28+
got = n - got
2429
}
25-
got = n - got
26-
}
27-
if want != 0 && got != want { // some extra checks
28-
b.Errorf("got %d, want %d\n", got, want)
29-
}
30+
if want != 0 && got != want { // some extra checks
31+
b.Errorf("got %d, want %d\n", got, want)
32+
}
33+
})
34+
b.Run("lut", func(b *testing.B) {
35+
got := -1
36+
n = 0
37+
c := NewCondition()
38+
c.EastAsianWidth = eastAsianWidth
39+
c.CreateLUT()
40+
b.ReportAllocs()
41+
b.ResetTimer()
42+
for i := 0; i < b.N; i++ {
43+
got = n
44+
for r := start; r < stop; r++ {
45+
n += c.RuneWidth(r)
46+
}
47+
got = n - got
48+
}
49+
if want != 0 && got != want { // some extra checks
50+
b.Errorf("got %d, want %d\n", got, want)
51+
}
52+
})
3053
return n
3154
}
3255
func BenchmarkRuneWidthAll(b *testing.B) {
@@ -49,20 +72,44 @@ func BenchmarkRuneWidth768EastAsian(b *testing.B) {
4972
func benchString1Width(b *testing.B, eastAsianWidth bool, start, stop rune, want int) int {
5073
b.Helper()
5174
n := 0
52-
got := -1
53-
c := NewCondition()
54-
c.EastAsianWidth = eastAsianWidth
55-
for i := 0; i < b.N; i++ {
56-
got = n
57-
for r := start; r < stop; r++ {
58-
s := string(r)
59-
n += c.StringWidth(s)
75+
b.Run("regular", func(b *testing.B) {
76+
got := -1
77+
c := NewCondition()
78+
c.EastAsianWidth = eastAsianWidth
79+
b.ResetTimer()
80+
b.ReportAllocs()
81+
for i := 0; i < b.N; i++ {
82+
got = n
83+
for r := start; r < stop; r++ {
84+
s := string(r)
85+
n += c.StringWidth(s)
86+
}
87+
got = n - got
6088
}
61-
got = n - got
62-
}
63-
if want != 0 && got != want { // some extra checks
64-
b.Errorf("got %d, want %d\n", got, want)
65-
}
89+
if want != 0 && got != want { // some extra checks
90+
b.Errorf("got %d, want %d\n", got, want)
91+
}
92+
})
93+
b.Run("lut", func(b *testing.B) {
94+
got := -1
95+
n = 0
96+
c := NewCondition()
97+
c.EastAsianWidth = eastAsianWidth
98+
c.CreateLUT()
99+
b.ResetTimer()
100+
b.ReportAllocs()
101+
for i := 0; i < b.N; i++ {
102+
got = n
103+
for r := start; r < stop; r++ {
104+
s := string(r)
105+
n += c.StringWidth(s)
106+
}
107+
got = n - got
108+
}
109+
if want != 0 && got != want { // some extra checks
110+
b.Errorf("got %d, want %d\n", got, want)
111+
}
112+
})
66113
return n
67114
}
68115
func BenchmarkString1WidthAll(b *testing.B) {

runewidth.go

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,13 @@ func handleEnv() {
3434
EastAsianWidth = env == "1"
3535
}
3636
// update DefaultCondition
37-
DefaultCondition.EastAsianWidth = EastAsianWidth
37+
if DefaultCondition.EastAsianWidth != EastAsianWidth {
38+
DefaultCondition.EastAsianWidth = EastAsianWidth
39+
if len(DefaultCondition.combinedLut) > 0 {
40+
DefaultCondition.combinedLut = DefaultCondition.combinedLut[:0]
41+
CreateLUT()
42+
}
43+
}
3844
}
3945

4046
type interval struct {
@@ -89,6 +95,7 @@ var nonprint = table{
8995

9096
// Condition have flag EastAsianWidth whether the current locale is CJK or not.
9197
type Condition struct {
98+
combinedLut []byte
9299
EastAsianWidth bool
93100
StrictEmojiNeutral bool
94101
}
@@ -104,10 +111,16 @@ func NewCondition() *Condition {
104111
// RuneWidth returns the number of cells in r.
105112
// See http://www.unicode.org/reports/tr11/
106113
func (c *Condition) RuneWidth(r rune) int {
114+
if r < 0 || r > 0x10FFFF {
115+
return 0
116+
}
117+
if len(c.combinedLut) > 0 {
118+
return int(c.combinedLut[r>>1]>>(uint(r&1)*4)) & 3
119+
}
107120
// optimized version, verified by TestRuneWidthChecksums()
108121
if !c.EastAsianWidth {
109122
switch {
110-
case r < 0x20 || r > 0x10FFFF:
123+
case r < 0x20:
111124
return 0
112125
case (r >= 0x7F && r <= 0x9F) || r == 0xAD: // nonprint
113126
return 0
@@ -124,7 +137,7 @@ func (c *Condition) RuneWidth(r rune) int {
124137
}
125138
} else {
126139
switch {
127-
case r < 0 || r > 0x10FFFF || inTables(r, nonprint, combining):
140+
case inTables(r, nonprint, combining):
128141
return 0
129142
case inTable(r, narrow):
130143
return 1
@@ -138,6 +151,27 @@ func (c *Condition) RuneWidth(r rune) int {
138151
}
139152
}
140153

154+
// CreateLUT will create an in-memory lookup table of 557056 bytes for faster operation.
155+
// This should not be called concurrently with other operations on c.
156+
// If options in c is changed, CreateLUT should be called again.
157+
func (c *Condition) CreateLUT() {
158+
const max = 0x110000
159+
lut := c.combinedLut
160+
if len(c.combinedLut) != 0 {
161+
// Remove so we don't use it.
162+
c.combinedLut = nil
163+
} else {
164+
lut = make([]byte, max/2)
165+
}
166+
for i := range lut {
167+
i32 := int32(i * 2)
168+
x0 := c.RuneWidth(i32)
169+
x1 := c.RuneWidth(i32 + 1)
170+
lut[i] = uint8(x0) | uint8(x1)<<4
171+
}
172+
c.combinedLut = lut
173+
}
174+
141175
// StringWidth return width as you can see
142176
func (c *Condition) StringWidth(s string) (width int) {
143177
g := uniseg.NewGraphemes(s)
@@ -271,3 +305,12 @@ func FillLeft(s string, w int) string {
271305
func FillRight(s string, w int) string {
272306
return DefaultCondition.FillRight(s, w)
273307
}
308+
309+
// CreateLUT will create an in-memory lookup table of 557055 bytes for faster operation.
310+
// This should not be called concurrently with other operations.
311+
func CreateLUT() {
312+
if len(DefaultCondition.combinedLut) > 0 {
313+
return
314+
}
315+
DefaultCondition.CreateLUT()
316+
}

runewidth_test.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
//go:build !js && !appengine
12
// +build !js,!appengine
23

34
package runewidth
@@ -87,7 +88,56 @@ func TestRuneWidthChecksums(t *testing.T) {
8788
t.Errorf("TestRuneWidthChecksums = %s,\n\tsha256 = %s want %s",
8889
testcase.name, gotSHA, testcase.wantSHA)
8990
}
91+
92+
// Test with LUT
93+
c.CreateLUT()
94+
for r := rune(0); r <= utf8.MaxRune; r++ {
95+
buf[r] = byte(c.RuneWidth(r))
96+
}
97+
gotSHA = fmt.Sprintf("%x", sha256.Sum256(buf))
98+
if gotSHA != testcase.wantSHA {
99+
t.Errorf("TestRuneWidthChecksums = %s,\n\tsha256 = %s want %s",
100+
testcase.name, gotSHA, testcase.wantSHA)
101+
}
102+
}
103+
}
104+
105+
func TestDefaultLUT(t *testing.T) {
106+
var testcases = []struct {
107+
name string
108+
eastAsianWidth bool
109+
wantSHA string
110+
}{
111+
{"ea-no", false, "4eb632b105d3b2c800dda9141381d0b8a95250a3a5c7f1a5ca2c4d4daaa85234"},
112+
{"ea-yes", true, "c2ddc3bdf42d81d4c23050e21eda46eb639b38b15322d35e8eb6c26f3b83ce92"},
113+
}
114+
115+
old := os.Getenv("RUNEWIDTH_EASTASIAN")
116+
defer os.Setenv("RUNEWIDTH_EASTASIAN", old)
117+
118+
CreateLUT()
119+
for _, testcase := range testcases {
120+
c := DefaultCondition
121+
122+
if testcase.eastAsianWidth {
123+
os.Setenv("RUNEWIDTH_EASTASIAN", "1")
124+
} else {
125+
os.Setenv("RUNEWIDTH_EASTASIAN", "0")
126+
}
127+
handleEnv()
128+
129+
buf := make([]byte, utf8.MaxRune+1)
130+
for r := rune(0); r <= utf8.MaxRune; r++ {
131+
buf[r] = byte(c.RuneWidth(r))
132+
}
133+
gotSHA := fmt.Sprintf("%x", sha256.Sum256(buf))
134+
if gotSHA != testcase.wantSHA {
135+
t.Errorf("TestRuneWidthChecksums = %s,\n\tsha256 = %s want %s",
136+
testcase.name, gotSHA, testcase.wantSHA)
137+
}
90138
}
139+
// Remove for other tests.
140+
DefaultCondition.combinedLut = nil
91141
}
92142

93143
func checkInterval(first, last rune) bool {

0 commit comments

Comments
 (0)