A faster way to convert a timestamp ➜ Hour, Min, Sec

50% faster than usual, by breaking the dependency chain

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):

  1. A non-obvious reordering of the calculation steps, breaking the dependency chain.
  2. Optional math tricks, including converting a base-60 clock into binary-friendly base-64.

Even if you don't like low-level bit hacks in your code,
there will be something here for you.


Approx. Relative Speed of Fastest 32-Bit AlgorithmsAs tested on AMD Ryzen 9
and Apple M4 Pro processors
(smaller numbers = faster)Approx guide, latency and throughput
differ per platform. See benchmark section
for specific results.

“Traditional Approaches” ~1-1.3×
Neri
(2020)
New “V1”
(Simple Math)
~¾×
New “V2”
(Fancy Math)
~×

Traditional Approach 1 Deep link

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

The intuitive sequential approachGiven: time (positive integer measured in seconds)  —  Then:
  1. hour   = time / 3600
  2. rem    = time % 3600 // Compiles to: time - hour * 3600
  3. minute = rem / 60
  4. second = 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.

Stepx86 steps
1_h = time * 2443359173
2mul-latency
3mul-latency
4hour = _h >> 43
5r = hour * 3600
6mul-latency
7mul-latency
8rem = time - r
9_m = rem * 2290649225
10mul-latency
11mul-latency
12minute = _m >> 37
13s = minute * 60
14mul-latency
15mul-latency
16second = 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.

Traditional Approach 2 Deep link

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 :

The "three separate components" approach
  1. hour   = time / 3600
  2. minute = (time / 60) % 60
  3. second = time % 60

This 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
StepMinute Comp.Hour & Second Comp.
1_tm = time * 2290649225_h = time * 2443359173
2mul-latencymul-latency
3mul-latencymul-latency
4total_min = _tm >> 37hour = _h >> 43
5_m = total_min * 2290649225s = total_min * 60
6mul-latencymul-latency
7mul-latencymul-latency
8a = _m >> 37second = time - s
9b = a * 60
10mul-latency
11mul-latency
12minute = 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.

Optimisation: Fixed-Point Math Deep link

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.

Standard division
Full-range
  1. u32 a = x / 60
  2. u32 b = x / 3600
 
Fixed-point approach
Restricted-range
  1. u32 a = (u64) x * 71582789 >> 32
  2. u32 b = (u64) x * 1193047 >> 32
Green colour denotes operations that are "free" on x86 / 32-bit targets.

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).

Neri's Approach - 2020 Deep link

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.

Cassio Neri - 2020Given: timeseconds ∈ [0‥97612918]
Max return-value: 27114:41:58
  1. u64 p1     = (u64) time * 71582789
  2. u32 tmin   = p1 >> 32
  3. u32 second = u32(p1) / 71582789
  4. u64 p2     = (u64) tmin * 71582789;
  5. u32 hour   = p2 >> 32
  6. 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
StepComputation 1Computation 2
1p1 = (u64) time * 71582789
2mul-latency
3mul-latency
4p2 = (u64) (p1>>32) * 71582789_s = (u64) u32(p1) * 4026531799
5mul-latencymul-latency
6mul-latencymul-latency
7_m = (u64) u32(p2) * 4026531799second = _s >> 58
8mul-latency
9mul-latency
10minute = _m >> 58
The result for hour is in the upper 32-bits of p2
which 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...

New Approach - V1 - Breaking the dependency chain Deep link

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:

New The Unobvious parallel approach Given: timeseconds
Range: Full
  1. tmin   = time / 60 // "Total Minutes"
  2. hour   = time / 3600
  3. second = time - tmin * 60;
  4. 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
StepComputation 1Computation 2
1_tm = time * 2290649225_h = time * 2443359173
2mul-latencymul-latency
3mul-latencymul-latency
4tmin = _tm >> 37hour = _h >> 43
5s = tmin * 60m = hour * 60
6mul-latencymul-latency
7mul-latencymul-latency
8second = time - sminute = tmin - m
New The Unobvious parallel approach (fixed-point)Given: timeseconds ∈ [0‥2257198]
Max valid return-value: 626:59:58
  1. u32 tmin   = (u64) time * 71582789 >> 32
  2. u32 hour   = (u64) time * 1193047 >> 32
  3. u32 second = time - tmin * 60;
  4. 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
