Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 23 additions & 23 deletions bench/base64-perf.scm
Original file line number Diff line number Diff line change
Expand Up @@ -88,48 +88,48 @@
) ;define

(do ((i 0 (+ i 1)))
((= i 20))
(string-base64-encode ascii-medium)
(string-base64-decode enc-ascii-medium)
(bytevector-base64-encode bv-medium)
(bytevector-base64-decode enc-bv-medium)
((= i 5))
(string-base64-encode ascii-small)
(string-base64-decode enc-ascii-small)
(bytevector-base64-encode bv-small)
(bytevector-base64-decode enc-bv-small)
) ;do

(define str-enc-tiny-iter 5000)
(define str-enc-tiny-iter 3000)

(define str-enc-small-iter 2000)
(define str-enc-small-iter 300)

(define str-enc-medium-iter 500)
(define str-enc-medium-iter 30)

(define str-enc-large-iter 100)
(define str-enc-large-iter 3)

(define str-decode-tiny-iter 5000)
(define str-decode-tiny-iter 1500)

(define str-decode-small-iter 2000)
(define str-decode-small-iter 150)

(define str-decode-medium-iter 500)
(define str-decode-medium-iter 15)

(define str-decode-large-iter 100)
(define str-decode-large-iter 2)

(define bv-enc-tiny-iter 5000)
(define bv-enc-tiny-iter 3000)

(define bv-enc-small-iter 2000)
(define bv-enc-small-iter 300)

(define bv-enc-medium-iter 500)
(define bv-enc-medium-iter 30)

(define bv-enc-large-iter 100)
(define bv-enc-large-iter 3)

(define bv-decode-tiny-iter 5000)
(define bv-decode-tiny-iter 1500)

(define bv-decode-small-iter 2000)
(define bv-decode-small-iter 150)

(define bv-decode-medium-iter 500)
(define bv-decode-medium-iter 15)

(define bv-decode-large-iter 100)
(define bv-decode-large-iter 2)

(define unified-enc-iter 500)
(define unified-enc-iter 30)

(define unified-decode-iter 500)
(define unified-decode-iter 15)

(display "=== (liii base64) 性能基准测试 ===")
(newline)
Expand Down
64 changes: 64 additions & 0 deletions devel/0079.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# [0079] bytevector-base64-decode Scheme 实现修复

## 任务相关的代码文件
- goldfish/liii/base64.scm
- tests/liii/base64/bytevector-base64-decode-test.scm
- bench/base64-perf.scm

## 如何测试
```
xmake config --yes
xmake b goldfish
bin/gf fmt goldfish/liii/base64.scm
bin/gf fmt tests/liii/base64/bytevector-base64-decode-test.scm
bin/gf tests/liii/base64/bytevector-base64-decode-test.scm
bin/gf tests/liii/base64/base64-decode-test.scm
bin/gf tests/liii/base64/base64-encode-test.scm
bin/gf bench/base64-perf.scm
```

## 2026/06/26 bytevector-base64-decode Scheme 实现修复
### What
修复 `(liii base64)` 库 `bytevector-base64-decode` 的多个缺陷,并补全边界与异常用例测试。

1. 修复长度校验时机错误:原实现先按 `(* input-N 3/4)` 分配输出向量再校验 `modulo 4`,当输入长度不是 4 的倍数时 `make-bytevector` 收到比率(如 `9/2`)直接崩溃,无法按预期抛出 `value-error`。改为先校验长度、再分配,并把容量计算改为整数 `quotient`
2. 修复填充合法性校验过松:原实现只在 c3/c4 处识别 `=`,无法拒绝 `=Zm9`、`Zg=v`、`====`、`Y===` 等"填充出现在非法位置"的输入;重写 `decode` 中的合法性判断,按 RFC 4648 仅允许 `VVVV` / `VVVP` / `VVPP` 三种四字符模式
3. 修复非法字符静默接受:原实现对 `YWJ-`、`YWJ_`、`!m9v`、`Zm9v@` 等含 URL-safe 字符或标点的输入不会报错;新实现对所有非 alphabet、非合法填充的字符统一抛出 `value-error`
4. 新增前置类型检查:输入非 bytevector 时抛出 `type-error`
5. 新增 `tests/liii/base64/bytevector-base64-decode-test.scm`,覆盖空输入、1~6 字节全部填充情况、字符集边界(含 `0xff`、NUL 字节)、长输入、round-trip 一致性、非法长度 / 非法字符 / 填充位置错误 / 类型错误,共 36 个 check

