Skip to content

sys: timeutil: add utility functions for struct timespec #90060

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
May 22, 2025

Conversation

cfriedt
Copy link
Member

@cfriedt cfriedt commented May 16, 2025

Add a number of utility functions for manipulating struct timespec.

  • timespec_add()
  • timespec_compare()
  • timespec_equal()
  • timespec_is_valid()
  • timespec_negate()
  • timespec_normalize()
  • timespec_sub()
  • timespec_from_timeout()
  • timespec_to_timeout()

If the __builtin_add_overflow() function is available, then these functions use are mostly branchless, which should provide decent performance on systems with caches and branch prediction (gated by CONFIG_SPEED_OPTIMIZATIONS). Otherwise, manually written alternatives exist that are also perhaps more readable.

The two functions at the end convert time durations between representation as struct timespec and k_timeout_t.

Fixes #88115

Doc Preview

Testing Done:

twister -i -T tests/lib/timespec_util
INFO    - Total complete:   45/  45  100%  built (not run):    0, filtered:  280, failed:    0, error:    0
INFO    - 7 test scenarios (322 configurations) selected, 280 configurations filtered (277 by static filter, 3 at runtime).
INFO    - 42 of 42 executed test configurations passed (100.00%), 0 built (not run), 0 failed, 0 errored, with no warnings in 49.20 seconds.
INFO    - 378 of 378 executed test cases passed (100.00%) on 9 out of total 1058 platforms (0.85%).
INFO    - 42 test configurations executed on platforms, 0 test configurations were only built.
INFO    - Saving reports...
INFO    - Writing JSON report /home/cfriedt/zephyrproject/zephyr/twister-out/twister.json
INFO    - Writing xunit report /home/cfriedt/zephyrproject/zephyr/twister-out/twister.xml...
INFO    - Writing xunit report /home/cfriedt/zephyrproject/zephyr/twister-out/twister_report.xml...
INFO    - Run completed

@cfriedt
Copy link
Member Author

cfriedt commented May 16, 2025

cc @trembel

Copy link
Collaborator

@bjarki-andreasen bjarki-andreasen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since timespec is specific to posix, should it be moved to include/zephyr/posix/ where

