diff --git a/bench/base64-perf.scm b/bench/base64-perf.scm index f0ea0e9e..b0dda1e8 100644 --- a/bench/base64-perf.scm +++ b/bench/base64-perf.scm @@ -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) diff --git a/devel/0079.md b/devel/0079.md new file mode 100644 index 00000000..92dcf36c --- /dev/null +++ b/devel/0079.md @@ -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 实现做性能对比 diff --git a/goldfish/liii/base64.scm b/goldfish/liii/base64.scm index 74910d4c..543f7229 100644 --- a/goldfish/liii/base64.scm +++ b/goldfish/liii/base64.scm @@ -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 diff --git a/tests/liii/base64/bytevector-base64-decode-test.scm b/tests/liii/base64/bytevector-base64-decode-test.scm new file mode 100644 index 00000000..519dd758 --- /dev/null +++ b/tests/liii/base64/bytevector-base64-decode-test.scm @@ -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)