Skip to content

Commit 94bdf81

Browse files
da-liiiclaude
andauthored
[0079] 修复 bytevector-base64-decode Scheme 实现并收紧 base64 性能基准 (#877)
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 2482799 commit 94bdf81

4 files changed

Lines changed: 289 additions & 66 deletions

File tree

bench/base64-perf.scm

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -88,48 +88,48 @@
8888
) ;define
8989

9090
(do ((i 0 (+ i 1)))
91-
((= i 20))
92-
(string-base64-encode ascii-medium)
93-
(string-base64-decode enc-ascii-medium)
94-
(bytevector-base64-encode bv-medium)
95-
(bytevector-base64-decode enc-bv-medium)
91+
((= i 5))
92+
(string-base64-encode ascii-small)
93+
(string-base64-decode enc-ascii-small)
94+
(bytevector-base64-encode bv-small)
95+
(bytevector-base64-decode enc-bv-small)
9696
) ;do
9797

98-
(define str-enc-tiny-iter 5000)
98+
(define str-enc-tiny-iter 3000)
9999

100-
(define str-enc-small-iter 2000)
100+
(define str-enc-small-iter 300)
101101

102-
(define str-enc-medium-iter 500)
102+
(define str-enc-medium-iter 30)
103103

104-
(define str-enc-large-iter 100)
104+
(define str-enc-large-iter 3)
105105

106-
(define str-decode-tiny-iter 5000)
106+
(define str-decode-tiny-iter 1500)
107107

108-
(define str-decode-small-iter 2000)
108+
(define str-decode-small-iter 150)
109109

110-
(define str-decode-medium-iter 500)
110+
(define str-decode-medium-iter 15)
111111

112-
(define str-decode-large-iter 100)
112+
(define str-decode-large-iter 2)
113113

114-
(define bv-enc-tiny-iter 5000)
114+
(define bv-enc-tiny-iter 3000)
115115

116-
(define bv-enc-small-iter 2000)
116+
(define bv-enc-small-iter 300)
117117

118-
(define bv-enc-medium-iter 500)
118+
(define bv-enc-medium-iter 30)
119119

120-
(define bv-enc-large-iter 100)
120+
(define bv-enc-large-iter 3)
121121

122-
(define bv-decode-tiny-iter 5000)
122+
(define bv-decode-tiny-iter 1500)
123123

124-
(define bv-decode-small-iter 2000)
124+
(define bv-decode-small-iter 150)
125125

126-
(define bv-decode-medium-iter 500)
126+
(define bv-decode-medium-iter 15)
127127

128-
(define bv-decode-large-iter 100)
128+
(define bv-decode-large-iter 2)
129129

130-
(define unified-enc-iter 500)
130+
(define unified-enc-iter 30)
131131

132-
(define unified-decode-iter 500)
132+
(define unified-decode-iter 15)
133133

134134
(display "=== (liii base64) 性能基准测试 ===")
135135
(newline)

