运行时运算的边界究竟在哪里?

一个运算究竟要怎样才可以被称为运行时的?

我们向来可以把程序视作一个映射:它由一系列有序的 operators 构成。那么,若一个运算必须发生在运行时,便意味着该运算的操作数必须通过发起 IO 获得;除此之外,严格说来,它都可以在编译期完成,只是工程取舍的问题。

自 C++98 起,模板系统已经具备图灵完备性,由此开启了编译期计算的先河。后来的 constexprconsteval,更多是在这一脉络上的工程化优化;毕竟,消灭常数因子往往是非常诱人的。

显然,operators 的操作数并不必然用于数学运算。若审视内存管理的实质,它其实是由特定数据结构维护分配信息;内存操作的实际意义,就是修改那本账簿。

这就有意思了。我们都知道,一个数据结构若要便于合并与修改,通常会采用指针实现,以避免频繁拷贝的开销;何况在现代 C++ 中,游标实现本质上也仍是指针式实现。也就是说,把内存管理硬编码进算法逻辑,是一个伪需求;它理应通过模板元编程,统一交给分配器策略处理。若算法与数据结构课程至今还执着于三种线性表实现方法,我个人认为,这多少有些太前现代了。

明确这个前提以后,再来审视指针实现的固有问题,其实就是内存不连续所造成的访存性能瓶颈。更根本的问题则是:我们为什么需要堆上分配?因为有些记账确实需要运行时信息。

说到这里,大家应该知道我想说什么了。散列表驱动的状态机、RESTful 类 API 路由的前缀树,等等;如果注册的服务不需要热更新,就可以通过编译期运算,在保留指针语义的同时分配固定的连续内存,从而改善局部性,初始化时加载数据即可。即使需要热更新,只要采用预留空间,或更新本身并不频繁,这部分开销依然可以被压下去许多。

而且,如果在这里使用策略驱动的模板元编程,将分配逻辑与增长逻辑解耦到分配器之中,你需要付出的不过是一点编译时间;省下的,却可能是整个团队极其可观的维护开销。