### Why
原 `bytevector-base64-decode` 在三个维度上有正确性缺陷:长度非 4X 时直接崩溃而非抛出预期的 `value-error`;填充位置错误的输入被错误解码(如 `Zg=v` 返回 `#u8(102)`);含非法字符的输入不报错,潜在地掩盖上游数据错误。这些缺陷使该函数无法作为可靠的解码原语使用,先在 Scheme 层修复语义,再讨论是否下沉到 C/C++ 层做性能优化。

### How
- 把长度校验 `unless (zero? (modulo input-N 4))` 上移到分配输出缓冲之前,从根上避免比率作为 `make-bytevector` 参数的情况
- 输出缓冲容量用 `(quotient (* input-N 3) 4)` 取整计算,避免 Scheme 比率运算
- 重写每 4 字符一组的 `decode`:先按位置识别 `=`,再查 `BASE64_TO_BYTE_V`;只允许三种合法填充模式(无填充 / 1 个尾部填充 / 2 个连续尾部填充),其余一律 `value-error`
- 类型检查在最外层 `unless (bytevector? bv)` 完成,与 `string-base64-decode`/`base64-decode` 的入口风格一致

## 2026/06/26 base64 性能基准收紧到 30s 以内
### What
调整 `bench/base64-perf.scm` 各档迭代次数,使整体运行时间从原先超过 60s 收紧到约 17s,便于在 CI 与本地频繁运行,同时仍能输出稳定的单次耗时数据。

1. warmup 由 20 次 medium 规模改为 5 次 small 规模,避免预热本身耗时过长
2. 按规模线性递减迭代次数:tiny 3000、small 300、medium 30、large 3(decode 因更慢再减半)
3. 统一入口 bench 由 500 次降至 30/15 次

### Why
原 bench 总耗时超过 60s,单次 `large(10KB)` 档就要约 47s,严重阻碍后续以 `bytevector-base64-decode` 下沉到 C/C++ 为目标的迭代优化对比。把每档控制在 0.4~1.4s 区间,既保证统计稳定性,又让完整跑一遍低于 30s 的可接受阈值。

### How
基于第一次实测得到的单次耗时(tiny ~0.4ms、small ~4.6ms、medium ~47ms、large ~470ms),按"每档目标 1~1.5s"反推迭代次数;decode 路径比 encode 慢约 30%,对应档位再折半,确保 16 个细项 + 统一入口 + warmup 的总和稳定落在 30s 内。

### 修复后的性能基线(17.3s)
| 操作 | tiny(8B) | small(100B) | medium(1KB) | large(10KB) |
|---|---|---|---|---|
| string-base64-encode | 407µs | 4.5ms | 44.5ms | 434ms |
| string-base64-decode | 510µs | 5.3ms | 53.7ms | 545ms |
| bytevector-base64-encode | 201µs | 2.2ms | 21.7ms | 221ms |
| bytevector-base64-decode | 279µs | 3.0ms | 30.7ms | 310ms |

观察:bytevector 路径比 string 快约 2 倍(省去 utf8 转换);decode 比 encode 慢约 30%(额外的合法性校验);large 档单次 310~545ms,是后续 C/C++ 优化的重点目标。

## 后续任务(暂不在本 PR 完成)
- 利用 tbox 重新实现 `bytevector-base64-decode`,与 Scheme 实现做性能对比
95 changes: 52 additions & 43 deletions goldfish/liii/base64.scm
Original file line number Diff line number Diff line change
Expand Up @@ -90,54 +90,63 @@
) ;define-constant