devel/0079.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# [0079] bytevector-base64-decode Scheme 实现修复
2+
3+
## 任务相关的代码文件
4+
- goldfish/liii/base64.scm
5+
- tests/liii/base64/bytevector-base64-decode-test.scm
6+
- bench/base64-perf.scm
7+
8+
## 如何测试
9+
```
10+
xmake config --yes
11+
xmake b goldfish
12+
bin/gf fmt goldfish/liii/base64.scm
13+
bin/gf fmt tests/liii/base64/bytevector-base64-decode-test.scm
14+
bin/gf tests/liii/base64/bytevector-base64-decode-test.scm
15+
bin/gf tests/liii/base64/base64-decode-test.scm
16+
bin/gf tests/liii/base64/base64-encode-test.scm
17+
bin/gf bench/base64-perf.scm
18+
```
19+
20+
## 2026/06/26 bytevector-base64-decode Scheme 实现修复
21+
### What
22+
修复 `(liii base64)``bytevector-base64-decode` 的多个缺陷,并补全边界与异常用例测试。
23+
24+
1. 修复长度校验时机错误:原实现先按 `(* input-N 3/4)` 分配输出向量再校验 `modulo 4`,当输入长度不是 4 的倍数时 `make-bytevector` 收到比率(如 `9/2`)直接崩溃,无法按预期抛出 `value-error`。改为先校验长度、再分配,并把容量计算改为整数 `quotient`
25+
2. 修复填充合法性校验过松:原实现只在 c3/c4 处识别 `=`,无法拒绝 `=Zm9``Zg=v``====``Y===` 等"填充出现在非法位置"的输入;重写 `decode` 中的合法性判断,按 RFC 4648 仅允许 `VVVV` / `VVVP` / `VVPP` 三种四字符模式
26+
3. 修复非法字符静默接受:原实现对 `YWJ-``YWJ_``!m9v``Zm9v@` 等含 URL-safe 字符或标点的输入不会报错;新实现对所有非 alphabet、非合法填充的字符统一抛出 `value-error`
27+
4. 新增前置类型检查:输入非 bytevector 时抛出 `type-error`
28+
5. 新增 `tests/liii/base64/bytevector-base64-decode-test.scm`,覆盖空输入、1~6 字节全部填充情况、字符集边界(含 `0xff`、NUL 字节)、长输入、round-trip 一致性、非法长度 / 非法字符 / 填充位置错误 / 类型错误,共 36 个 check
29+
30+
### Why
31+
`bytevector-base64-decode` 在三个维度上有正确性缺陷:长度非 4X 时直接崩溃而非抛出预期的 `value-error`;填充位置错误的输入被错误解码(如 `Zg=v` 返回 `#u8(102)`);含非法字符的输入不报错,潜在地掩盖上游数据错误。这些缺陷使该函数无法作为可靠的解码原语使用,先在 Scheme 层修复语义,再讨论是否下沉到 C/C++ 层做性能优化。
32+
33+
### How
34+
- 把长度校验 `unless (zero? (modulo input-N 4))` 上移到分配输出缓冲之前,从根上避免比率作为 `make-bytevector` 参数的情况
35+
- 输出缓冲容量用 `(quotient (* input-N 3) 4)` 取整计算,避免 Scheme 比率运算
36+
- 重写每 4 字符一组的 `decode`:先按位置识别 `=`,再查 `BASE64_TO_BYTE_V`;只允许三种合法填充模式(无填充 / 1 个尾部填充 / 2 个连续尾部填充),其余一律 `value-error`
37+
- 类型检查在最外层 `unless (bytevector? bv)` 完成,与 `string-base64-decode`/`base64-decode` 的入口风格一致
38+
39+
## 2026/06/26 base64 性能基准收紧到 30s 以内
40+
### What
41+
调整 `bench/base64-perf.scm` 各档迭代次数,使整体运行时间从原先超过 60s 收紧到约 17s,便于在 CI 与本地频繁运行,同时仍能输出稳定的单次耗时数据。
42+
43+
1. warmup 由 20 次 medium 规模改为 5 次 small 规模,避免预热本身耗时过长
44+
2. 按规模线性递减迭代次数:tiny 3000、small 300、medium 30、large 3(decode 因更慢再减半)
45+
3. 统一入口 bench 由 500 次降至 30/15 次
46+
47+
### Why
48+
原 bench 总耗时超过 60s,单次 `large(10KB)` 档就要约 47s,严重阻碍后续以 `bytevector-base64-decode` 下沉到 C/C++ 为目标的迭代优化对比。把每档控制在 0.4~1.4s 区间,既保证统计稳定性,又让完整跑一遍低于 30s 的可接受阈值。
49+
50+
### How
51+
基于第一次实测得到的单次耗时(tiny ~0.4ms、small ~4.6ms、medium ~47ms、large ~470ms),按"每档目标 1~1.5s"反推迭代次数;decode 路径比 encode 慢约 30%,对应档位再折半,确保 16 个细项 + 统一入口 + warmup 的总和稳定落在 30s 内。
52+
53+
### 修复后的性能基线(17.3s)
54+
| 操作 | tiny(8B) | small(100B) | medium(1KB) | large(10KB) |
55+
|---|---|---|---|---|
56+
| string-base64-encode | 407µs | 4.5ms | 44.5ms | 434ms |
57+
| string-base64-decode | 510µs | 5.3ms | 53.7ms | 545ms |
58+
| bytevector-base64-encode | 201µs | 2.2ms | 21.7ms | 221ms |
59+
| bytevector-base64-decode | 279µs | 3.0ms | 30.7ms | 310ms |
60+
61+
观察:bytevector 路径比 string 快约 2 倍(省去 utf8 转换);decode 比 encode 慢约 30%(额外的合法性校验);large 档单次 310~545ms,是后续 C/C++ 优化的重点目标。
62+
63+
## 后续任务(暂不在本 PR 完成)
64+
- 利用 tbox 重新实现 `bytevector-base64-decode`,与 Scheme 实现做性能对比

