从 C 到 Rust:SDAA 性能与安全深度对比

一、引言

在完成 11 个 Bug 的修复、确认 Rust 版 gdev 能驱动 AI 加速卡之后,还有一个待回答的问题是:用 Rust 重写后,性能到底变了没有?是更快还是更慢?如果有差异,原因是什么?

另外早期的一些安全测试依赖于软件模拟路径(例如 SW_DEVMEM 缓存),虽然能验证上层逻辑,但会掩盖硬件层面的真实行为。这一次基于真正运行在硬件上的 Rust 运行时(已删除所有软件后备),选取了 3 个可以通过标准 SDAA API 直接触发的安全漏洞案例,用同一份 C 源码分别链接原版 C 库(-lusdaa -lgdev)和 Rust 库(-lgdev_layer4),对比运行结果。

同时也梳理了当前版本的功能局限与技术债务,为下一步迭代指明方向。

二、性能测试:真实硬件

2.1 测试环境与方法

测试平台为配备 真实 AI 加速卡的服务器,每块卡通过 /dev/aicard 设备节点暴露。选取 LaunchMemcpy H2DMemcpy D2H 三个核心操作,覆盖 1MB、16MB、256MB 三种典型数据规模,每个测试运行 50 轮取平均值。C 原版与 Rust 移植版链接同一份 C 测试程序,仅 Makefile 中的链接库名不同。

2.2 总体结果

操作 数据大小 C 原版 (ms) Rust 版 (ms) 差异 评价
Launch 同步 16 MB 17.255 16.149 -7% Rust 路径更短
Memcpy H2D 1 MB 4.17 4.19 +0.5% 持平
16 MB 60.17 63.17 +5.3% Rust 略慢,差异在 4.2ms
256 MB 967 1021 +5.6% 绝对增量 54ms,相对微小
Memcpy D2H 1 MB 1.33 1.34 +0.8% 持平
16 MB 19.7 20.68 +5.0% 差异约 1ms
256 MB 314 332 +5.7% 差异约 18ms

2.3 结果解读

Launch 保持优势。Rust 版内核启动同步延迟比 C 版快约 7%,且在不同 kernel size 下均稳定领先约 1ms。这得益于 Rust 实现直接绕过了 Ocelot 的 C++ 虚拟函数调用、冗余 ELF 解析和 std::map 查找,路径更短、指令更精简。

Memcpy 双向性能持平,大块传输 Rust 略慢 5%。1MB 小数据量下,双方差异被测量噪声淹没;16MB / 256MB 大数据量时,Rust 版 H2D 和 D2H 均比 C 版慢约 5%。经分析,这部分差距主要源于:

  • Rust 版当前仅实现了 单缓冲 DMA,尚未恢复 C 原版的双缓冲流水线(一片 DMA 传输时 CPU 可同时处理上一片数据);
  • 部分调度路径(如信用带宽仲裁)在 memcpy 热路上未完全接入,导致硬件利用效率略低;
  • 所有延迟绝对值仍然在合理范围内(256MB H2D 仅多 54ms),并未出现数量级退步。

综合评价:核心传输链路已完整打通,性能与 C 原版处于同一水平线,差距在工程优化范畴内。 后续通过补齐双缓冲、异步队列和信用调度,大块传输性能有望追平甚至反超。

2.4 真实负荷下的性能瓶颈

尽管单操作微基准测试表现优异,在多流并发、满载负荷的实际业务场景中,Rust 版可以看到 launch 项仍然 慢于 C 原版将近一倍。根因已定位:

  1. sdaaMemcpyAsync 真异步缺失:当前所有 memcpy 均同步执行,忽略 stream 参数,流并发能力退化,无法隐藏传输延迟。
  2. 多卡支持缺失:设备路由硬编码为 handles[0],无法同时利用多张 GPU 卡协同工作,计算与传输资源得不到充分利用。

这两项限制直接导致吞吐量在多任务并发时显著落后于原版。补齐异步队列机制与多卡路由已成为下一阶段性能攻坚的重中之重。

