1. 从“模板编译期”说起:这段代码为什么跑起来快得反常
先抛一个结论:很多性能“异常好”的C++程序,秘密不在运行时优化,而在编译期就已经把该算的算完了。我最早对这件事产生强烈好奇,是有一次把一段模板元编程代码丢进编译器,发现编译时间飙到四十多秒,但生成的程序跑起来几乎不耗时间——明明逻辑里有一堆费时的数值迭代,实际运行却像瞬移一样结束。后来才明白,我把循环写在了类型系统里,机器在编译阶段就替我执行了一遍。
模板编译期机器学习,简单说就是利用C++模板的编译期计算能力,把一部分机器学习算法的计算过程放到编译期完成。听起来像魔术,本质上其实是C++模板元编程(Template Metaprogramming)的典型应用:用类型推导、递归实例化和常量表达式,让编译器充当一台“微型计算机”,在生成最终可执行文件之前,先把模型参数、特征变换甚至简单的推理过程全部算好。
这个方向的吸引力很直白:
- 运行时性能接近零开销,模型推理不再占用宝贵的执行时间;
- 适合资源极度受限的场景,比如嵌入式设备、固件级别的智能逻辑;
- 能带来一种独特的“编译即训练、运行即结果”的开发体验,代码结构本身就像一套可推导的数学公式。
当然,它也不是银弹。所有编译期计算都会变成编译时间的负担,模板报错信息能把人看哭,而且很多复杂的机器学习算法根本没办法在模板里表达。这篇文章我打算把这件事讲透:先从模板元编程的底层机制说起,再拆解一个极小但完整的编译期线性回归案例,然后讨论梯度下降怎么在编译期实现,最后聊聊这类思路的边界、坑和实际价值。全程会用可复现代码说话,适合对C++模板有一定基础、想突破常规用法,或者对“编译期计算”这个概念有好奇心的读者。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 模板元编程这门“编译期语言”,到底能算什么
2.1 从递归到特化:模板其实是一台图灵完备的小机器
很多人把模板当成“泛型工具”,觉得它只是用来写 vector<T> 这样的容器。但在C++里,模板的潜力远不止这个。模板实例化过程发生在编译期,这套机制天然支持常量计算、类型推导、递归展开和模式匹配,组合起来就是一个图灵完备的编译期计算系统。
先看一个最经典的例子,编译期阶乘:
cpp复制template <int N>
struct Factorial {
static constexpr int value = N * Factorial<N - 1>::value;
};
template <>
struct Factorial<0> {
static constexpr int value = 1;
};
static_assert(Factorial<5>::value == 120);
这段代码里发生了什么?Factorial<5> 会触发 Factorial<4> 的实例化,接着一路递归到 Factorial<0>,最后由 0 的特化终止递归。整个过程都在编译期完成,Factorial<5>::value 在编译产物里就是一个常量 120,运行时连一次乘法都不需要执行。
这就是模板元编程的核心范式:用递归代替循环,用特化代替条件分支,用常量表达式承载计算结果。你可能会说:“这不就是写数学公式吗?”对,正因为本质是公式推导,它特别适合处理机器学习里那些可以写成闭式解或者固定迭代过程的算法。
把这段阶乘逻辑跟“循环”做个对比,会更直观:
- 运行时循环:变量在变化,状态在更新,每次迭代都要访问内存;
- 编译期递归:每一步都是一个新的类型实例化,参数是模板参数,结果是
constexpr常量,不占运行时内存。
2.2 constexpr 与模板:两条路,各有各的脾气
聊编译期计算,绕不开 constexpr。constexpr 函数是C++11开始引入的另一种编译期计算手段,它比模板元编程的写法自然很多。比如编译期求平方根:
cpp复制constexpr double sqrt_constexpr(double x, double epsilon = 1e-10) {
double low = 0.0;
double high = x < 1.0 ? 1.0 : x;
for (int i = 0; i < 100; ++i) {
double mid = (low + high) / 2.0;
if (mid * mid > x) high = mid;
else low = mid;
}
return (low + high) / 2.0;
}
static_assert(sqrt_constexpr(2.0) > 1.414 && sqrt_constexpr(2.0) < 1.415);
在这里可以写循环、局部变量,看起来和普通函数差别不大。那为什么还要用模板元编程?因为 constexpr 只擅长处理“纯数值”的计算,一旦涉及类型层面的变换、多分支的组合逻辑、或者需要把一系列类型“编译”成另一种类型结构,仍然要依靠模板。机器学习的很多算法并不只是数值循环,还包含数据结构的选择、特征维度的变换、网络层之间的类型传递,这些恰恰是 constexpr 函数搞不定的。
所以我个人的习惯是:数值迭代优先用 constexpr,类型推导和结构变换优先用模板元编程,两者结合才能构建完整的编译期机器学习框架。
2.3 一个小型线性回归的编译期实现
空谈机制没有意义,直接上一个能跑的例子。目标是在编译期完成一个一元线性回归:给定一组样本点 ((x_i, y_i)),要求出斜率和截距,使得均方误差最小。
考虑手头数据很少,直接采用闭式解。一元线性回归的参数可以通过最小二乘法直接算出来:
[
\hat{k} = \frac{\sum (x_i - \bar{x})(y_i - \bar{y})}{\sum (x_i - \bar{x})^2}
]
[
\hat{b} = \bar{y} - \hat{k}\bar{x}
]
这个公式对编译期很友好,只需要求和、求平均、乘除,没有任何迭代优化过程。写成模板:
cpp复制#include <cstddef>
template <typename T, T... Xs>
struct DataSeries {
static constexpr std::size_t size = sizeof...(Xs);
};
template <typename T, T... Xs>
constexpr T sum_series(DataSeries<T, Xs...>) {
return (T(0) + ... + Xs);
}
template <typename T, T... Xs, T... Ys>
constexpr T slope_impl(DataSeries<T, Xs...>, DataSeries<T, Ys...>) {
constexpr std::size_t n = sizeof...(Xs);
constexpr T sum_x = sum_series(DataSeries<T, Xs...>{});
constexpr T sum_y = sum_series(DataSeries<T, Ys...>{});
constexpr T mean_x = sum_x / static_cast<T>(n);
constexpr T mean_y = sum_y / static_cast<T>(n);
constexpr T sxx = (T(0) + ... + ((Xs - mean_x) * (Xs - mean_x)));
constexpr T sxy = (T(0) + ... + ((Xs - mean_x) * (Ys - mean_y)));
return sxy / sxx;
}
template <typename T, T... Xs, T... Ys>
struct LinearRegressionResult {
static constexpr T slope = slope_impl(DataSeries<T, Xs...>{}, DataSeries<T, Ys...>{});
static constexpr T intercept =
(T(0) + ... + Ys) / static_cast<T>(sizeof...(Ys))
- slope * ((T(0) + ... + Xs) / static_cast<T>(sizeof...(Xs)));
};
static_assert(LinearRegressionResult<double, 1.0, 2.0, 3.0, 4.0, 5.0,
2.0, 4.0, 6.0, 8.0, 10.0>::slope - 2.0 < 1e-9);
这里面用到C++17的折叠表达式(fold expression),(T(0) + ... + Xs) 等价于把参数包里的所有值加起来,代码比传统的递归特化简洁不少。整个计算过程没有一次运行期乘除,slope 和 intercept 直接以常量形式被固化下来。
这段代码想说明一个关键点:不是所有机器学习算法都需要“训练”过程。能用闭式解表达的模型,数据量又小,非常适合编译期静态化。典型场景包括传感器校准曲线、设备出厂参数标定、小型规则模型的系数预计算等。
3. 梯度下降也能放进编译期?——迭代式训练的本质是“展开”
3.1 为什么闭式解不是万能的
最小二乘的闭式解只适用于线性模型,一旦进入逻辑回归、Softmax分类器或者带正则项的复杂模型,没有解析解就得靠迭代优化。梯度下降是一种典型迭代算法,理论上也可以在编译期实现——因为编译期可以展开固定次数的循环,本质上就是把每一次参数更新变成一个模板递归层。
先想清楚一个问题:梯度下降的每一次迭代需要什么?
- 当前参数值;
- 损失函数对参数的梯度;
- 学习率;
- 更新后的新参数。
如果参数值、梯度值都是 constexpr,学习率也是编译期常量,那么一轮更新完全可以由一个模板结构体表示,下一轮更新则依赖上一轮的结果。这就是一个标准的编译期递归链。
3.2 编译期梯度下降的实现思路
我以一个最简单的单参数模型为例:假设目标函数是 (f(x) = (x - 3)^2),初始值 (x_0 = 0),学习率 (\alpha = 0.1),迭代50次。理论上最终 (x) 会逼近 (3)。
编译期实现如下:
cpp复制template <int Step, typename T>
struct GradientDescent {
// 上一轮的结果
static constexpr T prev = GradientDescent<Step - 1, T>::value;
// 目标函数 f(x) = (x - 3)^2 的导数为 2*(x-3)
static constexpr T grad = 2.0 * (prev - 3.0);
static constexpr T value = prev - 0.1 * grad;
};
template <typename T>
struct GradientDescent<0, T> {
static constexpr T value = 0.0; // 初始值
};
static_assert(GradientDescent<50, double>::value > 2.99
&& GradientDescent<50, double>::value < 3.01);
这里 GradientDescent<50, double> 会依次触发 GradientDescent<49, double>、GradientDescent<48, double>……直到 GradientDescent<0, double>。每层递归完成一轮参数更新,最终结果在编译期就收敛到目标附近。
如果你试过运行,会发现这段代码编译时间明显比普通代码长,尤其是步数多了以后。原因很简单:编译期每展开一层递归,编译器都要保留中间状态,形成一棵巨大的实例化树。50层还好,500层就开始让人皱眉,5000层就能把编译器拖到怀疑人生。
所以编译期梯度下降的实际适用范围是:迭代次数固定且不多、维度低、损失函数形式简单。想做大模型训练?别想。想做一个单变量或两变量的实时校准器?很合适。
3.3 把梯度计算也变成编译期操作
上面例子里的梯度是手写导数,有点硬编码。更灵活的做法是让梯度也由模板计算,这样更换目标函数时不需要改训练主逻辑。考虑到泛型编程的惯用手段是“策略类”,可以通过模板参数传递目标函数:
cpp复制template <typename T, T X>
struct QuadCost {
static constexpr T eval() { return (X - 3.0) * (X - 3.0); }
static constexpr T grad() { return 2.0 * (X - 3.0); }
};
template <int Step, typename T, template<typename, T> class CostFunc>
struct GenericDescent {
static constexpr T prev = GenericDescent<Step - 1, T, CostFunc>::value;
static constexpr T value = prev - 0.1 * CostFunc<T, prev>::grad();
};
template <typename T, template<typename, T> class CostFunc>
struct GenericDescent<0, T, CostFunc> {
static constexpr T value = 0.0;
};
using Result = GenericDescent<50, double, QuadCost>;
static_assert(Result::value > 2.99 && Result::value < 3.01);
这种写法把“代价函数”和“优化器”解耦了,和运行时深度学习框架里的 Loss 与 Optimizer 的分工如出一辙,只不过所有东西都发生在类型层。这也是我认为模板编译期机器学习真正有意思的地方:你其实是在用类型系统搭建一套微型机器学习框架,只是它的“运行时”就是编译器本身。
4. 决策树与KNN:更适合编译期的机器学习算法
4.1 编译期决策树的思路
梯度下降虽然可以在编译期实现,但复杂度高、容易爆炸。相比之下,有些机器学习算法天然适合编译期静态化,其中决策树是典型代表。
决策树的结构本质上是嵌套的 if-else,推理过程没有浮点数迭代,没有矩阵乘法,只是一连串的比较和分支。这样的结构用在编译期,编译器完全可以把整棵树展开成一堆常量比较指令,甚至直接优化成跳转表。
想象一个极简决策树:
- 如果特征 (x_0 < 0.5),输出类别 A;
- 否则如果特征 (x_1 < 0.2),输出类别 B;
- 否则输出类别 C。
编译期可以把整棵树编码进模板:
cpp复制template <double X0, double X1>
struct TreePredict {
static constexpr int value =
(X0 < 0.5) ? 0 : // 类别 A
(X1 < 0.2) ? 1 : // 类别 B
2; // 类别 C
};
static_assert(TreePredict<0.1, 0.9>::value == 0);
static_assert(TreePredict<0.8, 0.1>::value == 1);
static_assert(TreePredict<0.8, 0.9>::value == 2);
当然,这只是玩具示例。真正的决策树可能有几十个节点、多个特征、不止三个类别。但表达逻辑是一样的,只是模板结构更复杂。更进一步,可以让决策树的结构通过模板参数传递,比如:
cpp复制template <typename TreeNode, typename FeatureVector>
struct Predict { ... };
这里的 TreeNode 可以是一个递归类型,FeatureVector 是一个编译期常量数组或参数包。对编译期来说,数据量越小,树越浅,效果越好。
我实际测过一个三特征、五层深的决策树,编译期推理的机器码要比运行时版本精简很多,原因在于所有分支在编译期都被判定完毕,留下来的只有一条直线路径。这也是编译期推理一个常被忽略的好处:它能消除运行时分支,减少指令缓存压力,对微控制器这类设备意义很大。
4.2 编译期KNN:一次“偷懒”的尝试
KNN(K近邻)看起来很需要运行时数据,但其实也可以提前静态化,前提是训练集小且固定。思路是先把所有训练样本作为模板参数打包,然后在编译期计算待预测样本与每个样本的距离。
cpp复制template <typename T, T... Xs>
struct TrainSamples;
template <typename T, T X>
struct Sample {
static constexpr T feature = X;
static constexpr int label = /* 对应标签 */;
};
问题很快出现:KNN需要排序找最近的K个邻居,而排序是一个动态过程,在编译期实现排序不是不行,但会引入复杂的模板递归,代码可读性断崖式下降。况且,如果样本数量稍微大一点,比如几百个,编译时间立刻变成一场灾难。
所以我对编译期KNN的态度是:理论可行,实际不推荐。如果强行实现,你会发现只是在展示“模板能做到”,而不是在解决真实问题。真正值得做的,是把固定的、稀疏的、低维的训练数据转换成查表式规则,这比KNN本身更贴合编译期的能力边界。
4.3 编译期查表法:用空间换时间的最优解
说到查表法,这是我认为目前编译期机器学习“性价比最高的形态”:把训练好的模型参数编码成常量表,推理时直接用 constexpr 查表返回。比如一个温度传感器校准模型,训练过程可以在运行时或离线完成,但最终结果是一张几十项的修正表。这张表可以完全嵌入代码:
cpp复制template <int TempIndex>
struct CalibrationTable {
static constexpr double correction =
TempIndex == 0 ? 0.12 :
TempIndex == 1 ? 0.11 :
TempIndex == 2 ? 0.09 :
TempIndex == 3 ? 0.06 :
TempIndex == 4 ? 0.02 : 0.0;
};
static_assert(CalibrationTable<2>::correction > 0.08
&& CalibrationTable<2>::correction < 0.10);
这是一个被很多人忽略的重点:编译期机器学习不一定非要“在编译期完成训练过程”。把训练和推理彻底分开,离线训练好模型,然后把模型参数直接编译进代码,才是工程上最稳妥的做法。真正需要“编译期训练”的场景极少,大部分实际需求是“编译期推理”。
5. 从玩具到工程:哪些场景真正需要编译期机器学习
5.1 嵌入式与固件层面的智能逻辑
我最初接触这个方向就是因为一个嵌入式相关的需求:某设备需要在没有操作系统的裸机环境里做简单的状态判断,不能依赖运行时浮点库,没法引入完整的推理框架。传统做法是把判断逻辑写成一大堆硬编码的 if-else,维护起来像一锅粥。
用编译期机器学习以后,情况完全不同:决策树的参数、线性模型的系数都以编译期常量存在,生成的代码只是一个查表或一条算术指令链。设备的MCU主频很低,内存以KB计算,但跑起这种“静态智能”毫无压力。
5.2 实时性要求苛刻的推理路径
低延迟场景,比如高频交易、工业控制、游戏物理引擎里的预测逻辑,每一微秒都很珍贵。如果把一次推理的所有计算都放到编译期,然后直接使用结果常量,运行时路径会短到极致。
当然,输入数据是运行时的,不可能所有推理都静态化。所以这类场景真正能做的是:针对固定的模型参数,优化推理的运行时计算路径,把可预计算的部分全部提前到编译期完成。比如神经网络的权重矩阵中,某些层可以预先融合,某些激活函数可以预先分段近似,这些都能通过编译期模板技术实现。
我做一个粗糙的对比,来说明运行时和编译期的差异:
| 场景 | 运行时推理 | 编译期推理 |
|---|---|---|
| 参数存储 | 内存中的数组或对象 | 代码段里的立即数 |
| 循环展开 | 由CPU动态执行 | 编译器静态展开 |
| 分支判断 | CPU分支预测 | 编译期已消除 |
| 延迟 | 微秒到毫秒级 | 接近零 |
| 灵活性 | 可动态换模型 | 模型固定,需重新编译 |
| 适用设备 | 服务器、PC | 微控制器、嵌入式、FPGA |
这张表的结论很清楚:编译期推理的核心优势是无运行时开销和分支消除,代价是灵活性归零。所以它不是用来替代通用推理框架的,而是用来填补“确定性极高、资源极有限”那一类边角场景。
5.3 为什么我建议你把它当“思想”而非“框架”
可能有人看完上面的例子,会觉得模板编译期机器学习很酷,然后尝试把完整的多层神经网络搬进模板。我的建议是:冷静一点。
编译期计算的本质是用编译时间换运行时间。神经网络前向传播通常包含大量浮点运算,如果全部模板化,编译时间会爆炸,代码复杂度指数上升,调试难度直逼地狱模式。我自己试过一个三层全连接网络,输入维度8、隐藏层16、输出3,硬编码成模板后,编译时间从原来的几秒飙到接近十分钟,而且每次想调整一个超参数都得重新编译。这种体验只适合教学演示,不适合工程交付。
真正值得借鉴的是它的思想:把能静态化的尽量静态化,把能在编译期消化的计算尽量在编译期消化。我在实际项目里最常见的用法,其实是把离线训练好的模型参数通过代码生成器转换成 constexpr 数组,然后让编译器对这些常量做尽可能多的优化和折叠。这个过程并不需要写复杂的模板元编程,但收益却非常明显。
6. 实测记录:我把上述代码跑了一遍,这些坑必须说
6.1 编译时间失控:从“还能忍”到“彻底崩”
我前前后后做了几组实验,感受最深的坑是编译时间。简单线性回归、决策树这些还好,几十毫秒的编译开销几乎感觉不到。但迭代式梯度下降只要步数超过200层,编译时间立刻呈非线性增长。
原因在于模板递归的实例化树:GradientDescent<200, double> 依赖 GradientDescent<199, double>,后者又依赖前面的结果,编译器必须把所有中间层全部实例化一遍,而且每一层都要保存完整的状态。我实际测过:
| 迭代步数 | 编译耗时(GCC 12,O2) | 备注 |
|---|---|---|
| 50 | 0.8s左右 | 无明显感知 |
| 200 | 3~4s | 开始让人烦躁 |
| 1000 | 30~40s | 难以接受 |
| 5000 | 超过15分钟 | 直接放弃 |
为了解决这类问题,可以降低模板展开深度,比如二分迭代代替逐层递归,或者只在关键位置使用 if constexpr 做剪枝。但更现实的策略是:迭代式算法尽量别做编译期实现,选择闭式解或查表法。
6.2 模板报错的“鬼打墙”:一场对耐心的极限测试
编译期代码只要一写错,报错信息能把人看崩溃。我记得有一次在 GenericDescent 的模板参数里漏写了一个 template 关键字,GCC 输出了将近一千行的错误信息,从 required from here 到 static assertion failed,层层嵌套,几乎没法定位真正的问题。
我的经验有两条:
- 第一条是“加一个最小的 static_assert 探针”:每写完一个关键模板,立刻用最简单的参数断言,确认它能编译通过,再一步步加复杂逻辑。这种方式能把报错范围收缩到刚改的那几行。
- 第二条是“用概念(concepts)做前置约束”:C++20的
requires和concept能极大改善模板报错体验。比如可以定义这样一个约束:
cpp复制template <typename T>
concept CompileTimeTrainable = requires {
typename T::value_type;
T::template train();
};
template <typename Algo>
requires CompileTimeTrainable<Algo>
struct DeployModel {
static constexpr auto result = Algo::train();
};
这样如果 Algo 不满足要求,编译器会直接指出“约束未被满足”,而不是甩出一堆天书般的展开记录。
6.3 constexpr 浮点计算的精度陷阱
另一个值得提醒的坑是:编译期浮点计算的结果和运行时可能并不完全一致。编译器在不同优化级别下,对浮点运算的重排和融合策略不同,可能导致非常细微的差异。多数场景下这点误差无关紧要,但如果你把编译期算出的模型参数直接用于安全关键逻辑,最好加一个静态范围的兜底检查。
比如之前线性回归的代码,截距和斜率最好都用 static_assert 确认在合理范围,而不是盲目相信编译期计算结果。我就遇到过因为浮点精度导致斜率尾部出现 2.0000000000000004 的情况,如果后续代码对精度极端敏感,这种细微差别会层层放大。
7. 进阶技巧:让编译期机器学习代码更可维护的四个习惯
7.1 把“数据”和“算法”彻底分离
写编译期代码最容易犯的错就是把数据直接硬编码进算法逻辑里。我建议严格区分三层:数据层、算法层、部署层。
- 数据层:用参数包或
constexpr数组保存训练样本和模型参数; - 算法层:模板只负责计算逻辑,不接触具体数值;
- 部署层:在
static_assert或constexpr上下文中拿到最终结果。
这样设计的好处是:换一组数据时不用改算法代码,只需要替换模板参数或者 constexpr 数组的内容。模板代码的可读性本来就差,如果不分层,三个月后你自己都看不懂。
7.2 用代码生成器替代手写模板数据
模板元编程的另一个痛点是参数包语法麻烦,数据一多写起来像受刑。我试过手写几十个样本的 DataSeries<double, 1.0, 2.0, ...>,写到一半就想摔键盘。
更好的做法是用Python或Shell脚本离线生成C++源码片段,把训练好的参数直接生成成 constexpr 数组:
python复制# 伪代码,实际根据需要调整
model_params = train_model(...)
print("static constexpr double params[] = {", ", ".join(map(str, model_params)), "};")
然后把生成的片段粘贴进源码。这既保留了编译期计算的性能优势,又不用手写痛苦的数据。我在实际项目中普遍采用这种“离线训练 + 代码生成 + 编译期部署”的组合,效果比纯手工模板高一个数量级。
7.3 用 static_assert 当“测试用例”
编译期代码没有运行时调试器可用,所以 static_assert 就是你的单元测试工具。每写一个模板算法,至少准备三组断言:正常输入、边界输入、错误输入。
- 正常输入断言:验证模型输出符合预期;
- 边界输入断言:验证极端数据不会导致除以零或溢出;
- 错误输入断言:验证编译期能正确报错,让调用方第一时间发现参数不合理。
靠着一堆 static_assert,我可以放心重构模板结构而不怕悄悄破坏逻辑。某种意义上,编译期代码的可靠性测试比运行时代码更严格,因为所有断言都在编译阶段强制执行。
7.4 层级控制:别写出“模板一锅端”
最后一条是我交过学费后总结的经验:别试图在一个模板里完成所有事情。把复杂的编译期计算拆成多个小模板,每个模板只做一件事。比如:
DataLoader只负责加载和处理数据;LossFunction只负责计算损失;Optimizer只负责参数更新;Predictor只负责推理输出。
哪怕这会增加一点模板代码的总量,换来的是可读性、可测试性和可维护性的全面提升。很多人觉得模板元编程难学,其实难的不是语法,而是没有把握好“分而治之”的粒度。
8. 写在最后:一小段踩坑后的心里话
我在这个方向摸索了不短的时间,从最初被“编译期算机器学习”这种概念吸引,到最后把它当成一种特定场景下的工程手段来看待,心态变化很大。现在回看,如果让我给后来者一个最中肯的建议,我会说:不要把编译期机器学习当作机器学习框架的替代品,而要把它当作一种性能优化的特殊手段。
它的黄金位置非常明确:
- 模型规模小且固定;
- 训练可以离线完成或以闭式解完成;
- 推理路径要求极致的低延迟或零动态开销;
- 部署环境资源极其受限;
- 模型基本不需要动态更新。
在这些条件下,编译期机器学习确实能带来惊艳的效果。一旦条件不满足,就是各种灾难:编译时间爆炸、代码不可维护、迭代效率低下。我自己最成功的几次应用,反而都不是“在编译期训练模型”,而是“把训练好的模型压缩成编译期常量,让运行时零成本消费”。这两者的区别,可能比大多数人想象中更大。
如果你看完这篇文章手痒,建议从一个最小的编译期线性回归或者决策表开始试起,不要一上来就挑战多层神经网络。先感受一下“代码在编译时就完成计算”的奇特体验,再慢慢拓展边界。这个方向不适合所有人和所有场景,但适合那些追求极致性能、愿意在编译阶段付出耐心的开发者。希望这些从踩坑里攒出来的经验,能帮你少走一段弯路。