goldfish/liii/base64.scm

Lines changed: 52 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -90,54 +90,63 @@
9090
) ;define-constant
9191

9292
(define (bytevector-base64-decode bv)
93-
(define (decode c1 c2 c3 c4)
94-
(let* ((b1 (BASE64_TO_BYTE_V c1))
95-
(b2 (BASE64_TO_BYTE_V c2))
96-
(b3 (BASE64_TO_BYTE_V c3))
97-
(b4 (BASE64_TO_BYTE_V c4))
98-
) ;
99-
(if (or (negative? b1)
100-
(negative? b2)
101-
(and (negative? b3) (not (equal? c3 BASE64_PAD_BYTE)))
102-
(and (negative? b4) (not (equal? c4 BASE64_PAD_BYTE)))
103-
) ;or
104-
(value-error "Invalid base64 input")
105-
(values (bitwise-ior (ash b1 2) (ash b2 -4))
106-
(bitwise-and (bitwise-ior (ash b2 4) (ash b3 -2)) 255)
107-
(bitwise-and (bitwise-ior (ash b3 6) b4) 255)
108-
(if (negative? b3) 1 (if (negative? b4) 2 3))
109-
) ;values
110-
) ;if
111-
) ;let*
112-
) ;define
113-
(let* ((input-N (bytevector-length bv))
114-
(output-N (* input-N 3/4))
115-
(output (make-bytevector output-N))
116-
) ;
93+
(unless (bytevector? bv)
94+
(type-error "input must be bytevector")
95+
) ;unless
96+
(let ((input-N (bytevector-length bv)))
11797
(unless (zero? (modulo input-N 4))
11898
(value-error "length of the input bytevector must be 4X")
11999
) ;unless
120-
(let loop
121-
((i 0) (j 0))
122-
(if (< i input-N)
123-
(receive (r1 r2 r3 cnt)
124-
(decode (bv i) (bv (+ i 1)) (bv (+ i 2)) (bv (+ i 3)))
125-
(bytevector-u8-set! output j r1)
126-
(when (>= cnt 2)
127-
(bytevector-u8-set! output (+ j 1) r2)
128-
) ;when
129-
(when (>= cnt 3)
130-
(bytevector-u8-set! output (+ j 2) r3)
131-
) ;when
132-
(loop (+ i 4) (+ j cnt))
133-
) ;receive
134-
(let ((final (make-bytevector j)))
135-
(vector-copy! final 0 output 0 j)
136-
final
100+
(define (decode c1 c2 c3 c4)
101+
(define pad? (lambda (c) (equal? c BASE64_PAD_BYTE)))
102+
(define c3-pad? (pad? c3))
103+
(define c4-pad? (pad? c4))
104+
(define b1 (BASE64_TO_BYTE_V c1))
105+
(define b2 (BASE64_TO_BYTE_V c2))
106+
(define b3 (BASE64_TO_BYTE_V c3))
107+
(define b4 (BASE64_TO_BYTE_V c4))
108+
(unless (and (not (pad? c1))
109+
(not (pad? c2))
110+
(not (negative? b1))
111+
(not (negative? b2))
112+
(or (and (not c3-pad?) (not c4-pad?) (not (negative? b3)) (not (negative? b4)))
113+
(and (not c3-pad?) c4-pad? (not (negative? b3)))
114+
(and c3-pad? c4-pad?)
115+
) ;or
116+
) ;and
117+
(value-error "Invalid base64 input")
118+
) ;unless
119+
(values (bitwise-ior (ash b1 2) (ash b2 -4))
120+
(if c3-pad? 0 (bitwise-and (bitwise-ior (ash b2 4) (ash b3 -2)) 255))
121+
(if c4-pad? 0 (bitwise-and (bitwise-ior (ash b3 6) b4) 255))
122+
(if c3-pad? 1 (if c4-pad? 2 3))
123+
) ;values
124+
) ;define
125+
(let ((output-N (quotient (* input-N 3) 4)))
126+
(let ((output (make-bytevector output-N)))
127+
(let loop
128+
((i 0) (j 0))
129+
(if (< i input-N)
130+
(receive (r1 r2 r3 cnt)
131+
(decode (bv i) (bv (+ i 1)) (bv (+ i 2)) (bv (+ i 3)))
132+
(bytevector-u8-set! output j r1)
133+
(when (>= cnt 2)
134+
(bytevector-u8-set! output (+ j 1) r2)
135+
) ;when
136+
(when (>= cnt 3)
137+
(bytevector-u8-set! output (+ j 2) r3)
138+
) ;when
139+
(loop (+ i 4) (+ j cnt))
140+
) ;receive
141+
(let ((final (make-bytevector j)))
142+
(vector-copy! final 0 output 0 j)
143+
final
144+
) ;let
145+
) ;if
137146
) ;let
138-
) ;if
147+
) ;let
139148
) ;let
140-
) ;let*
149+
) ;let
141150
) ;define
142151

143152
(define string-base64-decode
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
(import (liii base64) (liii check))
2+
3+
(check-set-mode! 'report-failed)
4+
5+
;; bytevector-base64-decode
6+
;; 将 Base64 编码的字节向量解码为原始字节向量。
7+
;;
8+
;; 语法
9+
;; ----
10+
;; (bytevector-base64-decode bv)
11+
;;
12+
;; 参数
13+
;; ----
14+
;; bv : bytevector
15+
;; 要解码的 Base64 字节向量(长度必须是 4 的倍数)。
16+
;;
17+
;; 返回值
18+
;; ----
19+
;; bytevector
20+
;; 解码后的原始字节序列。
21+
;;
22+
;; 注意
23+
;; ----
24+
;; - 空字节向量解码为空字节向量
25+
;; - 支持 1~2 个 '=' 填充
26+
;; - 输入字节可以是 0~255 任意值,结果字节向量保持原始字节,不做 UTF-8 转换
27+
;;
28+
;; 错误处理
29+
;; ----
30+
;; - 长度不是 4 的倍数 -> value-error
31+
;; - 含非法字符(含空白、URL-safe 字符) -> value-error
32+
;; - 填充位置错误 -> value-error
33+
;; - 输入非 bytevector -> type-error
34+
35+
;; 空输入
36+
(check (bytevector-base64-decode #u8()) => #u8())
37+
38+
;; 标准示例(与字符串入口一致)
39+
(check (bytevector-base64-decode (string->utf8 "Zm9v")) => #u8(102 111 111))
40+
(check (bytevector-base64-decode (string->utf8 "Zm9vYmFy"))
41+
=>
42+
#u8(102 111 111 98 97 114)
43+
) ;check
44+
45+
;; 各种长度(1~6 字节)覆盖全部填充情况
46+
(check (bytevector-base64-decode (string->utf8 "AA==")) => #u8(0))
47+
(check (bytevector-base64-decode (string->utf8 "AQ==")) => #u8(1))
48+
(check (bytevector-base64-decode (string->utf8 "/w==")) => #u8(255))
49+
(check (bytevector-base64-decode (string->utf8 "AAA=")) => #u8(0 0))
50+
(check (bytevector-base64-decode (string->utf8 "//8=")) => #u8(255 255))
51+
(check (bytevector-base64-decode (string->utf8 "AAAA")) => #u8(0 0 0))
52+
(check (bytevector-base64-decode (string->utf8 "AAAB")) => #u8(0 0 1))
53+
(check (bytevector-base64-decode (string->utf8 "AAAAAA==")) => #u8(0 0 0 0))
54+
(check (bytevector-base64-decode (string->utf8 "AAAAAAA=")) => #u8(0 0 0 0 0))
55+
(check (bytevector-base64-decode (string->utf8 "AAAAAAAB")) => #u8(0 0 0 0 0 1))
56+
57+
;; 标准 Base64 字符集边界
58+
(check (bytevector-base64-decode (string->utf8 "AAAA")) => #u8(0 0 0))
59+
(check (bytevector-base64-decode (string->utf8 "////")) => #u8(255 255 255))
60+
(check (bytevector-base64-decode (string->utf8 "+/3+")) => #u8(251 253 254))
61+
62+
;; 含 NUL 字节的解码(不应被截断)
63+
(check (bytevector-base64-decode (string->utf8 "AAAA")) => #u8(0 0 0))
64+
65+
;; 较长输入
66+
(check (bytevector-base64-decode (string->utf8 "VGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIHRoZSBsYXp5IGRvZy4=")
67+
) ;bytevector-base64-decode
68+
=>
69+
#u8(84
70+
104
71+
101
72+
32
73+
113
74+
117
75+
105
76+
99
77+
107
78+
32
79+
98
80+
114
81+
111
82+
119
83+
110
84+
32
85+
102
86+
111
87+
120
88+
32
89+
106
90+
117
91+
109
92+
112
93+
115
94+
32
95+
111
96+
118
97+
101
98+
114
99+
32
100+
116
101+
104
102+
101
103+
32
104+
108
105+
97
106+
122
107+
121
108+
32
109+
100
110+
111
111+
103
112+
46
113+
) ;#
114+
) ;check
115+
116+
;; Round-trip 一致性
117+
(check (bytevector-base64-decode (bytevector-base64-encode #u8(1 2 3 4 5)))
118+
=>
119+
#u8(1 2 3 4 5)
120+
) ;check
121+
(check (bytevector-base64-decode (bytevector-base64-encode #u8())) => #u8())
122+
(check (bytevector-base64-decode (bytevector-base64-encode #u8(255 0 127 128)))
123+
=>
124+
#u8(255 0 127 128)
125+
) ;check
126+
127+
;; 非法输入:长度不是 4 的倍数
128+
(check-catch 'value-error (bytevector-base64-decode (string->utf8 "Zg")))
129+
(check-catch 'value-error (bytevector-base64-decode (string->utf8 "Zm9")))
130+
(check-catch 'value-error (bytevector-base64-decode (string->utf8 "Zm9vYg")))
131+
(check-catch 'value-error (bytevector-base64-decode (string->utf8 "Zm9vYg===")))
132+
133+
;; 非法字符(含空白、URL-safe 字符 '-' '_' 等)
134+
(check-catch 'value-error (bytevector-base64-decode (string->utf8 "!m9v")))
135+
(check-catch 'value-error (bytevector-base64-decode (string->utf8 "Zm9v@")))
136+
(check-catch 'value-error (bytevector-base64-decode (string->utf8 "YWJ-")))
137+
(check-catch 'value-error (bytevector-base64-decode (string->utf8 "YWJ_")))
138+
139+
;; 填充位置错误
140+
(check-catch 'value-error (bytevector-base64-decode (string->utf8 "=Zm9")))
141+
(check-catch 'value-error (bytevector-base64-decode (string->utf8 "Zg=v")))
142+
(check-catch 'value-error (bytevector-base64-decode (string->utf8 "====")))
143+
(check-catch 'value-error (bytevector-base64-decode (string->utf8 "Y===")))
144+
145+
;; 类型错误
146+
(check-catch 'type-error (bytevector-base64-decode 123))
147+
(check-catch 'type-error (bytevector-base64-decode "Zm9v"))
148+
(check-catch 'type-error (bytevector-base64-decode 'sym))
149+
150+
(check-report)

0 commit comments

Comments
 (0)