Mini-Infer (22): 架构重构 — 链接器的魔法与“副作用”驱动的自动注册
Mini-Infer (22): 架构重构 — 链接器的魔法与“副作用”驱动的自动注册
1. 核心思想:从“拉 (Pull)”到“推 (Push)”
在此之前的架构中,我们采用的是 “显式触发” 模式:
- 代码逻辑:
Main函数 -> 调用Init()->Init调用RegisterConv(),RegisterRelu()… - 缺点:耦合度高。每增加一个算子,都要修改
Init函数。
现在,我们转向 “副作用驱动 (Side-Effect Driven)” 模式:
- 代码逻辑:
Conv.cpp定义一个静态全局变量 -> 程序启动 -> 加载Conv.o-> 触发静态变量构造函数 -> 构造函数执行Register()。 - 优点:高度解耦。
Main函数根本不需要知道Conv的存在,注册行为是加载库文件带来的“副作用”。
2. 隐形的大坑:静态链接的“死亡剔除” (Dead Code Stripping)
如果你直接在 .cpp 里写一个静态变量,然后把这个文件编译进静态库 (.a 或 .lib),你会发现:注册代码根本没有执行!
这是 C++ 链接器(Linker)的默认优化行为:
- 链接器发现
main函数没有引用FusionPass这个符号。 - 链接器判定
fusion_pass.o是“无用的死代码”。 - 链接器丢弃了整个
fusion_pass.o文件。 - 结果:里面的静态变量从未被初始化,注册逻辑从未执行。
3. 解决方案:CMake 接口目标与 --whole-archive
为了解决这个问题,我们需要强制链接器“吞下”所有的目标文件,无论它们是否被引用。
我们将修改 mini_infer/graph/CMakeLists.txt,引入一个特殊的接口目标 (Interface Target):mini_infer_graph_all。
CMake 实现解析
1 | # 1. 基础库:包含所有源文件 |
设计精髓:
- 传递性依赖:任何可执行文件(如
mini_infer_run)只需要链接mini_infer_graph_all,CMake 就会自动将--whole-archive标志传递给链接器。 - 平台兼容性:自动处理了 GCC/Clang (
-Wl,--whole-archive) 和 MSVC (/WHOLEARCHIVE) 的差异。
4. 代码实现:Pass 的自动注册
有了构建系统的支持,我们就可以在 C++ 中放心使用静态构造技巧了。
以 FusionPass 为例,我们在 mini_infer/graph/fusion_pass.cpp 的末尾添加:
1 | // mini_infer/graph/fusion_pass.cpp |
5. 全面重构:移除 KernelRegistryInitializer
既然我们解决了链接问题,是时候清理技术债务了。
我们不再需要 KernelRegistryInitializer 这个类,也不需要在 main 函数里调用 initialize()。
步骤 A:Kernel 的自动注册
在 gemm_cpu.cpp, im2col_cpu.cpp 等文件中,使用同样的 static struct 技巧:
1 | // mini_infer/kernels/cpu/gemm_cpu.cpp |
步骤 B:构建系统的统一
为 kernels 模块也应用同样的 CMake 模式:创建 mini_infer_kernels 和 mini_infer_kernels_all。
用户现在的 CMakeLists.txt 只需要这样写:
1 | add_executable(my_inference_app main.cpp) |
6. 总结
通过这次重构,我们完成了从代码级耦合到构建级解耦的转变。
- 开发者体验:编写新 Kernel 或 Pass 时,只需新建文件,写完注册代码即可,无需修改任何现有文件。
- 运行时零开销:注册过程发生在
main函数之前,运行时不需要反复检查is_initialized标志。 - 模块化:我们可以轻松地通过 CMake 决定链接哪些模块(例如,只链接 CPU Kernels 而不链接 CUDA Kernels),而不需要修改 C++ 源代码中的宏定义。
这就是成熟框架(如 TensorRT, PyTorch)背后的工程魔法。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 James的成长之路!
评论