StepComputation 1Computation 2
1_tm = time * 71582789  _h = time * 1193047   
2mul-latencymul-latency
3mul-latencymul-latency
4s = (_tm >> 32) * 60m = (_h >> 32) * 60
5mul-latencymul-latency
6mul-latencymul-latency
7second = time - sminute = (_tm >> 32) - m
The result for hour is in the upper 32-bits of _h
which can be accessed for "free": hour = _h>>32

This approach moves the two divisions to the start:

  • The computation of tmin and hour can occur simultaneously.
  • Once they complete, computation of 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!

Traditional Approach 3 Deep link

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:

The backwards decomposition approachGiven: time (positive integer measured in seconds)  —  Then:
  1. tmin   = time / 60
  2. second = time % 60
  3. hour   = tmin / 60
  4. minute = tmin % 60

The 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...

New Approach - V2 - Fixed-Point Hi/Low Bits Deep link

Using the new structure from "V1", and combining with something like Lemire's high + low bit technique, we get this very compact function:

New Parallel approach + Fixed-Point Hi/Low Bits Given: timeseconds ∈ [0‥2255818]
Max return-value: 626:36:58
  1. u64 hprd = (u64) time * 1193047
  2. u32 mlow = time * 71582789
  3. u32 hlow = (u32) hprd
  4.  
  5. u32 hour   = hprd >> 32
  6. u32 minute = (u64) hlow * 60 >> 32
  7. u32 second = (u64) mlow * 60 >> 32
Two compact chains
StepComputation 1Computation 2
1_h = time * 1193047_m = time * 71582789
2mul-latencymul-latency
3mul-latencymul-latency
4_n = u32(_h) * 60_s = u32(_m) * 60
5mul-latencymul-latency
6mul-latencymul-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 + 1
  • 71582789 = (1 << 32) / 60 + 1
New Parallel approach + Fixed-Point Hi/Low Bits (64-bit)Given: timeseconds ∈ [0‥2255818]
Max return-value: 626:36:58
  1. u64 M_MUL = (((u64) 1 << 32) / 60 + 1) << 32
  2. u64 H_MUL = (((u64) 1 << 32) / 3600 + 1) << 32
  3.  
  4. u128 hprd = (u128) time * H_MUL
  5. u64 mlow = time * M_MUL
  6. u64 hlow = (u64) hprd
  7.  
  8. u32 hour   = hprd >> 64
  9. u32 minute = (u128) hlow * 60 >> 64
  10. u32 second = (u128) mlow * 60 >> 64
Two compact chains
StepComputation 1Computation 2
1_h = time * H_MUL_m = time * M_MUL
2mul-latencymul-latency
3mul-latencymul-latency
4_n = u64(_h) * 60_s = u64(_m) * 60
5mul-latencymul-latency
6mul-latencymul-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) << 32
  • M_MUL = ((1 << 32) / 60 + 1) << 32

On 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...?

New Approach - V3 - A Base-64 Clock Trick Deep link

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:

xmodD=(x+cxD)mod(D+c)

In this case, setting c = 4 gives us the useful version where the mod becomes % 64:

New The very unintuitive modulus-padding approach Given: timeseconds ∈ [0‥2257198]
Max return-value: 626:59:58
  1. u32 tmin   = (u64) time * 71582789 >> 32  // = time / 60; "Total Minutes"
  2. u32 hour   = (u64) time * 1193047 >> 32   // = time / 3600
  3. u32 second = (time + 4 * tmin) % 64;
  4. u32 minute = (tmin + 4 * hour) % 64;
Base-60 people hate this one simple trick!

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:

  • x86 uses lea eax, [ebx + 4*ecx]
  • ARM uses ADD Rd, Rn, Rm, LSL #2
