A faster way to calculate the day-of-the-week
A faster way to calculate the day-of-the-week
计算星期几的更快方法
Converting a day-count (“rata-die”) to the day-of-the-week (“weekday”) sounds like it should be so trivial, that there’s almost nothing to say about it. But, as it turns out, when we look under the hood, this is a surprisingly complex problem. 将天数(“rata-die”)转换为星期几(“weekday”)听起来似乎微不足道,几乎没什么可说的。但事实证明,当我们深入探究时,这是一个令人惊讶的复杂问题。
Throughout this article I will present a range of really fast functions to solve this problem, tuned for different use cases (throughput vs latency, different platforms etc.). Each outperforms existing solutions, and many have a latency of just a single multiplication plus two cycles. A surprising result is presented: the weekday can be computed in ISO format ([1‥7] instead of [0‥6]), with the exact same instructions, just with tweaked constants (and zero speed penalty). 在本文中,我将介绍一系列用于解决此问题的超快速函数,它们针对不同的使用场景(吞吐量与延迟、不同平台等)进行了调优。每一个函数都优于现有的解决方案,许多函数的延迟仅为一次乘法加上两个周期。文中还展示了一个令人惊讶的结果:星期几可以以 ISO 格式([1‥7] 而非 [0‥6])计算,且使用完全相同的指令,只需微调常量(且没有速度损失)。
To give you a taste of the insanity, I’ll highlight my favourite function here, this crazy looking 3-instruction sequence (plus a constant load) is accurate over the full signed 32-bit range (it may not be the lowest latency full-range algo in this article, but has the highest throughput for x86): 为了让你感受一下这种疯狂,我将在这里重点介绍我最喜欢的函数。这个看起来很疯狂的 3 指令序列(加上一个常量加载)在整个有符号 32 位范围内都是准确的(它可能不是本文中延迟最低的全范围算法,但它在 x86 上具有最高的吞吐量):
Unix Weekday [0‥6] / ISO Weekday [1‥7] Unix 星期 [0‥6] / ISO 星期 [1‥7]
Given: input (rd = signed 32-Bit Unix day-count); Compute weekday [0‥6]: 给定:输入 (rd = 有符号 32 位 Unix 天数);计算星期 [0‥6]:
mov eax, 613566756 ; Constant Load: u32 M = (1 << 32) / 7
imul ecx ; rd * M (u32 a = low bits, i32 b = high bits)
lea eax, [eax-1828716544+edx*4] ; u32 r = a + 4 * b + (Z = 0x93000000)
shr eax, 29 ; weekday = r >> 29
You don’t need prior understanding of assembly to follow this blog post. By the end, you will understand why this code above works. 你不需要具备汇编语言的先验知识也能读懂这篇博文。读完后,你就会明白上述代码为何有效。
Simple Approaches
简单方法
Given: rd = rata-die (day-count, signed 32-Bit int), with epoch 1970-01-01 = Thursday (4) — Then: 给定:rd = rata-die(天数,有符号 32 位整数),纪元 1970-01-01 = 星期四 (4) — 则:
Double-mod (languages with signed ”%”, eg. C/C++)
双取模(支持有符号 ”%” 的语言,例如 C/C++)
weekday = ((rd % 7) + 7 + 4) % 7
Languages with special positive-mod (eg. Rust)
具有特殊正取模的语言(例如 Rust)
weekday = (rd + 4) POSMOD 7 — Where: weekday ∈ [0‥6] (0 = Sunday)
weekday = (rd + 4) POSMOD 7 — 其中:weekday ∈ [0‥6] (0 = 星期日)
This is what I would recommend in most non-library code where maintenance is more important than micro-optimisation. 在大多数非库代码中,如果维护比微优化更重要,我建议使用这种方法。
Hinnant
Hinnant 方法
Howard Hinnant’s technique (2014) was adopted by many date libraries. Howard Hinnant 的技术(2014 年)已被许多日期库采用。
Hinnant’s Algorithm (bit-size independent)
Hinnant 算法(与位宽无关)
weekday = rd >= -4 ? (rd + 4) % 7 : (rd + 5) % 7 + 6
This approach appears designed for simplicity and flexibility. It is the only algorithm from here onwards that does not rely on sign casting or overflow, nor is it bit-width specific. It will be the same logic for 8-bit through to 64-bit. 这种方法的设计初衷似乎是简洁和灵活。它是后续介绍中唯一不依赖符号转换或溢出,也不针对特定位宽的算法。从 8 位到 64 位,其逻辑完全相同。
Neri
Neri 方法
As usual, Cassio Neri’s work is the modern gold standard. In 2024 Neri published a very clean full-range solution to this problem. 像往常一样,Cassio Neri 的工作是现代的黄金标准。2024 年,Neri 发布了一个非常简洁的针对该问题的全范围解决方案。
Cassio Neri: 32-bit version (Range: Full signed 32-bit)
Cassio Neri:32 位版本(范围:完整的有符号 32 位)
weekday = (u32(rd) + (rd >= 0 ? 4 : 0)) % 7
Cassio Neri: 64-bit version (Range: Full signed 64-bit)
Cassio Neri:64 位版本(范围:完整的有符号 64 位)
weekday = (u64(rd) + (rd >= 0 ? 4 : -5)) % 7
If you want something pretty fast, full-range, and not too low-level, then this is the function for you. The real trick to avoiding overflow here is the cast from signed to unsigned before doing any work. 如果你想要一种速度相当快、支持全范围且不太底层的函数,那么这就是适合你的选择。这里避免溢出的真正诀窍是在进行任何计算之前将有符号数转换为无符号数。