[PATCH v3 0/1] offtime: optimized calculation of year from unix

Gary Gende gary@garygende.com
Mon Sep 1 10:01:21 GMT 2025


Updatting this patch to account for feedback and incorporate new refinements.


CHANGES:

* Now calculates the year starting at the first 400-year leap year cycle
  that is below the minimum year allowed in tm->tm_year (-2147483999).  This
  allows assuming that all year calculations will be positive (i.e. adding
  years rather than subtracting).  This enabled streamlining and improved
  performance.
* Checks if MIN is defined and, if not, defines it rather than defining an
  entirely new MACRO
* Added additional comments to explain the MACROs and other unintuitive code
  (e.g. why start at year -2147483999)
* Fix spelling and styling issues
* Update the patch subject line so it refers to the correct file (:whoops:!)


REGRESSION TESTS:

* Passes all make check tests other than elf/tst-shstk-legacy-1g,
  which seems to be a kernel/hardware issue.
  (ref: https://sourceware.org/bugzilla/show_bug.cgi?id=31877)


UPDATED PERFORMANCE BENCHMARKS:

* All times are the median time of five runs
* All tests run the entire __offset function, not just the year calculation
 ** minor changes were made to the __offset function to allow execution in
    relative isolation from the rest of the glibc library
* All test runs were given a random number number from an array
  of random numbers generated before each test
 ** Array size was 100, 10000, or 1000000, as noted in the table
 ** Random number was either "rand()" or "(rand() - rand()) * rand()"
  *** Updated "(rand() - rand()) * rand()" benchmark to correctly produce
      64-bit inputs that do not go outside struct tm's max/min values
* Tests were compiled with either -O0 or -O3 optimization flag, as noted
* "Current" is execution time, in seconds, for current algorithm
* "Proposed" is execution time, in seconds, for (new) proposed algorithm


 -- Tests on 9800X3D, 32GB RAM, Linux Mint 22.1, 150M loops per test --

Array Size	Value	Optimize	Current(s)	Proposed(s)	Relative Time
100	        rand()	    0		3.673038	2.259116	61.5%
100	        rand()  	3		1.954789	1.350536	69.1%
100	        (r-r)*r 	0		7.123674	2.270692	31.9%
100	        (r-r)*r	    3		3.961592	1.374237	34.7%
10000	    rand()	    0		3.896807	2.588002	66.4%
10000   	rand()	    3		2.112533	1.372340	65.0%
10000   	(r-r)*r	    0		8.391101	3.171792	37.8%
10000   	(r-r)*r	    3		4.948121	1.749075	35.3%
1000000	    rand()	    0		4.749969	3.463198	72.9%
1000000	    rand()	    3		3.151351	3.055907	97.0%
1000000	    (r-r)*r	    0		8.995366	4.123248	45.8%
1000000	    (r-r)*r	    3		5.595650	3.585664	64.1%


Special Case Tests:
 - "neg" = "rand() * -1" // Tests negative values specifically
 - "massive" - removed because corrected (r-r)*r test spends significant time
   time testing massive numbers, making this special case unecessary
 - "big (2600s)" = "20000000000 + rand()" // 20000000000 = 10/11/2603
 - "big (2100s)" = "5000000000 + rand()" // 5000000000 = 6/11/2128
 - "1970" = "rand() % (365 * SECS_PER_DAY)"
  -- Chosen to show current algorithm's advantage in the year 1970, where it is
     able to bypass the year calculation completely

Array Size	Value	Optimize	Current(s)	Proposed(s)	Relative Time
1000000	    neg	        3		3.122325	3.074963	98.5%
1000000	    massive	    *redundant with corrected (r-r)*r benchmarks*
1000000	    big (2600s)	3		3.830318	3.025371	79.0%
1000000	    big (2100s)	3		3.227695	3.023615	93.7%
1000000	    1970	    3		1.968458	2.891870	146.9%


 -- Tests on i3-2120T, 8GB RAM, WSL2 on Windows 10, 50M loops per test --

NOTE: Reported times only had 1/64 sec precision. Tests are still accurate,
      just very low precision.

Array Size	Value	Optimize	Current(s)	Proposed(s)	Relative Time
100	        rand()	    0		5.5625  	4.625	    83%
100	        rand()	    3		2.5625	    1.8125	    71%
100	        (r-r)*r	    0		10.765625	4.640625	43%
100	        (r-r)*r	    3		4.84375	    1.796875	37%
10000	    rand()	    0		6.4375	    5.390625	84%
10000   	rand()	    3		3.125	    2.546875	82%
10000   	(r-r)*r	    0		11.953125	5.71875	    48%
10000   	(r-r)*r	    3		5.640625	2.8125	    50%
1000000	    rand()	    0		6.46875	    5.421875	84%
1000000	    rand()	    3		3.15625	    2.5625	    81%
1000000	    (r-r)*r	    0		12.0        5.765625	48%
1000000	    (r-r)*r	    3		5.671875	2.859375	50%

Array Size	Value	Optimize	Current(s)	Proposed(s)	Relative Time
1000000	    neg	        3		3.15625	    2.625	    83%
1000000	    massive	    *redundant with corrected (r-r)*r benchmarks*
1000000	    big (2600s)	3		3.796875	2.625	    69%
1000000	    big (2100s)	3		3.296875	2.59375	    79%
1000000	    1970	    3		1.90625	    2.4375	    128%


 -- Tests on 9800X3D, 32GB RAM, WSL2 in Windows 10, 150M loops per test --

Array Size	Value	Optimize	Current(s)	Proposed(s)	Relative Time
100	        rand()	    3		1.991614	1.366046	68.6%
10000	    rand()	    3		2.103214	1.537606	73.1%
1000000	    rand()	    3		3.128251	3.088647	98.7%



RESPONSE TO OTHER COMMENTS:

* In response to:
> Also, why not use sys/param.h's MIN instead of reinventing the wheel?

My thought on this was to avoid producing additional dependency chains where it's not necessary.  I've changed it to check if MIN is already defined, and to define it if it's not.  This solution avoids dependencies without defining a new unnecessary MACRO.


* In response to:
> +  y += MIN_YEAR_GROUP_NUM(days / DIVISOR_100, 3) * 100;
> +  days -= MIN_YEAR_GROUP_NUM(days / DIVISOR_100, 3) * DIVISOR_100;
>
> This looks like it calculates the wrong number if days / DIVISOR_100 == -4. Similarly for the other use of MIN_YEAR_GROUP_NUM.

AND

> Why not start at 1 instead of at 1601?

Short Version:
Correcting for "days / DIVISOR_100 == -4" produces inaccurate results.  1601 was mostly arbitrary, and, as noted above, I've now found a good non-arbitrary reason to make it something else.

Long Version:
These seemingly innocent comments started me down a deep rabbit hole.  When creating v1 of this patch submission, I originally tried to account for situations where (days / DIVISOR_100) == -4, but doing so would produce inaccurate results. I didn't fully understand why, but my tests (including a test that compares several hundred million random inputs to the results of the current __offtime function) all consistently indicated that "correcting" these cases was actually incorrect.  However, the comments above made me consider two things: 1) I should probably actually figure out why, and 2) since it seemed that there was a performance benefit with my algorithm when processing negative inputs, maybe I should set the starting year to something exceptionally high so all inputs would effectively be negative (note: I've since settled on the opposite of that).

