Skip to content

sys: util: timeutil: add utils for manipulating 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

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from

Conversation

cfriedt
Copy link
Member

@cfriedt cfriedt commented May 16, 2025

Add a number of utility functions for manipulating struct timespec.

  • timespec_add_overflow()
  • timespec_compare()
  • timespec_equal()
  • timespec_is_valid()
  • timespec_negate_overflow()
  • timespec_normalize_overflow()
  • timespec_sub_overflow()

If the __builtin_add_overflow() and __builtin_sub_overflow() functions are available, then these functions use are mostly branchless, which should provide decent performance on systems with an instruction cache. Otherwise, manually written alternatives exist that are also perhaps more readable.

Fixes #88115

Doc Preview

Testing Done:

twister -i -p qemu_riscv64 -T tests/lib/timespec_util
...
INFO    - Total complete:    5/   5  100%  built (not run):    0, filtered:    2, failed:    0, error:    0
INFO    - 7 test scenarios (7 configurations) selected, 2 configurations filtered (2 by static filter, 0 at runtime).
INFO    - 5 of 5 executed test configurations passed (100.00%), 0 built (not run), 0 failed, 0 errored, with no warnings in 12.44 seconds.
INFO    - 35 of 35 executed test cases passed (100.00%) on 1 out of total 1058 platforms (0.09%).
INFO    - 5 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

- timeutils
integration_platforms:
- native_sim/64
tests:
Copy link
Member Author

Choose a reason for hiding this comment

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

It would also be good to add a testcase to verify the non-builtin code path.

Keeping this in draft until I've done that.

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

swap the relative position of the __cplusplus closing
bracket and the doxygen end-group comment.

Signed-off-by: Chris Friedt <[email protected]>
@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).

Add a number of utility functions for manipulating struct timespec.

* timespec_add_overflow()
* timespec_compare()
* timespec_equal()
* timespec_is_valid()
* timespec_negate_overflow()
* timespec_normalize_overflow()
* timespec_sub_overflow()

If the `__builtin_add_overflow()` and `__builtin_sub_overflow()`
functions are available, then these functions use are mostly
branchless, which should provide decent performance on systems with an
instruction cache. Otherwise, manually written alternatives exist that
are also perhaps more readable.

Signed-off-by: Chris Friedt <[email protected]>
@cfriedt cfriedt force-pushed the timespec-utils branch 2 times, most recently from c2ef0d4 to 8816044 Compare May 16, 2025 20:45
cfriedt added 3 commits May 16, 2025 16:52
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 <[email protected]>
Use the newly added timespec util functions to manipulate and
compare timespec structures with overflow detection.

Signed-off-by: Chris Friedt <[email protected]>
Add documentation in the timeutil group for recently added functions
for validating, comparing, and manipulating `struct timespec`
objects.

Signed-off-by: Chris Friedt <[email protected]>
@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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
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
3 participants