Use a better algorithm to calculate n % 7, taking into account that the divisor
is a Mersenne number. This is explained in this two-part lightning talk at
CppCon 2024 [1, 2].

Roughly speaking, the algorithm is used for n = sys_days but it has a
precondition that doesn't hold for all values of sys_days. However, it does hold
for every value coming out of year_month_day::operator sys_days().

Nevertheless, there's an issue because the conversion is done in two steps:
year_month_day -> sys_days -> weekday and in the second step the information
that the input sys_days was obtained from the first step (so the precondition
holds) is lost. In the talk a single function performs the two steps and uses
the optimization. Unless the standard adds a similar function (e.g. a weekday
constructor) or this is added as an extension (not done here), this is not an
option for libstdc++.

The issue is adressed in the following way. At the end of the fist step, the
code informs the compiler, through an [[assume]] attribute that the precondition
holds. Then, at the beginning of the second step, the code checks, through
__builtin_constant_p, if the precondition holds, in which case, it uses the
better algorithm or, otherwise, it falls back to n % 7.

The idea is illustrated in [3] which compares code generation (with and without
the optimization) and provides an exhaustive test of correctness. A benchmark is
shown in [4].

References:

[1] https://youtu.be/64mTEXnSnZs
[2] https://youtu.be/bnVkWEjRNeI
[3] https://godbolt.org/z/TMMcKj8Gv
[4] https://quick-bench.com/q/VPtEBjGDmmB4QEG2IDjIvfU6WzA

libstdc++-v3/ChangeLog:

        * include/std/chrono: Add postcondition to year_month_date::operator
        sys_days and check this condition in weekday::_S_from_days.
        * testsuite/std/time/weekday/1.cc: Add tests for special values.
---
The approach might be somewhat controversial and might need to be discussed. The
problem is that passing information from [[assume]] to __builtin_constant_p is
quite fragile in 3 different ways.

1) The distance in source code between [[assume]] and __builtin_constant_p makes
hard to understand why they are there in the first place. A reader who doesn't
know the full picture might feel tempted to remove either of these lines.

2) Even cosmetic changes in the conditions of [[assume]] or 
__builtin_constant_p might break the compiler's chain of thought and thus, the 
optimization.

3) Similarly to 2, changes in the compiler code that handles [[assume]] or 
__builtin_constant_p might break the optimization.

If any of the issues above happens, the result will still be right but a
performance regression will appear. Comments can mitigate the issues but a
performance test or a test for code generation would be more effective to
prevent performance regressions. Unfortunately, I don't know how either of these
can be done.

Version history:
 v2. Fix bug in v1, add tests and improve comments.
 v1. Initial patch.
 
 libstdc++-v3/include/std/chrono              | 29 ++++++++++++++++++--
 libstdc++-v3/testsuite/std/time/weekday/1.cc | 19 +++++++++++++
 2 files changed, 46 insertions(+), 2 deletions(-)

diff --git a/libstdc++-v3/include/std/chrono b/libstdc++-v3/include/std/chrono
index 8eb9fd9baac..b747f46c13f 100644
--- a/libstdc++-v3/include/std/chrono
+++ b/libstdc++-v3/include/std/chrono
@@ -984,7 +984,22 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
       static constexpr weekday
       _S_from_days(const days& __d)
       {
-       return weekday{__detail::__add_modulo<7>(4, __d.count())};
+       const auto __days = __d.count();
+       using _Up = make_unsigned_t<decltype(__days)>;
+       const auto __n = static_cast<_Up>(__days) + 12'687'434u;
+
+       // This condition holds when __d = sys_days(__ymd).time_since_epoch(),
+       // where __ymd's type is year_month_day.
+       const auto __use_fast_mod7 = __n < 178'956'973u;
+
+       if (__builtin_constant_p(__use_fast_mod7) && __use_fast_mod7)
+       {
+         // https://youtu.be/64mTEXnSnZs and https://youtu.be/bnVkWEjRNeI
+         const auto __wd = 613'566'757u * static_cast<uint32_t>(__n) >> 29;
+         [[assume(__wd != 7)]];
+         return weekday{__wd};
+       }
+       return weekday{__detail::__add_modulo<7>(4, __days)};
       }
 
     public:
@@ -1652,7 +1667,17 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
 
       constexpr
       operator sys_days() const noexcept
-      { return sys_days{_M_days_since_epoch()}; }
+      {
+       const auto __days = _M_days_since_epoch();
+
+       // The assume attribute below allows weekday::_S_from_days to use a
+       // faster algorithm for modulus 7.
+       const auto __n = static_cast<unsigned>(__days.count()) + 12'687'434u;
+       const auto __use_fast_mod7 = __n < 178'956'973u;
+       [[assume(__use_fast_mod7)]];
+
+       return sys_days{__days};
+      }
 
       explicit constexpr
       operator local_days() const noexcept
diff --git a/libstdc++-v3/testsuite/std/time/weekday/1.cc 
b/libstdc++-v3/testsuite/std/time/weekday/1.cc
index ac05f728a55..5c98cd84a64 100644
--- a/libstdc++-v3/testsuite/std/time/weekday/1.cc
+++ b/libstdc++-v3/testsuite/std/time/weekday/1.cc
@@ -66,6 +66,25 @@ constexpr_weekday()
   static_assert(weekday{local_days{1970y/January/1}} == Thursday);
   static_assert(weekday{local_days{2020y/August/21}} == Friday);
 
+  constexpr ymd_min = year::min()/January/1;
+  static_assert(weekday{ymd_min} == Saturday);
+  static_assert(weekday{sys_days{ymd_min}} - days{1} == Friday);
+  static_assert(weekday{local_days{ymd_min}} - days{1} == Friday);
+
+  constexpr ymd_max = year::max()/December/31;
+  static_assert(weekday{ymd_max} == Sunday);
+  static_assert(weekday{sys_days{ymd_max}} + days{1} == Monday);
+  static_assert(weekday{local_days{ymd_max}} + days{1} == Monday);
+
+  static_assert(weekday{sys_days::min()} == Wednesday);
+  static_assert(weekday{local_days::min()} == Wednesday);
+
+  static_assert(weekday{sys_days::max()} == Thursday);
+  static_assert(weekday{local_days::max()} == Thursday);
+
+  static_assert(weekday{sys_days{days{int64_t{1} << 32}}} == Monday);
+  static_assert(weekday{local_days{days{int64_t{1} << 32}}} == Monday);
+
   static_assert(++weekday{3} == weekday{4});
   static_assert(weekday{3}++ == weekday{3});
   static_assert(--weekday{3} == weekday{2});
-- 
2.48.1

Reply via email to