StepComputation 1Computation 2
1_t = time * 71582789_h = time * 1193047
2mul-latencymul-latency
3mul-latencymul-latency
4s = time + ((_t>>32) << 2)m = (_t >> 32) + ((_h>>32) << 2)
5second = s & 63minute = m & 63
Total latency of only five cycles! The result for hour is in the upper 32-bits of _h
which can be accessed for "free": hour = _h>>32
New The very unintuitive modulus-padding approach (64-bit)Given: timeseconds ∈ [0‥2257198]
Max return-value: 626:59:58
  1. u64 const M_MUL = (((u64) 1 << 32) / 60 + 1) << 32
  2. u64 const H_MUL = (((u64) 1 << 32) / 3600 + 1) << 32
  3.  
  4. u64 tmin   = (u128) time * M_MUL >> 64  // = time / 60; "Total Minutes"
  5. u32 hour   = (u128) time * H_MUL >> 64   // = time / 3600
  6. u32 second = (time + (tmin << 2)) & 63;
  7. u32 minute = (tmin + (hour << 2)) & 63;
Base-60 people hate this one simple trick!

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:

  • x86 uses lea eax, [ebx + 4*ecx]
  • ARM uses ADD Rd, Rn, Rm, LSL #2
StepComputation 1Computation 2
1_t = time * M_MUL_h = time * H_MUL
2mul-latencymul-latency
3mul-latencymul-latency
4s = time + ((_t>>64) << 2)m = (_t >> 64) + ((_h>>64) << 2)
5second = s & 63minute = m & 63
Total latency of only five cycles! The result for hour is in the upper 64-bits of _h
which 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).

Sub-Second Units Deep link

Millisecond timestamps

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.

New Parallel approach: Time + MillisecondsGiven: u32 time (measured in milliseconds) - Full range
  1. u32 tsec = time / 1000     // = (u64) time * 274877907  >> 38
  2. u32 tmin = time / 60000    // = (u64) time * 1172812403 >> 46
  3. u32 hour = time / 3600000  // = (u64) time * 2501999793 >> 53
  4. u32 milli  = time - tsec * 1000;
  5. u32 second = tsec - tmin * 60;
  6. 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 .

Bonus: Note on Leap Seconds Deep link

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.

Simple Leap Second TimeGiven: time [0..86400] (leap sec time)  —  Then:
  1. u32 is_leap = (time == 86400)
  2. hms hms = getHMS(time - is_leap)
  3. 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:

Leap Second HackGiven: time [0..86400] (leap sec time)  —  Then:
  1. u32 tinc = time + 1
  2. u32 tmin = ((u64) tinc * 143163919) >> 33
  3. u32 hour = ((u64) tinc * 2386065) >> 33
  4. u32 second = (time + tmin * 4) % 64
  5. u32 minute = (tmin + hour * 4) % 64

This 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!).

Bonus: Platform-Specific ARM Neon SIMD Optimisation Deep link

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):

New ARM Neon SIMD Throughput Optimised VersionGiven: timeseconds ∈ [0‥115199]
Max return-value: 31:59:59
  1. u32 hour = time * 37283 >> 27
  2. u32 tsec = time - hour * 3600
  3. u32 mins = tsec * 2185 >> 17
  4. 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.

Closing Thoughts Deep link

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 .


Appendix: Benchmark Results Deep link

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

H:M:S Latency

Algorithm:ScanTraditional 1Traditional 1 Fixed-PtTraditional 2Traditional 3 ("%" ver.)Cassio NeriNew v1New v1 Fixed-PtNew v2New v2 64-BitNew v3New v3 64-BitNew 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)

H:M:S Throughput

Algorithm:ScanTraditional 1Traditional 1 Fixed-PtTraditional 2Traditional 3 ("%" ver.)Cassio NeriNew v1New v1 Fixed-PtNew v2New v2 64-BitNew v3New v3 64-BitNew 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)

H:M:S Vector

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:

  • "New Neon SIMD" wins on the Macbook (128-bit NEON)
  • "New v1 and v3" win on the AMD Ryzen (512-bit AVX-512)
  • Neri's approach was optimised for scalar performance. The large number here on ARM is almost certainly due to the compiler failing to vectorise it properly.
Algorithm:ScanTraditional 1Traditional 1 Fixed-PtTraditional 2Traditional 3 ("%" ver.)Cassio NeriNew v1New v1 Fixed-PtNew v2New v2 64-BitNew v3New v3 64-BitNew 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)

Millisecond Latency

Algorithm:ScanTraditional
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)

Millisecond Throughput

Algorithm:ScanTraditional
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)

H:M:S Leap-Second Latency

Algorithm:ScanTraditional #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)

H:M:S Leap-Second Throughput

Algorithm:ScanTraditional #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)