7 September 2026
Nearly all major date/time libraries compute hour, minute and second the same way - slowly.
In this article, I will dive deep into this little problem, and demonstrate a seemingly impossible claim:
A daily timestamp [0..86399] can be converted into Hour, Minute and Second – with the latency of only two multiplications (and on some targets even lower!).
This is in contrast to most date libraries which take around 16 cpu-cycles to compute.
The speed gain will come from two main ideas (in order of importance):
Even if you don't like low-level bit hacks in your code,
there will be something here for you.
Assume you have already reduced your timestamp to a date (not considered here) and time [0‥86399] (the subject of this article).
We'll first look at the way time is further decomposed in most date libraries, including the Linux kernel , glibc
, Go's standard library
, CPython's datetime
, OpenJDK's java.time
time (positive integer measured in seconds) — Then:hour = time / 3600rem = time % 3600 // Compiles to: time - hour * 3600
minute = rem / 60second = rem % 60 // Compiles to: rem - minute * 60
Notice that there is a complete dependency chain throughout this entire process: second depends on minute, which depends on rem, which depends on hour. There is nothing here that can overlap, or be parallelised.
The table to the right shows the dependency chain, it is quite long.
This can still perform quite well if you are processing a lot of timestamps in a tight loop, as a superscalar processor can pipeline multiple indexes, to "fill the gaps" so to speak. That is to say, it can have reasonable throughput, but better options will be presented.
| Step | x86 steps |
|---|---|
| 1 | _h = time * 2443359173 |
| 2 | mul-latency |
| 3 | mul-latency |
| 4 | hour = _h >> 43 |
| 5 | r = hour * 3600 |
| 6 | mul-latency |
| 7 | mul-latency |
| 8 | rem = time - r |
| 9 | _m = rem * 2290649225 |
| 10 | mul-latency |
| 11 | mul-latency |
| 12 | minute = _m >> 37 |
| 13 | s = minute * 60 |
| 14 | mul-latency |
| 15 | mul-latency |
| 16 | second = rem - s |
Note: All step/cycle tables assume a 3-cycle integer multiply latency, and 1 cycle for everything else. This is an approximate latency model, but it correlates strongly with my benchmark observations.
Some date libraries take a slightly different approach, computing each component independently. Sometimes this decision is driven by the shape of the API (eg. if each component is calculated in a separate function), other times it's a conscious choice. Date libraries using this approach include V8 and Boost
and musl
:
hour = time / 3600minute = (time / 60) % 60
second = time % 60This might be a little surprising at first that this can be faster.
When we trace through the computations, we get this table on the right. The dependency chain has been broken!
But, there is a downside.
| Superscalar x86 steps | ||
|---|---|---|
| Step | Minute Comp. | Hour & Second Comp. |
| 1 | _tm = time * 2290649225 | _h = time * 2443359173 |
| 2 | mul-latency | mul-latency |
| 3 | mul-latency | mul-latency |
| 4 | total_min = _tm >> 37 | hour = _h >> 43 |
| 5 | _m = total_min * 2290649225 | s = total_min * 60 |
| 6 | mul-latency | mul-latency |
| 7 | mul-latency | mul-latency |
| 8 | a = _m >> 37 | second = time - s |
| 9 | b = a * 60 | |
| 10 | mul-latency | |
| 11 | mul-latency | |
| 12 | minute = total_min - b | |
Traditional Approach 1 had 8 computations, whereas this one has 10.
This suggests the latency might have improved, but at similar or worse throughput, and that is exactly what we observe in the benchmarks.
As is often the theme in this "fast date" series of articles, manually implementing a mul-shift for division can speed things up on some processors.
u32 a = x / 60u32 b = x / 3600u32 a = (u64) x * 71582789 >> 32
u32 b = (u64) x * 1193047 >> 32
The mul-shifts are collectively accurate for over 626 hours. This means that if the input is bounded to just one day [0‥86399], then the substitutions are valid, and will provide a speed boost on 32-bit and x86 processors, and some SIMD chips.
Applying this to the Traditional Approach 1 & 2, reduces the latency chain length by two each (14 and 10 steps respectively).
As usual, the only other person as crazy as me to look into optimising this function is none other than Cassio Neri.
In 2020 Mr Neri added the following optimised variant to his high-performance calendar repo , and it was implemented in the datealgo-rs repo.
time — seconds ∈ [0‥97612918]27114:41:58u64 p1 = (u64) time * 71582789
u32 tmin = p1 >> 32
u32 second = u32(p1) / 71582789
u64 p2 = (u64) tmin * 71582789;
u32 hour = p2 >> 32
u32 minute = u32(p2) / 71582789
This utilises a variant of the technique from "Faster Remainder by Direct Computation " (2019, Lemire et al), where the high and low bits from the multiplication are used.
| x86 steps | ||
|---|---|---|
| Step | Computation 1 | Computation 2 |
| 1 | p1 = (u64) time * 71582789 | |
| 2 | mul-latency | |
| 3 | mul-latency | |
| 4 | p2 = (u64) (p1>>32) * 71582789 | _s = (u64) u32(p1) * 4026531799 |
| 5 | mul-latency | mul-latency |
| 6 | mul-latency | mul-latency |
| 7 | _m = (u64) u32(p2) * 4026531799 | second = _s >> 58 |
| 8 | mul-latency | |
| 9 | mul-latency | |
| 10 | minute = _m >> 58 | |
The result for hour is in the upper 32-bits of p2which can be accessed for "free": hour = p2>>32 | ||
This is usually faster than the traditional approaches, but it depends on the target platform. In order to find a faster way, we'll need to re-structure the problem...
So far, the algorithms have had a little bit of parallelism, but they've usually been quite lopsided.
Usually one column of the computation table is much longer than the other.
With a little bit of shuffling, we can streamline the function to be fully parallelised:
time — seconds tmin = time / 60 // "Total Minutes"
hour = time / 3600second = time - tmin * 60;minute = tmin - hour * 60;The reason this is not an obvious approach is that minuteis now computed entirely from two intermediary results (not directly from time).
| Two compact chains | ||
|---|---|---|
| Step | Computation 1 | Computation 2 |
| 1 | _tm = time * 2290649225 | _h = time * 2443359173 |
| 2 | mul-latency | mul-latency |
| 3 | mul-latency | mul-latency |
| 4 | tmin = _tm >> 37 | hour = _h >> 43 |
| 5 | s = tmin * 60 | m = hour * 60 |
| 6 | mul-latency | mul-latency |
| 7 | mul-latency | mul-latency |
| 8 | second = time - s | minute = tmin - m |
time — seconds ∈ [0‥2257198]626:59:58u32 tmin = (u64) time * 71582789 >> 32
u32 hour = (u64) time * 1193047 >> 32
u32 second = time - tmin * 60;
u32 minute = tmin - hour * 60;
The reason this is not an obvious approach is that minuteis now computed entirely from two intermediary results (not directly from time).
| Two compact chains | ||
|---|---|---|
| Step | Computation 1 | Computation 2 |
| 1 | _tm = time * 71582789 | _h = time * 1193047 |
| 2 | mul-latency | mul-latency |
| 3 | mul-latency | mul-latency |
| 4 | s = (_tm >> 32) * 60 | m = (_h >> 32) * 60 |
| 5 | mul-latency | mul-latency |
| 6 | mul-latency | mul-latency |
| 7 | second = time - s | minute = (_tm >> 32) - m | The result for hour is in the upper 32-bits of _hwhich can be accessed for "free": hour = _h>>32 |
This approach moves the two divisions to the start:
tmin and hour can occur simultaneously.minute and second can also overlap, as they have no co-dependency.We haven't even pulled any magic tricks out of the bag yet, and this simple rearrangement has caused the shortest dependency chain so far.
As simple as this change is, no precedent has appeared in my research of date libraries so far. This type of approach is used in other areas of computer science, such as converting numerical bases (eg. binary to decimal digits), but is perhaps not the first thing that comes to mind when dealing with time. If you learn of any prior date library that decomposed time this way, please let me know and I will update this page!
I was able to find one library that came very close to "V1", but was let down by the compiler; and that is: the .NET runtime .
In this approach, one computes second early and works up the chain in reverse:
time (positive integer measured in seconds) — Then:tmin = time / 60second = time % 60hour = tmin / 60minute = tmin % 60The latency depth of "Traditional 2" with the operation-count of "Traditional 1", best of both worlds.
I suspect it is not too rare, as it reads conceptually as "Traditional Approach 1" backwards. This is also the same as Neri's approach, before the bit tricks are applied.
This performs better than "Traditional Approach 1" due to tmin being directly computed, and available for the next two steps simultaneously. However in some rare instances the compiler can do even better and recognise that hour, being time / 60 / 60 can be rearranged to time / 3600, turning this into "New V1"!
From the looks of it, the .NET JIT Compiler is not able to recognise this optimisation: View on Sharplab . And when it comes to C++ compilers, MSVC fails to recognise the optimisation in all cases, and Clang and GCC only recognise it when the
% calls are replaced with subtractions and multiplications: View on GodBolt .
Given this approach relies on the compiler being smart, which is not always the case, writing it in the form of "New V1" is likely preferred. "New V1" also provides the structure which lends to the next optimisation...
Using the new structure from "V1", and combining with something like Lemire's high + low bit technique, we get this very compact function:
time — seconds ∈ [0‥2255818]626:36:58u64 hprd = (u64) time * 1193047
u32 mlow = time * 71582789
u32 hlow = (u32) hprd
u32 hour = hprd >> 32
u32 minute = (u64) hlow * 60 >> 32
u32 second = (u64) mlow * 60 >> 32
| Two compact chains | ||
|---|---|---|
| Step | Computation 1 | Computation 2 |
| 1 | _h = time * 1193047 | _m = time * 71582789 |
| 2 | mul-latency | mul-latency |
| 3 | mul-latency | mul-latency |
| 4 | _n = u32(_h) * 60 | _s = u32(_m) * 60 |
| 5 | mul-latency | mul-latency |
| 6 | mul-latency | mul-latency |
| The results are in the upper 32-bits which can be accessed for "free": hour = _h>>32 minute = _n>>32 second = _s>>32 | ||
Note that the constants come from the following:
1193047 = (1 << 32) / 3600 + 171582789 = (1 << 32) / 60 + 1time — seconds ∈ [0‥2255818]626:36:58u64 M_MUL = (((u64) 1 << 32) / 60 + 1) << 32
u64 H_MUL = (((u64) 1 << 32) / 3600 + 1) << 32
u128 hprd = (u128) time * H_MUL
u64 mlow = time * M_MUL
u64 hlow = (u64) hprd
u32 hour = hprd >> 64
u32 minute = (u128) hlow * 60 >> 64
u32 second = (u128) mlow * 60 >> 64
| Two compact chains | ||
|---|---|---|
| Step | Computation 1 | Computation 2 |
| 1 | _h = time * H_MUL | _m = time * M_MUL |
| 2 | mul-latency | mul-latency |
| 3 | mul-latency | mul-latency |
| 4 | _n = u64(_h) * 60 | _s = u64(_m) * 60 |
| 5 | mul-latency | mul-latency |
| 6 | mul-latency | mul-latency |
| The results are in the upper 64-bits which can be accessed for "free": hour = _h>>64 minute = _n>>64 second = _s>>64 | ||
Note that the constants come from the following:
H_MUL = ((1 << 32) / 3600 + 1) << 32M_MUL = ((1 << 32) / 60 + 1) << 32On most targets, this compiles to nothing but four multiplications.
If you don't mind the math, this is likely the best option for most date libraries.
This isn't really quite Lemire's approach, as the remainder from hour is not measured in the same units as the hour was divided, but it's similar.
Multiplying the low-bits by 60 is perhaps on first glance a strange thing to do. The reason this works, is because the low 32-bits can be thought of as like a percentage progression between whole-integer steps in the high half. For example: 0x00000000 represents 0% of an hour having passed, and 0xFFFFFFFF represents the final moments of an hour. Multiplying this by 60 is like taking the percentage of time elapsed, multiplying, and rounding down.
This is very low latency, and optimal throughput on most chips.
We have got it all the way down to a 6-cycle latency. How could it possibly be made one cycle less...?
Finally, to fulfill the promise at the start of this article (overall latency faster than two-muls), we can utilise the identity from the previous weekday article for a final speed boost:
In this case, setting c = 4 gives us the useful version where the mod becomes % 64:
time — seconds ∈ [0‥2257198]626:59:58u32 tmin = (u64) time * 71582789 >> 32 // = time / 60; "Total Minutes"
u32 hour = (u64) time * 1193047 >> 32 // = time / 3600
u32 second = (time + 4 * tmin) % 64;u32 minute = (tmin + 4 * hour) % 64;Implementation note: If you are using signed integers for whatever reason, change % 64 to & 63 to maintain max-speed (% requires a sign correction for negative inputs).
Once tmin and hour are available, the part(x + 4 * y) executes in a single cycle on both x86 and 32-bit ARM, due to:
lea eax, [ebx + 4*ecx]ADD Rd, Rn, Rm, LSL #2| Step | Computation 1 | Computation 2 |
|---|---|---|
| 1 | _t = time * 71582789 | _h = time * 1193047 |
| 2 | mul-latency | mul-latency |
| 3 | mul-latency | mul-latency |
| 4 | s = time + ((_t>>32) << 2) | m = (_t >> 32) + ((_h>>32) << 2) |
| 5 | second = s & 63 | minute = m & 63 |
Total latency of only five cycles! The result for hour is in the upper 32-bits of _hwhich can be accessed for "free": hour = _h>>32 | ||
time — seconds ∈ [0‥2257198]626:59:58u64 const M_MUL = (((u64) 1 << 32) / 60 + 1) << 32
u64 const H_MUL = (((u64) 1 << 32) / 3600 + 1) << 32
u64 tmin = (u128) time * M_MUL >> 64 // = time / 60; "Total Minutes"
u32 hour = (u128) time * H_MUL >> 64 // = time / 3600
u32 second = (time + (tmin << 2)) & 63;u32 minute = (tmin + (hour << 2)) & 63;Implementation note: If you are using signed integers for whatever reason, change % 64 to & 63 to maintain max-speed (% requires a sign correction for negative inputs).
Once tmin and hour are available, the part(x + 4 * y) executes in a single cycle on both x86 and 32-bit ARM, due to:
lea eax, [ebx + 4*ecx]ADD Rd, Rn, Rm, LSL #2| Step | Computation 1 | Computation 2 |
|---|---|---|
| 1 | _t = time * M_MUL | _h = time * H_MUL |
| 2 | mul-latency | mul-latency |
| 3 | mul-latency | mul-latency |
| 4 | s = time + ((_t>>64) << 2) | m = (_t >> 64) + ((_h>>64) << 2) |
| 5 | second = s & 63 | minute = m & 63 |
Total latency of only five cycles! The result for hour is in the upper 64-bits of _hwhich can be accessed for "free": hour = _h>>64 | ||
So, this is a lower latency algorithm than the previous one, but more operations overall, and thus worse throughput. If you are open to such low-level tricks in your codebase, then it's worth benchmarking this against the previous version to see whether latency or throughput matters for you.
Note, an alternative computation for second is (mlow >> 26) - (mlow >> 30), where mlow is the low 32-bits from the wide result calculation for tmin. It is slightly faster in the 32-bit version for ARM64, but slower on x86 (a bit niche, so this is not itemised in the benchmarks).
The technique of breaking the dependency chain extends to sub-second units.
The below is a very low-latency / high throughput way to extract time with milliseconds as a separate field.
u32 time (measured in milliseconds) - Full rangeu32 tsec = time / 1000 // = (u64) time * 274877907 >> 38
u32 tmin = time / 60000 // = (u64) time * 1172812403 >> 46
u32 hour = time / 3600000 // = (u64) time * 2501999793 >> 53
u32 milli = time - tsec * 1000;u32 second = tsec - tmin * 60;u32 minute = tmin - hour * 60;The millisecond approach can be modified to use the "Base-64 Clock Trick", but it cannot be adapted to the "Fixed-Point Hi/Low Bits" technique without widening to 64-bit. Staying within 32-bit math will be best for auto-vectorisation.
Examples for deciseconds, centiseconds, and nanoseconds are available in the testcase repository .
The algorithms presented here are appropriate for Unix time (i.e. no leap seconds).
Leap seconds themselves probably warrant an entire new blog post, so I'll keep this section brief. In short, some computer systems need to be able to handle one extra "leap second" at the end of a day. It resolves as 23:59:60, which doesn't flow naturally from the math (24:00:00 being more natural).
Leap seconds are scheduled to be phased out by 2035, but that won't mean we can delete all our leap second code any time soon.
time [0..86400] (leap sec time) — Then:u32 is_leap = (time == 86400)hms hms = getHMS(time - is_leap)
hms.second += is_leap
The code above is a simple branch-free way to account for leap seconds (in the UTC+0 timezone), but what if you don't want to pay the three-cycle penalty? Well, this alternative below achieves the same thing:
time [0..86400] (leap sec time) — Then:u32 tinc = time + 1u32 tmin = ((u64) tinc * 143163919) >> 33
u32 hour = ((u64) tinc * 2386065) >> 33
u32 second = (time + tmin * 4) % 64u32 minute = (tmin + hour * 4) % 64This abuses errors in the fixed-point math, to have the first "error" line up exactly where we want, on that final leap second.
A round-down multiplier is used, with a pre-increment, as that is the type of adjustment that yields an eventual long-minute and long-hour.
While I do not have a formal proof of the above math, it is exhaustively checked over a day's range in the benchmark codebase.
As with any leap-second implementation, this is backwards-compatible with regular time, as well as the circumstances that could arise where negative leap seconds are required (despite leap seconds being phased out, this might actually happen soon!).
The code examples throughout this article have been targeting scalar performance. SIMD can give a further performance improvement, however SIMD optimisations are highly platform-dependent. In general, the 32-bit versions are going to perform better than 64-bit on SIMD, because that usually gives twice as many "lanes".
On x86 (AVX-512): "New v1" and "New v3" seem to give the best x86 SIMD performance.
On ARM Neon SIMD targets (incl. Apple M-Series chips), we can get better SIMD throughput with a modified version of "Traditional Approach 1" with the following (no divisions, no high 32-bit results):
time — seconds ∈ [0‥115199]31:59:59u32 hour = time * 37283 >> 27
u32 tsec = time - hour * 3600
u32 mins = tsec * 2185 >> 17
u32 secs = tsec - mins * 60
The reason this wins out on ARM NEON is because its 128 bit SIMD implementation can only maintain four 32-bit lanes in cases where there's no 64-bit widened result.
By using smaller multipliers and shifts, we can make sure we never need the upper 32-bits.
On x86, this benches a lot slower than the previous ones, so it is very much platform and usecase specific.
So, we have explored a variety of approaches, and as is often the case: "Which one is best? It depends".
If you want clean readable math, choose "V1".
If you want high throughput and near-minimal latency, choose "V2 - Fixed-Point Hi/Low Bits".
If you want even lower latency, and don't mind crazy math tricks, choose "V3 - A Base-64 Clock Trick".
If you are designing an SIMD database engine, consider V1, V3 or "ARM Neon" depending on your target.
Or, if you aren't concerned about the performance, just keep what you already have!
But at the very least, use unsigned integers. Signed division and modulus will cost you more speed than you probably realise.
These articles are not just for finding the fastest code, but exploring ideas, which will hopefully translate to other areas of development.
In an ideal world, the compiler would make us not have to worry about all this.
It is only a tiny mathematical leap to go from the "Traditional Approaches" to the "New V1", and with a compiler hint about valid input ranges, the compiler could then apply the "Hi/Low Bits" logic for us where it helps. Alas, compilers are not as smart as people think, so we'll just have to keep on going.
There are many date/time related articles in the pipeline (will it ever end?); the next date/time article will be about Easter. However, the very next blog post will be taking on a completely different topic: presenting a new class of map projections I've developed, which are equal area and optimised for interactive computer use (topical given the UN just voted on this).
If you would like to be notified when these articles come out, follow me on X .
Revision history:
Exhaustive tests of the ranges asserted in this article can be verified via the testcase code: https://github.com/benjoffe/fast-world-calendars .
The same repository is used for benchmarking, where a large set of random dates are loaded into memory and tested. Latency mode feeds the results into the next computation via bitwise-XOR to ensure no parallel execution:
git clone git@github.com:benjoffe/fast-world-calendars.git cd fast-world-calendars cmake -B build -DCMAKE_BUILD_TYPE=Release cmake --build build -j ./build/bin/time_test ./build/bin/time_bench ./build/bin/time_bench -latency ./build/bin/time_bench -batch ./build/bin/time_bench -vector
| Algorithm: | Scan | Traditional 1 | Traditional 1 Fixed-Pt | Traditional 2 | Traditional 3 ("%" ver.) | Cassio Neri | New v1 | New v1 Fixed-Pt | New v2 | New v2 64-Bit | New v3 | New v3 64-Bit | New Neon SIMD |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| MacBook Pro 2024 (MacOS 15.6.1) Apple M4 Pro Compiler: Apple clang 17.0.0 | - (14637) | 1.29x (391924) | 1.00x (306797) | 0.96x (294294) | 0.96x (294388) | 1.00x (306567) | 0.65x (204351) | 0.75x (234143) | 0.74x (231124) | 0.64x (201383) | 0.70x (218114) | 0.60x (190581) | 1.03x (316149) |
| AMD Ryzen 9 9950X3D Ubuntu 24.04.3 LTS Compiler: Clang 18.1.3 (x86_64) | - (11870) | 1.13x (226549) | 1.04x (209129) | 0.79x (161322) | 0.79x (161256) | 1.00x (201103) | 0.61x (126751) | 0.61x (126690) | 0.62x (130097) | 0.49x (104161) | 0.69x (141656) | 0.49x (104602) | 1.07x (214875) |
| AMD Ryzen 9 9950X3D Ubuntu 24.04.3 LTS Compiler: GCC 14.2.0 (x86_64) | - (6052) | 1.31x (218506) | 1.36x (226924) | 1.03x (172973) | 1.04x (174628) | 1.00x (167913) | 0.74x (126513) | 0.77x (131477) | 0.69x (117053) | 0.60x (103541) | 0.63x (107772) | 0.60x (103540) | 1.36x (225815) |
| Raspberry Pi Zero 32-Bit ARM Raspbian GNU/Linux 13 (trixie) Compiler: GCC 12.2.0 (armv6l) | - (5134) | 1.43x (39040) | 1.17x (32893) | 1.17x (32903) | 1.17x (32886) | 1.00x (28772) | 1.04x (29801) | 0.78x (23652) | 0.79x (23819) | 0.88x (25871) | 0.70x (21600) | 0.70x (21623) | 1.26x (34953) |
| Algorithm: | Scan | Traditional 1 | Traditional 1 Fixed-Pt | Traditional 2 | Traditional 3 ("%" ver.) | Cassio Neri | New v1 | New v1 Fixed-Pt | New v2 | New v2 64-Bit | New v3 | New v3 64-Bit | New Neon SIMD |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| MacBook Pro 2024 (MacOS 15.6.1) Apple M4 Pro Compiler: Apple clang 17.0.0 | - (14661) | 1.13x (37567) | 0.98x (34560) | 1.10x (37053) | 1.10x (37035) | 1.00x (34994) | 0.71x (29078) | 0.76x (30159) | 0.65x (27778) | 0.57x (26251) | 0.97x (34297) | 0.68x (28494) | 0.83x (31533) |
| AMD Ryzen 9 9950X3D Ubuntu 24.04.3 LTS Compiler: Clang 18.1.3 (x86_64) | - (8023) | 1.10x (56596) | 0.98x (51542) | 1.06x (54766) | 1.09x (56347) | 1.00x (52310) | 0.89x (47576) | 1.07x (55341) | 0.72x (39866) | 0.44x (27644) | 0.96x (50725) | 0.87x (46696) | 1.13x (58052) |
| AMD Ryzen 9 9950X3D Ubuntu 24.04.3 LTS Compiler: GCC 14.2.0 (x86_64) | - (11662) | 1.01x (49311) | 0.90x (44982) | 1.05x (50774) | 0.87x (44001) | 1.00x (48815) | 0.87x (44027) | 0.83x (42539) | 0.82x (41955) | 0.51x (30775) | 0.78x (40802) | 0.83x (42364) | 0.89x (44783) |
| Raspberry Pi Zero 32-Bit ARM Raspbian GNU/Linux 13 (trixie) Compiler: GCC 12.2.0 (armv6l) | - (9594) | 1.42x (36715) | 1.13x (31101) | 1.40x (36196) | 1.40x (36186) | 1.00x (28656) | 0.80x (24898) | 0.98x (28347) | 0.74x (23619) | 0.74x (23614) | 0.98x (28341) | 0.98x (28350) | 1.24x (33141) |
Note: "vector mode" is only benched for GCC, as Clang refused to vectorise any of the loops in the benchmark. As SIMD / Vector is just a side-point of this article, I have not focused on figuring out why. I'm open to a pull request though if anybody wants to mess around with it.
Vector performance is highly platform dependent:
| Algorithm: | Scan | Traditional 1 | Traditional 1 Fixed-Pt | Traditional 2 | Traditional 3 ("%" ver.) | Cassio Neri | New v1 | New v1 Fixed-Pt | New v2 | New v2 64-Bit | New v3 | New v3 64-Bit | New Neon SIMD |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| MacBook Pro 2024 (MacOS 15.6.1) Apple M4 Pro Compiler: GCC 15.2.0 | - (8191) | 1.00x (13546) | 0.63x (11562) | 1.83x (17976) | 1.08x (13977) | 6.18x (41268) | 1.00x (13532) | 0.62x (11505) | 2.15x (19688) | 3.73x (28191) | 1.38x (15582) | 3.77x (28384) | 0.25x (9528) |
| AMD Ryzen 9 9950X3D Ubuntu 24.04.3 LTS Compiler: GCC 14.2.0 (x86_64) | - (1435) | 1.00x (22071) | 1.35x (29213) | 0.33x (8204) | 0.31x (7928) | 0.44x (10594) | 0.26x (6880) | 0.32x (8124) | 0.39x (9565) | 1.39x (30031) | 0.28x (7283) | 2.26x (48159) | 0.80x (17919) |
| Algorithm: | Scan | Traditional MS 1 | Traditional MS 2 | New MS |
|---|---|---|---|---|
| MacBook Pro 2024 (MacOS 15.6.1) Apple M4 Pro Compiler: Apple clang 17.0.0 | - (14637) | 1.00x (380584) | 0.75x (290332) | 0.63x (245017) |
| AMD Ryzen 9 9950X3D Ubuntu 24.04.3 LTS Compiler: Clang 18.1.3 (x86_64) | - (11870) | 1.00x (274423) | 0.65x (182796) | 0.48x (138735) |
| AMD Ryzen 9 9950X3D Ubuntu 24.04.3 LTS Compiler: GCC 14.2.0 (x86_64) | - (6052) | 1.00x (264536) | 0.72x (192688) | 0.55x (147219) |
| Raspberry Pi Zero 32-Bit ARM Raspbian GNU/Linux 13 (trixie) Compiler: GCC 12.2.0 (armv6l) | - (5134) | 1.00x (46247) | 1.05x (48290) | 1.02x (47264) |
| Algorithm: | Scan | Traditional MS 1 | Traditional MS 2 | New MS |
|---|---|---|---|---|
| MacBook Pro 2024 (MacOS 15.6.1) Apple M4 Pro Compiler: Apple clang 17.0.0 | - (14661) | 1.00x (57063) | 0.95x (55051) | 0.67x (43048) |
| AMD Ryzen 9 9950X3D Ubuntu 24.04.3 LTS Compiler: Clang 18.1.3 (x86_64) | - (8023) | 1.00x (85539) | 0.93x (79821) | 0.75x (65925) |
| AMD Ryzen 9 9950X3D Ubuntu 24.04.3 LTS Compiler: GCC 14.2.0 (x86_64) | - (11662) | 1.00x (68206) | 1.00x (68025) | 0.84x (59063) |
| Raspberry Pi Zero 32-Bit ARM Raspbian GNU/Linux 13 (trixie) Compiler: GCC 12.2.0 (armv6l) | - (9594) | 1.00x (47279) | 0.94x (45205) | 0.70x (35832) |
| Algorithm: | Scan | Traditional #1 Standard Branchless | Traditional #2 Standard Branchless | V2 Standard Branchless | V2 Leap Hack |
|---|---|---|---|---|---|
| MacBook Pro 2024 (MacOS 15.6.1) Apple M4 Pro Compiler: Apple clang 17.0.0 | - (14637) | 1.00x (403547) | 0.75x (306471) | 0.64x (262256) | 0.55x (227264) |
| AMD Ryzen 9 9950X3D Ubuntu 24.04.3 LTS Compiler: Clang 18.1.3 (x86_64) | - (11870) | 1.00x (274451) | 0.73x (203188) | 0.59x (167219) | 0.41x (119667) |
| AMD Ryzen 9 9950X3D Ubuntu 24.04.3 LTS Compiler: GCC 14.2.0 (x86_64) | - (6052) | 1.00x (265479) | 0.78x (207161) | 0.62x (167743) | 0.44x (119632) |
| Raspberry Pi Zero 32-Bit ARM Raspbian GNU/Linux 13 (trixie) Compiler: GCC 12.2.0 (armv6l) | - (5134) | 1.00x (44174) | 1.08x (47263) | 0.74x (33920) | 0.71x (32878) |
| Algorithm: | Scan | Traditional #1 Standard Branchless | Traditional #2 Standard Branchless | V2 Standard Branchless | V2 Leap Hack |
|---|---|---|---|---|---|
| MacBook Pro 2024 (MacOS 15.6.1) Apple M4 Pro Compiler: Apple clang 17.0.0 | - (14661) | 1.00x (51323) | 0.91x (47956) | 0.68x (39687) | 0.48x (32196) |
| AMD Ryzen 9 9950X3D Ubuntu 24.04.3 LTS Compiler: Clang 18.1.3 (x86_64) | - (8023) | 1.00x (73326) | 0.98x (71842) | 0.73x (55623) | 0.70x (53444) |
| AMD Ryzen 9 9950X3D Ubuntu 24.04.3 LTS Compiler: GCC 14.2.0 (x86_64) | - (11662) | 1.00x (65309) | 0.95x (62612) | 0.78x (53771) | 0.61x (44165) |
| Raspberry Pi Zero 32-Bit ARM Raspbian GNU/Linux 13 (trixie) Compiler: GCC 12.2.0 (armv6l) | - (9594) | 1.00x (41913) | 0.89x (38269) | 0.63x (30087) | 0.51x (25945) |