I wasn't able to find any promising leads for #1, so I started with #2.  It turned out that setting the starting year to anything more than 1970 (even 2001), created significant issues.  This didn't make sense at first because setting the starting year to 1601 and inputting large negative numbers (i.e. years before 1601) worked fine, but when the starting year was after 1970, suddenly negative numbers could be a problem.  I tried manually adjusting the results in different ways to see if I could fix it at all, with the intent to develop a more general solution later.  It became clear very quickly that "subtracting years" involved different adjustments than "adding years".  I also eventually realized that some modifications needed to be made on leap years while other did not.  Most of these modifications could be done after all of the leap year blocks (i.e. 400 year, 100 year, 4 year, and 1 year division) were processed, but some could not.  If an error that only happens during a leap year makes 31 Dec look like 1 Jan, there was no way to know if a 1 Jan result needed to be adjusted or not.  Unfortunately, it was also impossible to make the checks while processing the leap year blocks because there was no way to know if the result was going to be a leap year before the year was calculated.

I was eventually able to resolve the above issues by having the 400 year calculation always subtract an extra 400 years.  I realized that was effectively making it so that all further calculations were guaranteed to be "positive" (i.e. adding years instead of subtracting them).  I then realized that, in v1 and v2 of this patch, allowing (days / DIVISOR_100) to equal -4 was necessary because it effectively did the same thing.  Accurately calculating the year with subtraction couldn't be done due to the need to know ahead of time if the year was going to result in a leap year. The fix was to overshoot the result so the rest could be done with addition.  After realizing that was the case, I changed the starting year to be just below the minimum possible value for a struct tm (as defined in the C standard, with tm->tm_year being represented by a regular signed int).  As a result, I could remove checks for cases where the days could be negative.  That was no longer a possibility.  I confirmed that removing the checks did not cause any inaccuracies with the previously mentioned random tests (that compare my algorithms result to the result of the current algorithm), some custom tests to check inputs that had caused problems during development and debugging, and with passing results from make check.





Developer Certificate of Origin
Version 1.1

Copyright (C) 2004, 2006 The Linux Foundation and its contributors.

Everyone is permitted to copy and distribute verbatim copies of this
license document, but changing it is not allowed.


Developer's Certificate of Origin 1.1

By making a contribution to this project, I certify that:

(a) The contribution was created in whole or in part by me and I
    have the right to submit it under the open source license
    indicated in the file; or

(b) The contribution is based upon previous work that, to the best
    of my knowledge, is covered under an appropriate open source
    license and I have the right under that license to submit that
    work with modifications, whether created in whole or in part
    by me, under the same open source license (unless I am
    permitted to submit under a different license), as indicated
    in the file; or

(c) The contribution was provided directly to me by some other
    person who certified (a), (b) or (c) and I have not modified
    it.

(d) I understand and agree that this project and the contribution
    are public and that a record of the contribution (including all
    personal information I submit with it, including my sign-off) is
    maintained indefinitely and may be redistributed consistent with
    this project or the open source license(s) involved.

Signed-off-by: Gary Gende <gary@garygende.com>




Gary Gende (1):
  offtime: optimized calculation of year from unix timestamp

 time/offtime.c | 48 ++++++++++++++++++++++++++++++++++--------------
 1 file changed, 34 insertions(+), 14 deletions(-)

-- 
2.43.0



More information about the Libc-alpha mailing list