为什么 Rust 编译器这么慢?

这是一篇关于在 Docker 中构建 Rust 网站时遇到编译速度慢问题的详细记录及优化过程的文章,主要内容如下:

  • 问题描述:作者的网站主要由单个 Rust 二进制文件提供服务,每次修改都需要重新构建静态链接二进制文件、复制到服务器并重启网站,这很不理想,所以想改用容器部署。但快速的 Rust 构建与 Docker 并不简单。
  • 基础:Docker 中的 Rust

    • 简单方式:通常的方法是创建多个 Docker 阶段,先在构建阶段构建 Rust 程序,然后在最终阶段复制二进制文件。但这种方式每次有变化都会重新构建所有内容,耗时约 4 分钟。
    • 更好的缓存:Luca Palmieri 的cargo-chef工具可帮助预构建所有依赖项作为 Docker 构建缓存的单独层,只在代码库更改时重新编译代码库,而不是依赖项。但实际效果并不理想,大部分时间仍在最终二进制文件的构建上。
  • rustc的耗时分析

    • 使用cargo --timingsrustc的自我剖析功能-Zself-profile来获取更多信息,发现主要耗时在链接时优化(LTO)和LLVM_module_codegen_emit_obj上。
    • 调整 LTO 和调试设置(debugopt-level),发现禁用所有优化时编译最终二进制文件约需 50 秒,"fat" LTO 比完全禁用 LTO 耗时约 4 倍。
  • 深入探究 LLVM

    • 通过-Ztime-llvm-passes-Zllvm-time-trace获取 LLVM 的剖析信息,发现优化(OptFunction)和内联(InlinerPass)是耗时的两个部分。
    • 尝试调整 LLVM 的内联参数(如--inlinedefault-threshold等),发现降低内联阈值可使编译时间缩短,但难以确定最佳值。
    • 分析具体函数的优化时间,发现一些来自依赖项(如serde_jsontokio_postgres等)和core的函数优化时间较长,尤其是一些异步函数和闭包。
  • 优化尝试及结果

    • 对一些耗时的异步函数和闭包进行拆分、合并和使用Pin<Box<dyn Future>>等操作,可使编译时间有所减少。
    • 综合多种优化措施,如减少内联、拆分昂贵函数、移除依赖项的泛型等,最终将编译时间从约 175 秒减少到 9.1 秒,包括启用-Zshare-generics和切换为 debian 基础镜像等。
  • 总结与展望

    • 工具在解决问题时效果良好,文档也足够帮助相对缺乏经验的人改进代码库。
    • 指出一些仍需解决的问题,如异步函数调用图的编译时间、rustccore::ptr::drop_in_place的处理等,并提出可能的改进方向。
    • 建议在某些情况下设置opt-level = 0可能就足够了。
阅读 17
0 条评论