(define (bytevector-base64-decode bv)
(define (decode c1 c2 c3 c4)
(let* ((b1 (BASE64_TO_BYTE_V c1))
(b2 (BASE64_TO_BYTE_V c2))
(b3 (BASE64_TO_BYTE_V c3))
(b4 (BASE64_TO_BYTE_V c4))
) ;
(if (or (negative? b1)
(negative? b2)
(and (negative? b3) (not (equal? c3 BASE64_PAD_BYTE)))
(and (negative? b4) (not (equal? c4 BASE64_PAD_BYTE)))
) ;or
(value-error "Invalid base64 input")
(values (bitwise-ior (ash b1 2) (ash b2 -4))
(bitwise-and (bitwise-ior (ash b2 4) (ash b3 -2)) 255)
(bitwise-and (bitwise-ior (ash b3 6) b4) 255)
(if (negative? b3) 1 (if (negative? b4) 2 3))
) ;values
) ;if
) ;let*
) ;define
(let* ((input-N (bytevector-length bv))
(output-N (* input-N 3/4))
(output (make-bytevector output-N))
) ;
(unless (bytevector? bv)
(type-error "input must be bytevector")
) ;unless
(let ((input-N (bytevector-length bv)))
(unless (zero? (modulo input-N 4))
(value-error "length of the input bytevector must be 4X")
) ;unless
(let loop
((i 0) (j 0))
(if (< i input-N)
(receive (r1 r2 r3 cnt)
(decode (bv i) (bv (+ i 1)) (bv (+ i 2)) (bv (+ i 3)))
(bytevector-u8-set! output j r1)
(when (>= cnt 2)
(bytevector-u8-set! output (+ j 1) r2)
) ;when
(when (>= cnt 3)
(bytevector-u8-set! output (+ j 2) r3)
) ;when
(loop (+ i 4) (+ j cnt))
) ;receive
(let ((final (make-bytevector j)))
(vector-copy! final 0 output 0 j)
final
(define (decode c1 c2 c3 c4)
(define pad? (lambda (c) (equal? c BASE64_PAD_BYTE)))
(define c3-pad? (pad? c3))
(define c4-pad? (pad? c4))
(define b1 (BASE64_TO_BYTE_V c1))
(define b2 (BASE64_TO_BYTE_V c2))
(define b3 (BASE64_TO_BYTE_V c3))
(define b4 (BASE64_TO_BYTE_V c4))
(unless (and (not (pad? c1))
(not (pad? c2))
(not (negative? b1))
(not (negative? b2))
(or (and (not c3-pad?) (not c4-pad?) (not (negative? b3)) (not (negative? b4)))
(and (not c3-pad?) c4-pad? (not (negative? b3)))
(and c3-pad? c4-pad?)
) ;or
) ;and
(value-error "Invalid base64 input")
) ;unless
(values (bitwise-ior (ash b1 2) (ash b2 -4))
(if c3-pad? 0 (bitwise-and (bitwise-ior (ash b2 4) (ash b3 -2)) 255))
(if c4-pad? 0 (bitwise-and (bitwise-ior (ash b3 6) b4) 255))
(if c3-pad? 1 (if c4-pad? 2 3))
) ;values
) ;define
(let ((output-N (quotient (* input-N 3) 4)))
(let ((output (make-bytevector output-N)))
(let loop
((i 0) (j 0))
(if (< i input-N)
(receive (r1 r2 r3 cnt)
(decode (bv i) (bv (+ i 1)) (bv (+ i 2)) (bv (+ i 3)))
(bytevector-u8-set! output j r1)
(when (>= cnt 2)
(bytevector-u8-set! output (+ j 1) r2)
) ;when
(when (>= cnt 3)
(bytevector-u8-set! output (+ j 2) r3)
) ;when
(loop (+ i 4) (+ j cnt))
) ;receive
(let ((final (make-bytevector j)))
(vector-copy! final 0 output 0 j)
final
) ;let
) ;if
) ;let
) ;if
) ;let
) ;let
) ;let*
) ;let
) ;define

(define string-base64-decode
Expand Down
150 changes: 150 additions & 0 deletions tests/liii/base64/bytevector-base64-decode-test.scm
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
(import (liii base64) (liii check))

(check-set-mode! 'report-failed)

;; bytevector-base64-decode
;; 将 Base64 编码的字节向量解码为原始字节向量。
;;
;; 语法
;; ----
;; (bytevector-base64-decode bv)
;;
;; 参数
;; ----
;; bv : bytevector
;; 要解码的 Base64 字节向量(长度必须是 4 的倍数)。
;;
;; 返回值
;; ----
;; bytevector
;; 解码后的原始字节序列。
;;
;; 注意
;; ----
;; - 空字节向量解码为空字节向量
;; - 支持 1~2 个 '=' 填充
;; - 输入字节可以是 0~255 任意值,结果字节向量保持原始字节,不做 UTF-8 转换
;;
;; 错误处理
;; ----
;; - 长度不是 4 的倍数 -> value-error
;; - 含非法字符(含空白、URL-safe 字符) -> value-error
;; - 填充位置错误 -> value-error
;; - 输入非 bytevector -> type-error

;; 空输入
(check (bytevector-base64-decode #u8()) => #u8())

;; 标准示例(与字符串入口一致)
(check (bytevector-base64-decode (string->utf8 "Zm9v")) => #u8(102 111 111))
(check (bytevector-base64-decode (string->utf8 "Zm9vYmFy"))
=>
#u8(102 111 111 98 97 114)
) ;check

;; 各种长度(1~6 字节)覆盖全部填充情况
(check (bytevector-base64-decode (string->utf8 "AA==")) => #u8(0))
(check (bytevector-base64-decode (string->utf8 "AQ==")) => #u8(1))
(check (bytevector-base64-decode (string->utf8 "/w==")) => #u8(255))
(check (bytevector-base64-decode (string->utf8 "AAA=")) => #u8(0 0))
(check (bytevector-base64-decode (string->utf8 "//8=")) => #u8(255 255))
(check (bytevector-base64-decode (string->utf8 "AAAA")) => #u8(0 0 0))
(check (bytevector-base64-decode (string->utf8 "AAAB")) => #u8(0 0 1))
(check (bytevector-base64-decode (string->utf8 "AAAAAA==")) => #u8(0 0 0 0))
(check (bytevector-base64-decode (string->utf8 "AAAAAAA=")) => #u8(0 0 0 0 0))
(check (bytevector-base64-decode (string->utf8 "AAAAAAAB")) => #u8(0 0 0 0 0 1))

;; 标准 Base64 字符集边界
(check (bytevector-base64-decode (string->utf8 "AAAA")) => #u8(0 0 0))
(check (bytevector-base64-decode (string->utf8 "////")) => #u8(255 255 255))
(check (bytevector-base64-decode (string->utf8 "+/3+")) => #u8(251 253 254))

;; 含 NUL 字节的解码(不应被截断)
(check (bytevector-base64-decode (string->utf8 "AAAA")) => #u8(0 0 0))

;; 较长输入
(check (bytevector-base64-decode (string->utf8 "VGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIHRoZSBsYXp5IGRvZy4=")
) ;bytevector-base64-decode
=>
#u8(84
104
101
32
113
117
105
99
107
32
98
114
111
119
110
32
102
111
120
32
106
117
109
112
115
32
111
118
101
114
32
116
104
101
32
108
97
122
121
32
100
111
103
46
) ;#
) ;check

;; Round-trip 一致性
(check (bytevector-base64-decode (bytevector-base64-encode #u8(1 2 3 4 5)))
=>
#u8(1 2 3 4 5)
) ;check
(check (bytevector-base64-decode (bytevector-base64-encode #u8())) => #u8())
(check (bytevector-base64-decode (bytevector-base64-encode #u8(255 0 127 128)))
=>
#u8(255 0 127 128)
) ;check

;; 非法输入:长度不是 4 的倍数
(check-catch 'value-error (bytevector-base64-decode (string->utf8 "Zg")))
(check-catch 'value-error (bytevector-base64-decode (string->utf8 "Zm9")))
(check-catch 'value-error (bytevector-base64-decode (string->utf8 "Zm9vYg")))
(check-catch 'value-error (bytevector-base64-decode (string->utf8 "Zm9vYg===")))

;; 非法字符(含空白、URL-safe 字符 '-' '_' 等)
(check-catch 'value-error (bytevector-base64-decode (string->utf8 "!m9v")))
(check-catch 'value-error (bytevector-base64-decode (string->utf8 "Zm9v@")))
(check-catch 'value-error (bytevector-base64-decode (string->utf8 "YWJ-")))
(check-catch 'value-error (bytevector-base64-decode (string->utf8 "YWJ_")))

;; 填充位置错误
(check-catch 'value-error (bytevector-base64-decode (string->utf8 "=Zm9")))
(check-catch 'value-error (bytevector-base64-decode (string->utf8 "Zg=v")))
(check-catch 'value-error (bytevector-base64-decode (string->utf8 "====")))
(check-catch 'value-error (bytevector-base64-decode (string->utf8 "Y===")))

;; 类型错误
(check-catch 'type-error (bytevector-base64-decode 123))
(check-catch 'type-error (bytevector-base64-decode "Zm9v"))
(check-catch 'type-error (bytevector-base64-decode 'sym))

(check-report)
Loading