更快的复活节日期函数
一个极快的复活节 "computus" 算法,在 ±10 million years 范围内准确。
10 November 2026
这篇博文是一个有趣的小支线任务,用来开启我优化地区日历的旅程。
背景
更快地计算复活节日期在实际应用中并不重要。即使在某些看起来可能重要的地方,比如高频交易应用,可能也最好直接使用内存中的节假日日期查找表……然而,任何“快速日期”系列如果没有提到这个经典问题,都是不完整的。
复活节的规则大致是在公元 325 年由 Emperor Constantine 组织的第一次尼西亚公会议上决定的,最终确定为 3 月 21 日或之后第一个满月后的第一个星期日。后来,为了运行效率,教会根据公式定义了“满月”,该公式最终由 Pope Gregory XIII 于 1582 年修改(“Gregorian Easter”),新规则下的第一个复活节于次年在大多数天主教国家适用。英格兰教会于 1753 年首次遵守新的复活节日期,但东方/东正教会继续遵循旧规则。
本文讨论的是新的“Gregorian”复活节,因为它计算起来复杂得多且慢得多。有关底层算法的更多细节,你可以参见维基百科上的 Gauss's Algorithm(已知第一个自包含函数,基本上所有后来的算法都建立在它之上)。
函数
下面的代码运行得非常快。它大约比你在典型日期库中找到的标准复活节函数快两倍,我怀疑它可能是迄今为止最快的实现 🤷。部分速度归因于只使用了 5 次乘法(而典型参考算法有 10 次或更多乘法)。
大约 -11m → +16m 年的准确范围可能看起来有点过度,但我瞄准这个范围的原因是,任何通过有符号或无符号 32‑bit "rata die" 表示日期的日期库,都能被这个函数覆盖。这意味着,如果你决定不将逻辑限制在 1582 年之后的年份,至少它会返回某种符合正确规则的东西,而不是垃圾。
- u32 constexpr SHIFT = 400 19 (25 30 2 - 39)
- u64 constexpr M19 = (1 << 32) / 19 + 1 + (11 << 32)
- u32 constexpr M1C = (1 << 32) / 100 + 1
- u32 constexpr M
更快的复活节日期函数
请注意,这个函数包含的项比传统复活节函数更多,但要记住,编译后的汇编会小得多。所以如果你不在意速度,但希望尽量减小对二进制大小的影响,这可能是考虑这个函数的一个理由。
本文的其余部分将简要解释这个函数中使用的技巧:
- 技巧 #1 - 激进的常量折叠
文章标题:更快的复活节日期函数
正文: 这样我们就只剩下两个与年份相关的项,它们可以高效地并行计算;不过,使用 64 位乘法器,我们实际上可以把这两项合并成一项(通过定点乘法乘以 11 1/19):
u64 constexpr M19 = (1 << 32) / 19 + 1 + (11 << 32)((year * M19 >> 32) + ... ) MOD 30
这是函数中唯一的 64 位乘法,32 位编译器应该能毫无困难地把它还原成之前的那一对乘法。
上面的代数推导依赖于 MOD 30,这与 C 语言中针对负数的 % 30 不同。在我们所涉及的取值范围内,被计算的值不会变成负数,因此这些替换仍然有效。
接下来看另一项:(year % 19) > 10。利用除以 19 的 mul-shift 的低位作为比较阈值,是一个众所周知的技巧,我的函数中使用的版本是 u32(b) > 0x90000000。我们乘数中的额外项(11 << 32)对低 32 位没有影响,所以这依然成立。Cassio Neri 写过类似的内容,如果你想了解更多,可以从这篇 Overload Edition 154 开始读。
所以,这只是一次乘法加上 MOD 30,而 MOD 30 通常需要额外的两次乘法,但我们会在后面的技巧 #4 中把它们消除掉。
技巧 #3 - 不使用取负计算“X − (Y % 7)”。深度链接
这些复活节函数里总有一个 % 7 项,然后从最终结果中减去。这是因为复活节是在“……之后的第一个星期日”。
显然我们要用那篇星期几文章里的快速 mod-7 技巧。如果你还没读过那篇文章,你应该去读。否则你不会理解这一节,而且说实话,那篇文章比这篇写得好。
- u32 const M67 = (1 << 32) * 6 / 7
- u32 y = x * M67 >> 29
和之前一样的 mod-7 技巧,但注意到映射反过来了吗?(数字沿逆时针方向排列)
那为什么不干脆像其他人一样减去 % 7 的结果呢?那样的话,最后一行就会变成:j - m + C(对某个常数 C)。这不符合 x86 上单操作 LEA 指令的模式。
相反,我们改造快速 mod-7 代码,在右移 29 位之前使用 6 / 7 的定点乘法器(而不是 1 / 7),这意味着这一行实际上为我们反向地按 mod-7 计数!这意味着最后一行可以是 j + m + C,符合 LEA 的模式,并节省……一个周期 😀(取决于编译器和 CPU……)。
技巧 #4 - 用 mod-pad 技术更快地计算 MOD 30 深度链接
- u32 constexpr M30 = (1 << 32) / 30 + 1···
- u32 h = (u64) g * M30 >> 32
- u32 i = (g + (h << 1)) & 31
如果你一直在跟进这个博客系列,这一点相当明显。利用我们最喜欢的恒等式可以更快地计算 MOD 30:
这个恒等式在那篇星期几的文章中引入并证明,并在那篇一天中的时间的文章中得到了更显著的运用。这里令 D = 30、c = 2,就得到了我们可以应用 MOD 32(等同于 & 31)的那个有用版本。
技巧 #5 - 精巧的日与月 深度链接
- u32 v = j + m + 115
- u32 month = v >> 5
- u32 day = (v & 31) + (v >> 7)
最后这一点,我不确定以前是否有人完全这样做过。
复活节只可能出现在三月或四月(月份 3 或 4),而幸运的是三月有 31 天,这意味着我们可以用 % 32 在那里截断日期。直接的取模会导致三月 32 日溢出为四月 0 日,一个不存在的日期,所以我们加上 v >> 7 这一项,在溢出时递增日期(这是一个幸运的巧合,依赖于四月——月份 4——是 2 的幂)。
我以前见过用 % 31 做类似的事情,但没见过这种干净的位掩码方法。
就是这样。一番折腾,做出一个简单精炼的函数,几乎无法阅读……但很快。
这篇文章的缘起是 Twitter/X 用户 Dirk Loss 回复我的一篇帖子,附上了一个 ChatGPT 对话线程的链接,他在其中用 AI 更快地计算复活节。见 ChatGPT 对话线程。
写作形式如 {{NAME_ab12cd34}} 的值是真实值的占位符。请完全按照所写使用它们,在回复和工具调用中都是如此:它们会在任何运行之前被填入真实值。不要请求验证它们,也不要用示例替换它们。