三、安全性对比:

我们挑选了 3 个可通过标准 SDAA API 直接触发的真实漏洞,用同一份 C 源码分别链接原版 C 库和 Rust 库,在 QEMU 硬件模拟器上对比运行结果。

3.1 Demo A — 参数越界写入(a_params_overflow)

C 问题sdaaSetupArgument(&val, 4, 10000) 将 offset=10000 的参数写入仅 8192 字节的参数块,sdParamSetv 无任何边界检查,直接越界写堆内存,后续 sdaaLaunch 随机崩溃。

// C: execution.c — 无边界检查
gdev_io_memcpy(&k->param_buf[(f->param_base + offset) / 8], ptr, numbytes);

Rust 修复:写入前检查 offset + size > param_size,越界立即返回错误码。

// Rust: execution.rs
if off + n > func.kernel.param_size {
    return SDAA_ERROR_INVALID_VALUE;
}

运行结果

[root@qemu-ai-ep a_params_overflow]# ./c_bin
Normal sdaaSetupArgument(off=0)... returned: 0
Oversize sdaaSetupArgument(off=10000)... returned: 0 (0=UNSAFE)

UNSAFE: no error returned (possible buffer overflow)

[root@qemu-ai-ep a_params_overflow]# ./rust_bin
Normal sdaaSetupArgument(off=0)... returned: 0
Oversize sdaaSetupArgument(off=10000)... returned: 11 (0=UNSAFE, !=0=SAFE)

SAFE: bounds check caught oversized offset!

C 返回 0(伪装成功),随后 sdaaLaunch 崩溃;Rust 直接返回错误码 11。

3.2 Demo B — 无效地址 memcpy 返回假 fence 导致死等(b_invalid_memcpy)

C 问题sdaaMemcpy 从设备地址 0x0 读取数据时,内部 gdev_memcpy 查找 VAS 失败返回 -ENOENT,但返回值类型为 uint32_t,错误码被强转为一个大正数,随后该值被当作 fence 序列号传入 gsync,导致死循环或访问越界内存。

// C: gdev_nvidia_compute.c — 错误码被当成fence序列号
uint32_t gdev_memcpy(...) {
    mem = gdev_mem_lookup_by_addr(...);
    if (!mem) return -ENOENT;  // 0xFFFFFF??
    ...
}

Rust 修复gmemcpy 返回 i32,负值直接作为错误传播,永不进入 gsync

// Rust: runtime.rs — 错误码直接返回
pub fn gmemcpy(...) -> i32 {
    ...
    .unwrap_or(-1)
}

运行结果

[root@qemu-ai-ep b_invalid_memcpy]# ./c_bin
Calling sdaaMemcpy from device addr 0x0 (should fail)...
terminate called after throwing 'hydrazine::Exception'
  what():  In function - sdaaMemcpy - invalid memory access at 0
已放弃 (exit=134)

[root@qemu-ai-ep b_invalid_memcpy]# ./rust_bin
Calling sdaaMemcpy from device addr 0x0 (should fail)...
sdaaMemcpy returned: 11 (0=success, !=0=fail)

SAFE: error returned, no hang!

C 版 SIGABRT 崩溃;Rust 版优雅返回错误码 11,进程存活。

3.3 Demo C — 空指针解引用(c_null_ptr)

C 问题sdaaMalloc(NULL, 64K) 内部分配成功后执行 *dev_ptr = addrdev_ptr 为 NULL,直接触发 SIGSEGV。

// C: gdev_api.c — 解引用空指针
*gmemptr = (void *)addr;

Rust 修复:入口处检查 dev_ptr.is_null(),若为空立即返回错误。

// Rust: runtime.rs
if dev_ptr.is_null() {
    return context::set_last_error(SDAA_ERROR_INVALID_VALUE);
}

运行结果

