You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何为自定义类型解锁-ffast-math并提升代码代数运算效率?

问题

我该如何提升自定义实现类型代码的代数运算效率?

是否存在可继承的虚拟空类型,能告知编译器该类型属于fast-math范畴?或是有现成的编译期类型可提供所需的全部表达式模板?

前置说明:任何关于编译期和运行期的假设均不成立,整个流程分为两个连续的编译与执行阶段。

  • 第一阶段是编译并运行initial_source.cpp,该程序运行后会生成generated_program.cpp文件,无论使用何种编译选项,生成的文件内容均不会改变。
  • 接下来编译并运行generated_program.cpp,我关注的是这最后一步的运行性能。实践表明,当initial_source.cpp的代数表达式更简洁时,生成的generated_program.cpp会更短、运行速度更快。
  • 面向数学爱好者:任何在代数上等价但形式不同的initial_source.cpp文件,会生成在代数上等价但形式不同的generated_program.cpp文件(忽略舍入误差)。

因此,我希望告知编译器的并非:

  • 编译运行initial_source.cpp时处理字符串和其他基础类型存在索引偏移,请对这部分进行优化。

而是:

  • 你看到这些包含未知数字类型的代数表达式了吗?请将它们当作double类型处理,重新整理为最简洁的表达式,之后再以任意方式编译代码即可。
背景

我有一个可被当作double或float等类型使用的自定义类型Number。

对于任意函数:

template<typename Tfloat>
void f(Tfloat* y, Tfloat* x){ ... }

我可以传入自定义类型来调用它,并由此生成其他代码(比如伴随代码、切线代码、邻接矩阵、图等)。我希望生成的代码易于求值,但其长度和效率极大地取决于f内部的细节。

例如,若f中有如下代码:

auto z1 =  (x-(-y));
auto z2 = -(x-(-y));
auto z3 = 1+x+2+3+4;
auto z4 = cos(-x);
auto z5 = -sin(-x);

显然,上述代码的效率远低于以下版本:

auto z1 =  x+y;
auto z2 = -z1;
auto z3 = x+(1+2+3+4);
auto z4 = cos(x);
auto z5 = sin(x);

第一个版本生成的伴随代码长度可能翻倍,所需内存也翻倍,最终运行速度可能比第二个版本慢10倍。

然而,编译器自然不会修改涉及自定义Number类型的代数表达式和赋值操作,因为它无法确定该类型的加法是否满足交换律。

我知道如何实现表达式模板,但这需要大量工作。既然编译器在处理标准类型时擅长这类优化,我绝不愿花时间去实现一个性能更差的版本,更何况代数优化只是我项目任务中的一个小细节。

补充说明

聊胜于无

我明白程序的最优代数形式取决于Number的实现细节,但我们完全可以假设其各操作的相对成本与double一致(即加法远快于乘法,除非使用FMA;无符号切换比有符号切换成本更低)。如果这涉及太多问题,核心需求是:我需要一个比当前方案更好的方法,且无需重复造轮子(即无需为符号切换、求和与乘积顺序实现表达式模板)。

编译器确实不会优化自定义类型的代数表达式(这正是问题的核心)

由于许多代数对象不满足交换律(比如矩阵乘法),即使x是像矩阵一样复杂的类型,我们只是想为每个元素添加常数,编译器也不会优化z3的表达式。同样,编译器不会将x-(-y)替换为x+y,因为开发者完全可以将一元负号运算符实现为非自反运算符。

理论上,全知的编译器可以将生成的代码(所有类型均为double)优化到与代数优化后的初始代码生成的代码相同的水平,毕竟这两种生成代码在代数上是等价的(忽略舍入误差)。但现实中,编译器从代数形式糟糕的初始代码生成的代码运行速度要慢得多。

澄清对话中的误解

部分读者认为编译器最终会将两种生成代码优化到同一水平。我们来拆解这个问题:假设Number类型每次执行操作时都会在控制台打印不同的字符串,那么无论使用何种优化选项,编译器都不会改变控制台输出。因此,使用该自定义类型运行初始代码生成的代码、文本文件或伴随代码,内容都不会因优化选项而改变,这凸显了z3存在的问题。

再来看伴随代码:每个操作都会在伴随记录(tape)中至少存储一个数值,后续会按记录的逆序进行导数计算。不难想象,这类代码可能长达数十万行。无论编译器多么优秀,都无法识别出在数十万行非线性自动生成代码中,两个相隔甚远的操作及其对应的记录元素可通过代数等价性移除。编译器仅会进行局部代码分析。理论上,所有生成的汇编代码都是等价的,在最优优化下会完全相同(若最优解唯一),但我需要的是现实可行的解决方案。

非等价性示例

假设存在如下代码:

void f(Tfloat& y, Tfloat& x){ y = 1+x+2+3+4; }

它会生成如下C++伴随代码文件:

void gf(Tfloat& y, Tfloat& x, Tfloat& gy, Tfloat& gx){
auto tmp1 = 1+x;
auto tmp2 = tmp1+1;
auto tmp3 = tmp2+2;
y         = tmp3+3;
auto gtmp3+= gy;
auto gtmp2+= gtmp3;
auto gtmp1+= gtmp2;
gx += gtmp1;
}

相比之下,代数优化后的初始代码会生成如下截然不同的伴随记录:

void gf(Tfloat& y, Tfloat& x, Tfloat& gy, Tfloat& gx){
y = x+(1+2+3+4);
gx += gy;
}

诚然,理想的编译器可以从这两段生成代码中得到相同的优化结果,但在现实场景下,针对超大型文件时,这根本无法实现。

内容的提问来源于stack exchange,提问作者user23311233

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.19 02:18:15