struct` timespec {
	time_t tv_sec;
	long tv_nsec;
};

is also defined? include/zephyr/sys I believe is primarily for zephyr native libraries, though there obviously are a few overlaps there like fdtable. Not sure how or where we draw such lines :)

*
* @return `true` if the operation would result in integer overflow, otherwise `false`.
*/
static inline bool timespec_add(struct timespec *a, const struct timespec *b)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if timespec itself is not nameclashing, the header should probably just be named timespec.h given the namespace timespec_util is not used for any function.

Copy link
Member Author

@cfriedt cfriedt May 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just merged it directly into timeutil.h

#90060 (comment)

Copy link
Member Author

@cfriedt cfriedt May 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on the structure of the documentation, it might be better just to add the contents of this file directly to include/zephyr/sys/timeutil.h and adjust doxygen groups accordingly.

@cfriedt cfriedt changed the title sys: util: timespec_util: add collection of inlines for timespec sys: util: timeutil: add utils for manipulating struct timespec May 16, 2025
Copy link
Collaborator

@npitre npitre left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few comments:

First, the bool return value. It feels to me like it should be the other
way around i.e. true=OK and false=problem. Currently you have things like:

    return timespec_negate(&neg) || timespec_add(a, &neg);

This just looks semantically wrong. It should be like: if we can negate
and add then return OK e.g.:

    return timespec_negate(&neg) && timespec_add(a, &neg);

Another example:

    return __builtin_add_overflow(a->tv_sec, b->tv_sec, &a->tv_sec) ||
           __builtin_add_overflow(a->tv_nsec, b->tv_nsec, &a->tv_nsec) ||
           timespec_normalize(a);

IMHO this would be more logically readable as:

    return !__builtin_add_overflow(a->tv_sec, b->tv_sec, &a->tv_sec) &&
           !__builtin_add_overflow(a->tv_nsec, b->tv_nsec, &a->tv_nsec) &&
           timespec_normalize(a);

Translation: if no overflow on the first add and no overflow on the second
add and normalization worked then return OK (true). Yes,
__builtin_add_overflow() returns true on error but that's in the name.
Here we have timespec_add() and the name doesn't imply an error.
Users would have more readable code if:

    if (timespec_add(a, b) {
        /* true: the addition was possible */
    } else {
        /* false: addition was _not_ possible */
    }

Next, I'm a little concerned by the optimization. This:

    int64_t sec = (ts->tv_nsec >= NSEC_PER_SEC) * (ts->tv_nsec / NSEC_PER_SEC) +
+                 (ts->tv_nsec < 0) * DIV_ROUND_UP(-ts->tv_nsec, NSEC_PER_SEC);

That's potentially two divisions just for the purpose of avoiding a branch.
On many architectures (if not most) a branch is still cheaper than a
division, even more so if the division ends up in a library call given
a 64-bit dividend.

And finally, you should add more values to your test case. In particular,
tests with tv_nsec being -NSEC_PER_SEC - 1, -2*NSEC_PER_SEC + 1,
-2*NSEC_PER_SEC - 1, etc. would provide more coverage.

@cfriedt
Copy link
Member Author

cfriedt commented May 16, 2025

Since timespec is specific to posix, should it be moved to include/zephyr/posix/

@bjarki-andreasen - mentioned in these docs as well as the C standard, while struct timespec originated with POSIX, it has been a part of ISO C since C11.

See also
https://en.cppreference.com/w/c/chrono/timespec

@bjarki-andreasen
Copy link
Collaborator

Since timespec is specific to posix, should it be moved to include/zephyr/posix/

@bjarki-andreasen - mentioned in these docs as well as the C standard, while struct timespec originated with POSIX, it has been a part of ISO C since C11.

See also
https://en.cppreference.com/w/c/chrono/timespec

Fair enough :) I have no strong opinion, just noticed that we do define the timespec struct in the posix folder. Anyway PR looks good, definate improvement!

@cfriedt
Copy link
Member Author

cfriedt commented May 16, 2025

IMHO this would be more logically readable as:

    return !__builtin_add_overflow(a->tv_sec, b->tv_sec, &a->tv_sec) &&
           !__builtin_add_overflow(a->tv_nsec, b->tv_nsec, &a->tv_nsec) &&
           timespec_normalize(a);

From my perspective, that is worse - there are 5 logical operations instead of 3.

Yes, __builtin_add_overflow() returns true on error but that's in the name.

I'm sort of thinking I would prefer to just append _overflow. To me, it's less useful to add multiple extra logical operations.

Here we have timespec_add().

Yeah, I could definitely rename that to timespec_add_overflow(). That would be in-line with all of the math-extras.h functions.

https://github.com/zephyrproject-rtos/zephyr/tree/main/include/zephyr/sys/math_extras.h

Next, I'm a little concerned by the optimization. This:

    int64_t sec = (ts->tv_nsec >= NSEC_PER_SEC) * (ts->tv_nsec / NSEC_PER_SEC) +
+                 (ts->tv_nsec < 0) * DIV_ROUND_UP(-ts->tv_nsec, NSEC_PER_SEC);

That's potentially two divisions just for the purpose of avoiding a branch. On many architectures (if not most) a branch is still cheaper than a division, even more so if the division ends up in a library call given a 64-bit dividend.

Agreed.

What I initially had is only cheap on processors that have e.g. icache, dcache, HW divide, and only cheap compared to potential cache misses.

I would be happy to use #ifdef CONFIG_SPEED_OPTIMIZATIONS here, because you are right in pointing out that the usual optimization for Zephyr should be for size.

And finally, you should add more values to your test case. In particular, tests with tv_nsec being -NSEC_PER_SEC - 1, -2*NSEC_PER_SEC + 1, -2*NSEC_PER_SEC - 1, etc. would provide more coverage.

Absolutely - thanks for the feedback @npitre.

@cfriedt
Copy link
Member Author

cfriedt commented May 16, 2025

Fair enough :) I have no strong opinion, just noticed that we do define the timespec struct in the posix folder. Anyway PR looks good, definate improvement!

Yeah, that's true - we should probably eliminate duplicate definitions especially with #30105

One kind of pervasive thing in POSIX (not sure if it's like this in the c spec as much) is that types can be originally defined in several header files. E.g. <sys/types.h> or <unistd.h> or <time.h> as the case may be. It's a weird overlap. If we use official-ish guard macros, it might work.

@cfriedt cfriedt force-pushed the timespec-utils branch 2 times, most recently from e72e78a to 7aeb687 Compare May 16, 2025 20:27
Copy link
Collaborator

@npitre npitre left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMHO this would be more logically readable as:

    return !__builtin_add_overflow(a->tv_sec, b->tv_sec, &a->tv_sec) &&
           !__builtin_add_overflow(a->tv_nsec, b->tv_nsec, &a->tv_nsec) &&
           timespec_normalize(a);

From my perspective, that is worse - there are 5 logical operations
instead of 3.

Absolutely not. There are no more operations. The compiler is happy
to flip the logic around as it see fit and it won't care one way or the
other.. OTOH this is better semantics for humans reading the code.

I'm sort of thinking I would prefer to just append _overflow. To me,
it's less useful to add multiple extra logical operations.

Please don't! This is an abomination!

Adding _overflow everywhere instead of ! in a few places is a really
a bad tradeoff. Again, the compiler doesn't care either ways (the code won't
be any bigger or any less efficient).

@cfriedt cfriedt force-pushed the timespec-utils branch 3 times, most recently from 8816044 to 2118238 Compare May 16, 2025 20:53
@cfriedt
Copy link
Member Author

cfriedt commented May 16, 2025

Please don't! This is an abomination!

Hehe.. sure, if you insist. I feel less strongly

Copy link
Member

@ycsin ycsin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@cfriedt cfriedt changed the title sys: util: timeutil: add utils for manipulating struct timespec sys: timeutil: add utility functions for struct timespec May 17, 2025
@cfriedt
Copy link
Member Author

cfriedt commented May 17, 2025

  • removed _overflow() from fn signatures, invert return values, adjust docs
  • cast NSEC_PER_SEC to long when compared with tv_nsec to ensure correct behaviour on 32-bit architectures

@cfriedt
Copy link
Member Author

cfriedt commented May 17, 2025

I might take a crack at fixing #90029 here as well. It would be nice to be able to put the tests in the tests/unit/timeutil testsuite when they are initially committed instead of moving them later.

@cfriedt cfriedt force-pushed the timespec-utils branch 2 times, most recently from da9e55d to 7a7a195 Compare May 17, 2025 17:37
npitre
npitre previously approved these changes May 20, 2025
Copy link
Collaborator

@npitre npitre left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are doing:

     .tv_sec = (timeout.ticks == K_TICKS_FOREVER) * INT64_MAX +
               (timeout.ticks != K_TICKS_FOREVER) * (ns / NSEC_PER_SEC),
     .tv_nsec = (timeout.ticks == K_TICKS_FOREVER) * (NSEC_PER_SEC - 1) +
                (timeout.ticks != K_TICKS_FOREVER) *
                        (ns - (ns / NSEC_PER_SEC) * NSEC_PER_SEC),

Instead of (ns - (ns / NSEC_PER_SEC) * NSEC_PER_SEC) you could simply do
(ns % NSEC_PER_SEC). When computing (ns / NSEC_PER_SEC) earlier, the
modulus often comes for free as a byproduct of the division and the compiler
would just use it given the opportunity. Granted, a smart compiler will
conclude that the above code can be substituted by a modulus already,
meaning that in the end it doesn't make much difference to the resulting
binary, therefore it is best to just use the simpler code.

No need to respin the PR just for this hence my +1, but if you need to
re-push then this change should be considered.

Add a number of utility functions for manipulating struct timespec.

* timespec_add()
* timespec_compare()
* timespec_equal()
* timespec_is_valid()
* timespec_negate()
* timespec_normalize()
* timespec_sub()
* timespec_from_timeout()
* timespec_to_timeout()

If the `__builtin_add_overflow()` function is available, then the
API is mostly branchless, which should provide decent performance on
systems with an instruction cache and branch prediction. Otherwise,
manually written alternatives exist that are also perhaps more
readable.

The two functions at the end convert time durations between
representation as `struct timespec` and `k_timeout_t`.

Signed-off-by: Chris Friedt <cfriedt@tenstorrent.com>
@cfriedt
Copy link
Member Author

cfriedt commented May 20, 2025

  • added cast to time_t from INT64_MAX (should resolve issue here)
  • used modulo operator instead of multiply / divide

Additional Testing Done:

twister -i -p native_sim/native/64 -s buildsystem.cmake.board.extend_two
INFO    - Using 'zephyr' toolchain.
INFO    - Building initial testsuite list...
INFO    - Writing JSON report /home/cfriedt/zephyrproject/zephyr/twister-out/testplan.json
INFO    - JOBS: 32
INFO    - Adding tasks to the queue...
INFO    - Added initial list of jobs to queue
INFO    - Total complete:    1/   1  100%  built (not run):    0, filtered:    0, failed:    0, error:    0
INFO    - 1 test scenarios (1 configurations) selected, 0 configurations filtered (0 by static filter, 0 at runtime).
INFO    - 1 of 1 executed test configurations passed (100.00%), 0 built (not run), 0 failed, 0 errored, with no warnings in 10.56 seconds.
INFO    - 1 of 1 executed test cases passed (100.00%) on 1 out of total 1058 platforms (0.09%).
INFO    - 1 test configurations executed on platforms, 0 test configurations were only built.
INFO    - Saving reports...
INFO    - Writing JSON report /home/cfriedt/zephyrproject/zephyr/twister-out/twister.json
INFO    - Writing xunit report /home/cfriedt/zephyrproject/zephyr/twister-out/twister.xml...
INFO    - Writing xunit report /home/cfriedt/zephyrproject/zephyr/twister-out/twister_report.xml...
INFO    - Run completed

npitre
npitre previously approved these changes May 20, 2025
Copy link
Collaborator

@npitre npitre left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still missing one modulo in case you repush.

cfriedt added 3 commits May 20, 2025 16:33
Add a timespec util testsuite. This should have reasonably high enough
coverage to be useful.

I would have preferred to add this as an architecture-independent
unit test (for the unit_testing platform) under tests/unit/timeutil but
there is an inconsistency about the size of time_t on the unit_testing
and native_sim/native platforms. On every other platform supported by
Zephyr, time_t is 64-bits. However, on those platforms, time_t is only
32-bits.

Signed-off-by: Chris Friedt <cfriedt@tenstorrent.com>
Add documentation in the timeutil group for recently added functions
for validating, comparing, and manipulating `struct timespec`
objects as well as functions for converting between representation
of time durations as either `struct timespec` or `k_timeout_t`.

Signed-off-by: Chris Friedt <cfriedt@tenstorrent.com>
Use the newly added timespec util functions to manipulate and
compare timespec structures with overflow detection.

Signed-off-by: Chris Friedt <cfriedt@tenstorrent.com>
@cfriedt
Copy link
Member Author

cfriedt commented May 20, 2025

@npitre - jeez - this is like Where's Waldo 😆 Can't sneak anything past you (not that I was trying, it was just hard for me to see).

Copy link

@cfriedt cfriedt requested a review from npitre May 21, 2025 02:36
cfriedt added a commit to cfriedt/zephyr that referenced this pull request May 22, 2025
Add copyright for changes that were included with zephyrproject-rtos#90060.

Signed-off-by: Chris Friedt <cfriedt@tenstorrent.com>
@cfriedt
Copy link
Member Author

cfriedt commented May 22, 2025

@andyross, @peter-mitsis - please (re-)review when you have a moment. One of you must approve as assignee.

@kartben kartben merged commit 8a5c744 into zephyrproject-rtos:main May 22, 2025
27 checks passed
@kartben
Copy link
Collaborator

kartben commented May 22, 2025

@cfriedt clang is unhappy, could you please have a look?
https://github.com/zephyrproject-rtos/zephyr/actions/runs/15197606622

@kartben
Copy link
Collaborator

kartben commented May 22, 2025

It's actually also failing with gcc https://github.com/zephyrproject-rtos/zephyr/actions/runs/15197606643

@cfriedt cfriedt deleted the timespec-utils branch May 23, 2025 01:34
cfriedt added a commit to cfriedt/zephyr that referenced this pull request May 23, 2025
Add copyright for changes that were included with zephyrproject-rtos#90060.

Signed-off-by: Chris Friedt <cfriedt@tenstorrent.com>
kartben pushed a commit that referenced this pull request May 27, 2025
Add copyright for changes that were included with #90060.

Signed-off-by: Chris Friedt <cfriedt@tenstorrent.com>
return (k_timeout_t){
.ticks = ((ts->tv_sec == INT64_MAX) && (ts->tv_nsec == NSEC_PER_SEC - 1)) *
K_TICKS_FOREVER +
((ts->tv_sec != INT64_MAX) && (ts->tv_sec >= 0)) *
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm seeing compile errors on this code.

/mnt/host/source/src/third_party/zephyr/main/include/zephyr/sys/timeutil.h:680:18: error: result of comparison of constant 9223372036854775807 with expression of type 'const __time_t' (aka 'const long') is 
always true [-Werror,-Wtautological-constant-out-of-range-compare]                                     
  680 |                          ((ts->tv_sec != INT64_MAX) && (ts->tv_sec >= 0)) *
      |                            ~~~~~~~~~~ ^  ~~~~~~~~~
/mnt/host/source/src/third_party/zephyr/main/include/zephyr/sys/timeutil.h:678:25: error: result of comparison of constant 9223372036854775807 with expression of type 'const __time_t' (aka 'const long') is 
always false [-Werror,-Wtautological-constant-out-of-range-compare]
  678 |                 .ticks = ((ts->tv_sec == INT64_MAX) && (ts->tv_nsec == NSEC_PER_SEC - 1)) *
      |                            ~~~~~~~~~~ ^  ~~~~~~~~~
2 errors generated.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

west twister -p native_sim/native -s rollback/rollback.secure_clear_os with clang

Copy link
Member Author

@cfriedt cfriedt May 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm seeing compile errors on this code.

The code / line numbers do not match what is in main currently.

There was a fix submitted. Can you please ensure you are testing a commit that includes d61a7a8 and d3ec916?

Shreyas-Shankar155 pushed a commit to MihiraMadhava/zephyr that referenced this pull request Jun 3, 2025
Add copyright for changes that were included with zephyrproject-rtos#90060.

Signed-off-by: Chris Friedt <cfriedt@tenstorrent.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

lib: posix: clock: Incorrect normalization when setting time lower than current uptime
9 participants