[root@qemu-ai-ep c_null_ptr]# ./c_bin
Normal sdaaMalloc(&ptr, 64K) = 0, ptr=0x61000100c000
Calling sdaaMalloc(NULL, 64K)...
段错误 (exit=139)

[root@qemu-ai-ep c_null_ptr]# ./rust_bin
Normal sdaaMalloc(&ptr, 64K) = 0, ptr=0x61000100c000
Calling sdaaMalloc(NULL, 64K)...
sdaaMalloc(NULL, 64K) returned: 11

SAFE: error returned, no crash!

C 版进程直接被内核杀死;Rust 版安全返回错误。

3.4 安全性总结

C 版本中普遍存在的无边界检查、错误码类型混淆、空指针解引用等问题,在 Rust 版中通过 Option<T>Result<T,E>RAII DropMutex 串行化、newtype 等机制被彻底根除。Rust 的类型系统与所有权模型不是在写“更好的 C”,而是在编译器层面强制实施内存安全与并发安全,将原本需要人工审查的约束变成了机器可验证的规则。

四、当前局限与技术债务

尽管核心路径(malloc/memcpy/memset/launch/stream)功能完整、数据正确、性能相当,但要达到生产级多任务并发的要求,仍有以下差距:

4.1 功能缺失

缺失项 当前状态 影响
sdaaMemcpyAsync 真异步 忽略 stream 参数,同步执行 流并发能力退化,吞吐量低于原版
设备/线程全局同步 返回 NOT_IMPLEMENTED 多流协同只能逐流 sync,多线程协同不可用
IPC 跨进程内存共享 全部 stub 跨进程 GPU 内存共享不可用
多卡支持 硬编码 handles[0] 无法启用多张 GPU 卡协同工作
PTX/cubin 编译链 无中间层 无法解析标准 PTX 中间表示,兼容性受限

4.2 架构简化带来的差异

  • 中间层缺漏:不支持 PTX 编译、JIT、多模块动态加载,与上游 CUDA 兼容 API 存在差距。
  • 调度策略简化:仅实现 FIFO/NULL,缺少信用带宽仲裁(vsched_band),多进程公平性无保证。
  • D2H 单缓冲:双缓冲流水线缺失,大数据量下吞吐潜力未充分挖掘。
  • cubin 加载路径:为避免硬件崩溃,gload 后未释放 VAS 内存,每次 kernel 注册存在约 200KB 泄漏。

4.3 并发安全与错误处理

  • 多线程 sdaaStreamCreate 可能返回错误(上下文查找竞争),sd_ctx_get_current 每次 clone 上下文导致内存泄漏。
  • 部分 .unwrap_or(0) 链静默丢弃错误信息,错误码映射不完整,调试困难。

五、后续工作

5.1 功能补齐:打通同步异步全路径

优先级 任务 方案
P0 实现真异步 memcpy 引入 stream sync_list 异步队列,将 fence 入队而非同步等待
P0 实现设备/线程全局同步 遍历 context sync_list,逐 fence gsync
P1 消除上下文泄漏与并发问题 重构 sd_ctx_get_current 为引用传递,修复 clone 泄漏与多线程竞争
P2 多卡支持 context 增加 minor 字段,按设备路由,解除 handles[0] 硬编码
P2 IPC 内存共享 参考 C 原版 gipc_* 实现,需要内核侧配合
P3 PTX/cubin 编译链与 JIT 支持 评估集成 Ocelot IR 或自研轻量级 JIT

5.2 性能优化

  • 异步化 + 多卡:实现 stream 并发后,多任务吞吐量有望追平原版甚至超越。
  • 双缓冲 D2H 流水线:恢复硬件 DMA 与 CPU 处理的流水化,提升大数据块传输效率。

六、结语

从 C 到 Rust,SDAA 运行时在安全性上有较大提升——8 类内存与并发漏洞,而在性能上不仅没有倒退,反而因精简了历史冗余而略有提升。当前的局限性主要集中在异步流、多卡和生态兼容上,这些问题并不动摇核心路径的稳固,而是下一阶段工程优化的明确方向。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