优化冒险:通过面向数据的设计(和其他技巧)使并行 Rust 工作负载更快 | 博客 | Guillaume Endignoux

这是关于在多线程上优化 Rust 工作负载的第二篇文章。

  • 优化内容总结

    • 内联(Inlining):添加#[inline(always)]使一些小函数内联,运行时减少约 30%,如定点算术的新类型函数。
    • 编写更好的算术(Writing better arithmetic):乘法运算中,先使用checked_mul()判断乘积是否适合i64,再决定是否使用i128,运行时又减少 30%;优化BigInt后端的乘法实现,直接使用BigInt的除法,运行时减少七倍。
    • 优化BigInt(Optimizing BigInt:使用smallvec优化BigInt的内存布局,运行时减少 12% - 25%,多线程时相对增益更大。
    • 面向数据的设计(Data-oriented design):通过使用u8索引、smallvec技术、Box<[_]>等优化选票数据结构,使运行时在最佳场景下减少 20%;尝试位打包虽减少数据缓存缺失,但使程序运行变慢 30%,且代码重构复杂。
    • 排序和克隆数据(Sorting and cloning data):排序输入文件可减少分支缺失,使指令执行更高效,但会增加数据缓存缺失;克隆排序后的数据可使顺序读取更快,运行时减少 20%,但依赖分配器的隐式行为,生产中可使用自定义分配工具。
    • 优化BigRational(Optimizing BigRational:根据数学公式,将BigRational的分母表示为质因数分解,进行部分 GCD/LCM 简化,最终全简化,使程序运行速度提高 14 - 18 倍,结合BigIntsmallvec优化,整体提高 15 - 20 倍。
    • 通用优化(Generic optimizations)

      • 日志过滤(Log filtering):使用log crate 的max_level_debug功能和log_enabled!宏,可减少运行时 5% - 10%,某些情况下可减少 40%。
      • 禁用算术溢出检查(Disabling arithmetic overflow checks):在热循环中禁用算术溢出检查,运行时减少 20%。
      • 恐慌 = 中止(Panic = abort):将panic!的行为改为中止程序,在使用大整数算术时运行时减少 15% - 20%,其他情况结果不一。
      • codegen-units = 1:将每个 crate 的编译单元设置为 1,可使运行时在某些场景下提高 15%。
      • LTO(Link-time optimization):启用 LTO 可使运行时在某些场景下提高 5%。
      • PGO(Profile-guided optimization):尝试 PGO 未观察到改进,可能是测试时间短或 PGO 不是万能的。
      • 目标本机 CPU(Targetting the native CPU):使用target-cpu=native选项未使运行时有明显变化。
      • 切换分配器(Switching allocators):使用 jemalloc 未使运行时有明显变化,可能是程序的分配模式对默认分配器友好。
  • 结果总结:最终选择的并行框架对性能不是最重要的,最大的改进来自算法(以更好的方式编写算术)、内存表示(smallvec、面向数据的设计)或通用优化(如强制函数内联)。更新的基准测试表明,单线程性能最佳,Rayon 在壁时间和用户时间上比自定义并行ism 慢 20%和两倍,使用 Rayon 时系统时间开销随线程数增加而显著增加,使用自定义并行ism 时系统时间开销稳定且最小。

总体而言,优化 Rust 程序需要综合考虑多个方面,以获得最佳性能。

阅读 8
0 条评论