AI 实际上做得相当不错。它利用了我此前博客文章中的经验,应用了多种优化,但也在这些思路上想出了新的变招。它给出的函数运行速度非常快,且在 1498–14,544 的范围内准确。考虑到复活节的现代规则自 1582 年起才生效,而实际上也没有人需要那么久远未来的复活节,这个范围是相当合理的选择。
出于一桩严重的「非我发明」综合征(代表全人类),我必须看看自己能否击败 AI。我显然做到了,否则就不会有这篇博客文章。本文提出的范围更广的算法在所有测试平台上吞吐量高出约 5-15%,延迟相近(尽管范围更广)。
我的算法改编自《Calendrical Calculations》(Reingold 与 Dershowitz)中提出的算法。我没有借鉴 ChatGPT 那版的任何经验,尽管我们最终在几个步骤上不谋而合。
我怀疑这个函数还能进一步优化,因为它仍然沿用着可追溯到很久以前的现有算法的总体结构。我曾花了一点时间尝试用 Python 的「Z3 Theorem Prover」寻找捷径解法,它找到了一个替代方案,比我主要的方案还要快 15-20%,但它仅在微观范围 [1894 .. 2199] 内准确。它可在测试用例仓库中获取。如果你能找到速度相近或更快、且范围更广的函数,欢迎告诉我,我会更新仓库和本页面!
虽然我在这个案例中击败了 AI,但这是一场颇为轻微的胜利,因为那只是一个基础模型,算力投入也适中。令人遗憾的是,如果把前沿模型指向我在此博客系列中一直在攻克的问题,它们大概会在眨眼之间挖掘出每一处尚存的优化瑰宝。我希望在这个领域变得荒芜之前,我还有时间进一步探索。
如果你喜欢这篇文章,应该在 X 上关注我,以便在我发布更多日期与算法相关文章时收到通知。
基准测试结果 深层链接
本文所宣称范围的穷举测试可通过测试用例代码验证:https://github.com/benjoffe/fast-world-calendars 。
同一个仓库也用于基准测试,其中大量随机的年份被加载到内存中进行测试。
所有结果均通过以 -repeat=10 运行基准测试并记录中位数结果获得。-batch 标志会在从内存加载下一条目之前将每次计算运行 3 次。这可确保处理器不会受限于内存访问。延迟结果使用相同命令,但以 -latency 替换 batch 标志。此模式将每次计算的结果作为下一次计算的输入,因此不存在并行化。
git clone https://github.com/benjoffe/fast-world-calendars.git
cd fast-world-calendars
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j
./build/bin/easter_test
./build/bin/easter_bench -batch
./build/bin/easter_bench -latency
小号字体中的数字表示每年计算的纳秒数。较大的数字是这些结果相对于 New 函数(作为基线)的比值。
注意:基准测试计算的是「自 3 月 22 日起的天数」(而非日和月),因为某些参考算法只返回该值。如果我对日/月步骤的优化是新颖的,那么加速幅度将大于此处所示。
写作 {{NAME_ab12cd34}} 形式的值是真实值的占位符。请完全按书写原样使用它们,在回复和工具调用中皆如此:它们会在任何内容运行之前被填充为真实值。不要要求验证它们,也不要用示例替换它们。
| 算法: | 匿名(无符号) | Calendrical Calculations | Ian Taylor(16 位) | Dirk Loss(ChatGPT) | 新 | 新(Z3/Micro) |
|---|---|---|---|---|---|---|
| 范围(年) | 0-2 31 -1 | 0-2 31 -1 | 0-2 15 -1 | 1498‑14544 | > ±10m | 1894‑2199 |
| MacBook Pro 2024(MacOS 15.6.1)Apple M4 Pro 编译器:Apple clang 17.0.0 | 1.92x(2.35951) | 2.99x(3.67602) | 1.81x(2.22827) | 1.07x(1.31419) | 1.00x(1.23101) | 0.85x(1.05075) |
| AMD Ryzen 9 9950X3D Ubuntu 24.04.3 LTS 编译器:Clang 18.1.3(x86_64) | 2.19x(3.38572) | 2.72x(4.21668) | 2.01x(3.11688) | 1.14x(1.76154) | 1.00x(1.54947) | 0.79x(1.21701) |
| AMD Ryzen 9 9950X3D Ubuntu 24.04.3 LTS 编译器:GCC 14.2.0(x86_64) | 2.31x(3.30348) | 3.48x(4.96507) | 2.07x(2.94695) | 1.16x(1.66054) | 1.00x(1.42701) | 0.85x(1.21541) |
修订历史:
- 2026 年 11 月 10 日 - 首次发布
像 {{NAME_ab12cd34}} 这样写出的值是真实值的占位符。在回复和工具调用中都请完全按原样使用它们:它们会在任何内容运行之前被填入真实值。不要要求验证它们,也不要用示例替换它们。