ssea 算法详解(中文)
语言切换: English Version | README (EN) | README (CN)
本文档覆盖 ppp/cryptography/ssea 中的每一个 算法(在本库中对齐镜像):
标量实现、SIMD 实现(或"为何不适用 SIMD"的形式化论证)、所选择方法的可行性证明、
实测基准、以及边界与错误语义。
所有非标量路径均已验证与标量参考在每个输入长度 1...100000 上逐字节一致
(见 tests/ 测试套件)。
1. shuffle_data / unshuffle_data
算法
for i in [0, size): j = (i ^ key) % size; swap(data[i], data[j])
unshuffle_data 以逆序 运行同一循环;swap 序列是自身的逆
(每个 swap 是对合,逆序运行即还原组合)。
标量实现
shuffle_data_scalar / unshuffle_data_scalar --- 1:1 移植。
SIMD 可行性论证(不适用)
- 目标索引
j = (i ^ key) % size依赖 i 的 32 位模除(运行时模数) ---
不存在闭式向量公式。 - 每次 swap 访问两个数据依赖的随机内存位置 。SIMD 面向连续流;
随机置换受内存延迟约束,任何 SIMD gather/scatter 原语仍按元素
逐一加载/存储 --- 相对标量 swap 无吞吐收益。 - 复杂度 O(n) + 随机访存,无可开发的数据级并行。
结论:标量是正确选择,SIMD 无法帮助。
优化(标量)
2 的幂 size:j = (i ^ key) & (size - 1) 以 AND 取代 32 位 div
(20-30 周期)。2^k 尺寸实测 1.8-2.9x。
| 尺寸 | 标量 | 优化 | 加速 |
|---|---|---|---|
| 256 | 839 MB/s | 1505 MB/s | 1.79x |
| 4096 | 853 MB/s | 2468 MB/s | 2.89x |
| 65535(非 2 幂) | 789 MB/s | 834 MB/s | 1.06x |
2. delta_encode(SSE2 约 8x,AVX2 约 10x)
算法
out[0] = in[0] - kf; out[i] = in[i] - in[i-1] (mod 256)
每个输出字节只依赖当前与前一个输入字节 --- 数据并行模式。
SSE2 实现(16 字节/轮,约 6 条指令)
a = loadu(in + i) // [in[i]..in[i+15]]
prev = pslldq(a, 1) | prev_last // [in[i-1], in[i]..in[i+14]]
out = psubb(a, prev) // 模 256 减法
prev_last = srli_si128(a, 15) // 下一块需要的 in[i+15]
prev_last 首块仅保留 kf 的低字节 (pand 0xFF ---
cvtsi32_si128 写入 4 字节;负值/大 kf 时掩码是必须的)。
证明
pslldq(a,1)[j] = a[j-1],故 prev[j] = in[i+j-1];psubb 为模 256 减法,
与标量 Byte 运算一致。首块以 in[-1] ≡ kf (mod 256) 匹配
out[0] = in[0] - kf。
边界语义
空/空指针 → 返回 0。尾(< 16 字节)标量处理,prev_byte = in[i-1]
由主循环延续。
3. delta_decode(SSE2 约 8x,AVX2 约 8.7x)
算法
out[0] = in[0] + kf; out[i] = out[i-1] + in[i] (mod 256)
这是前缀和 --- 串行依赖链,但可并行化。
SSE2 实现(Hillis-Steele 扫描,16 lane)
p = a
p = p + pslldq(p, 1) // 跨度 1
p = p + pslldq(p, 2) // 跨度 3
p = p + pslldq(p, 4) // 跨度 7
p = p + pslldq(p, 8) // 跨度 15 -> p[j] = sum(in[0..j])
out = p + carry // carry = 上一块末字节
carry = out[15] // srli_si128(15) + cvtsi128_si32 提取
方向: 扫描需要"前一个"字节(低地址方向),必须用 pslldq(slli)。
用 psrldq 会读到"下一个"字节,产生右向和 --- 错误的前缀。
证明
模加法满足结合律:块内前缀 p[j] = Σ in[0..j] 以对数深度
(4 轮,跨度 1+2+4+8=15)精确计算(模 256),跨块进位
out[15] 精确链接各块,与标量递推一致。
边界语义
同 encode:空/空指针 → 0;尾标量,acc 延续。
4. base94_encode(SSSE3+ pshufb:1.4-4.4x)
算法
b = (byte - kf) & 0xFF
b < 93 : 输出 1 字符 0x20 + b
b >= 93 : 输出 2 字符 0x20 + (b/93 + 92), 0x20 + (b%93)
说明(免除法 SIMD 形式)
-
因
b ∈ [93,255],b/93 ∈ {1,2}--- 逃逸首字符恒为'}'(0x7D) 或
'~'(0x7E):hi = 0x7D + (b >= 186) lo = b - 93*(b>=93) - 93*(b>=186) + 0x20 // == b%93 + 0x20 -
每字节输出顺序为
[hi, lo](首字符在前,余数在后)。 -
每字节输出长度
L[i] = 1 + (b>=93)使输出位置数据依赖 →
变长展开。
为何需要 SSSE3 pshufb(可行性证明)
展开是字节 gather:out[pos[i]] = lo[i],pos[i] 为 L 的前缀和。
纯 SSE2 没有字节级任意重排指令 (pshufb 是 SSSE3 引入);替代方案
可证明更差:
- 逐孔移位链:O(孔数) 次
psrldq + mask + or≈ 24+ ops/16B 且掩码
数据依赖 --- 比标量还差; - SWAR 乘法压缩:~20+ ops/16B + 掩码相关的魔术常数 --- 复杂且更慢。
选择: 以 8 位展开掩码索引预计算 256-entry 表 + 一条pshufb
为本库采用的方法(计算核保持 SSE2)。
实现(8 输入字节/轮)
b8 = loadl(in+i); b8 = psubb(b8, kf) // b = byte - kf
biased = pxor(b8, 0x80) // 无符号比较偏置
L1 = pcmpgtb(biased, 92^0x80) // b >= 93
L2 = pcmpgtb(biased, 185^0x80) // b >= 186
lo = b8 - (L1&93) - (L2&93) + 0x20 // b%93 + 0x20
hi = 0x7D + (L2&1) // '}' 或 '~'
ex = punpcklbw(hi, lo) // [hi0,lo0,hi1,lo1,...]
mask = movemask(L1) & 0xFF // 位 i = 第 i 对为 2 字符
out16 = pshufb(ex, ENCODE_TABLE[mask]) // gather
store 16 字节; op += 8 + popcount(mask)
ENCODE_TABLE[m] = 每对的源索引:2 字符对取 [hi, lo],1 字符对取 [lo]
(其余为垃圾,按长度计数截断)。
边界语义
两遍(先长度后写入)保持缓存友好;尾 < 8 字节标量。
空/空指针 → 错误语义(0/nullptr)。
5. base94_decode(SSSE3+ pshufb:1.3-3.8x)
算法
ch >= 0x20, offset = ch - 0x20 <= 94 (否则报错)
offset >= 93 -> 逃逸对: v = (offset-92)*93 + next_offset (校验)
输出字节 = v + kf
说明
- 逃逸起始 iff
ch >= 0x7D;配对值v = 93 + 93*(ch>=0x7E) + next - 0x20。 - 每个逃逸对只删除其续字符 ;输出位置 =
i - (之前的逃逸数)---
与 encode 镜像的变长压缩(同一 256-entry 表,索引用2*i匹配
punpcklbw(v,v)重复布局)。 - 校验(字符 < 0x20、> 0x7E、续字符偏移 > 93、
'~'+ 续偏移 > 69 →
v > 255)以偏置字节pcmpgtb向量化;任一违规回退标量参考 ,
错误语义精确保留。
跨块处理
位置 7 的逃逸消费字节 8(下一块首字节)。删除掩码进位:
del = (esc << 1) | carry;标量尾从 i + carry 开始跳过被消费字节。
边界语义
输入 < 16 字节 → 直接标量参考(SIMD 开销大于收益)。截断逃逸、
字母表外字符、值溢出均与标量一致地返回失败。
6. base94_decimal(整数 <-> Base94 字符串)
算法
uint64 -> 字符串:反复/94与%94(≤ 11 位,反转)。字符串/字节 -> uint64:n = n*94 + 数字链。
SIMD 可行性论证(不适用)
- 串行依赖: 每一位依赖上一位的余数/累加值 --- 无并行形式。
- SSE2 无 64 位整数除法 (
%94所需)。 - 数据极小: 全部输入/输出仅 8...11 字节;SIMD 初始化
(加载/广播/查表)超过总工作量。 - 无批量场景: 每包头部一次转换,无法摊薄。
结论: 标量是唯一合理选择。已用边界值(0、1、93、94、94²±1、
UINT64_MAX)、20 万次随机往返、错误路径验证。
7. random_next / lcgmod(PRNG)
算法
三步 LCG L(x) = 1103515245*x + 12345 (mod 2^32) 的 16 位折叠组合:
result = ((t1>>16)&0x7FF)<<20 ^ ((t2>>16)&0x3FF)<<10 ^ ((t3>>16)&0x3FF)
SIMD 可行性论证(不适用)
- 单次调用是单 seed 的 3 步串行折叠 --- 调用内无可并行内容。
- 批量场景原则上存在(并行 LCG 跳步),但见 §8:生产调用模式
kf = random_next(&kf)以返回值覆盖 seed,序列成为非线性 fold 链 ,
跳步不适用。
结论: 标量;批量层面的分析属于 §8。
8. masked_xor / masked_xor_random_next
masked_xor(定键)
对每个 32 位字异或常量 kf(尾:16/8 位)。
- SSE2:16 字节一次
pxor(指令数约降 16 倍)。 - 实测 0.65-0.96x --- MSVC /O2 已将标量循环自动向量化到同宽度,
达到内存带宽(约 70 GB/s)。 手动 SSE2 版本保留作可移植性参考,
但 MSVC 下推荐标量。结论:编译器已覆盖,保持标量。
masked_xor_random_next(每字 LCG 密钥流)
对每个 32 位字: kf = random_next(&kf); word ^= kf
尾 (16/8 位): word ^= kf (不再更新)
密钥流语义: random_next 将推进后的 seed 写入 *seed 并返回
折叠值 ,随后该值被赋回 kf --- 覆盖了 seed 。因此密钥流是
k_{n+1} = fold(k_n) --- 非线性 fold 链 (fold 含移位/XOR)。
LCG 跳步(L3 幂)可证明无法重现此序列。故密钥流严格串行,无法并行。
实现: 标量密钥流生成 + SSE2 批量 XOR(每轮 4 字)。实测约 1.0x ---
密钥流占主导,XOR 批处理增益有限。
结论:可接受;对该精确语义,并行密钥流数学上不可能。
边界语义
零长度 → true;负长度 → false。尾键使用(末字后不更新)与标量完全一致;
已用 1...100000 全长度 + 多 kf 验证。
汇总表
| # | 算法 | 标量 | SIMD | 实测 | 选择的方法 |
|---|---|---|---|---|---|
| 1 | shuffle/unshuffle | ref | 不适用(已论证) | 2 幂 1.8-2.9x(AND) | 标量 + 2 幂 AND |
| 2 | delta_encode | ref | SSE2 及以上(AVX2 最优) | 约 8x,AVX2 10x | SSE2+ |
| 3 | delta_decode | ref | SSE2 及以上(AVX2 最优) | 约 8x | SSE2+ |
| 4 | base94_encode | ref/LUT | SSSE3 及以上(AVX2 最优) | 1.4-4.4x | SSSE3+ |
| 5 | base94_decode | ref | SSSE3 及以上(SSE4.1 最优) | 1.3-3.8x | SSSE3+ |
| 6 | base94_decimal | ref | 不适用(已论证) | --- | 标量 |
| 7 | random_next/lcgmod | ref | 不适用(已论证) | --- | 标量 |
| 8 | masked_xor | ref | AVX2/标量 | AVX2 2x;标量优于 128 位 | AVX2 否则标量 |
| 9 | masked_xor_random_next | ref | 密钥流标量 + XOR SSE2 | 约 1.0x | 混合 |
正确性保证: 每条 SIMD 路径与标量参考在全部长度 1...100000、边界组合、
错误路径、canary(越界)检测、未对齐偏移、多 kf/key 下逐字节一致 ---
见 tests/ 测试套件。