吞错误(fallback / silent failure / 返回假值)会导致查找一个很小的 ULP 问题耗费无穷漫长的时间和人力。
规则:
-
外部符号解析失败 → 立刻
throw std::runtime_error,进程退出。 不返回 nullptr,不 fallback 到纯 C++ 循环,不返回假值。 -
任何系统级前提条件不满足(动态库未加载、ABI 不匹配、 符号缺失)→ 立刻 throw,附带清晰的错误消息说明原因和修复方法。
-
禁止 silent failure。 一个静默错误浪费的排查时间远超一个清晰的 crash + stack trace。
-
数学意义上的失败(如奇异矩阵)也要 throw,不要 return false/zero。 调用方如果期望处理奇异矩阵,应当显式 catch。
核心原则:C++ 绑定的接口和实现必须与目标 Python 库完全一致。 测试代码是"黄金标准"——它反映 Python 库的真实行为。禁止通过修改测试来绕过 C++ 实现缺陷。
- 错误: C++ 绑定擅自调换参数顺序(如把数组放在 mask 前面)
- 正确: 严格匹配目标库的参数位置
- 实例:
compress(arr, mask)→compress(condition, a)
- 错误: 用
-1表示"未传入参数"——目标库用None表示 - 正确: 使用
py::none()匹配目标库的None语义 - 实例:
eye(N, M=-1, k=0)→eye(N, M=py::none(), k=0) - 规则: 目标库用什么哨兵,C++ 就用什么。不要发明自己的。
- 禁止:
std::pow()/std::atan2()/std::exp()等 libm 函数直接调用 - 原因: 目标库使用的 Intel SVML / 自定义多项式实现与 libm 差 1-3 ULP
- 正确: 通过
dlsym从目标库的 .so 中解析它实际使用的数学符号,在 C++ 侧调用
- 错误:
dlsym("npy_atan2")→ 解析到 libm 的标量atan2(差 1 ULP) - 正确:
dlsym("__svml_atan28")→ 解析到目标库实际使用的向量实现 - 方法: 用
nm -D <目标库.so> | grep <函数名>查看目标库实际导出了什么符号, 选择与目标库运行时路径一致的版本
内部实现头文件(如数学后端桥接)必须在所有依赖它的模块头文件之前加载。
否则深层模块无法使用桥接符号,可能静默 fallback 到 std::。
当发现对齐测试失败时:
- 诊断: 用最小复现脚本确认差异量级(ULP)
- 定位根因: 追溯 C++ 调用链,找出哪一步使用了非等价的实现(参数错误、
std::函数、错误的 dlsym 符号等) - 修复 C++ 源码: 修改实现使其严格匹配目标库的等效路径
- 严禁改测试: 测试反映目标库的真实行为。修改测试来绕过 C++ 缺陷等同于作弊
- 在请求新增 API 前,先确认 目标库或 pybind11 是否已有等价功能
- 示例:
py::isinstance<py::array_t<float>>(arr)可直接判断 dtype,不需要自己写is_float32() - 示例:
astype(arr, "float64")已实现类型转换,不需要写ensure_float64()
关键 flag:
-O2 -ffp-contract=off— 禁用 FMA 融合,确保每一步计算独立-fno-builtin-{exp,log,sin,cos,tan,pow,atan2,sqrt,...}— 阻止编译器用内置实现替换 dlsym 路径- 无
-march=native— 避免编译器自动矢量化改变计算顺序 - 通过
dlsym从目标库的 .so 解析数学符号
-O3 -march=native— 允许编译器自由优化- 无
-fno-builtin-*— 使用编译器内置数学函数 - 与目标库预期差 0-2 ULP(可接受范围)