Re: [PATCH] libstdc++: Optimize chrono year::is_leap
Francisco Muniz <[email protected]>
| Newsgroups | gmane.comp.gcc.patches,gmane.comp.gcc.libstdc++.devel |
|---|---|
| Message-ID | <CAO9xfXfDGPJjnVKsi-Bph1X5ygRvWXma9+OR89DhvW1H6qY6dw@mail.gmail.com> |
Is this good for trunk? Em dom., 2 de ago. de 2026 às 10:03, Cassio Neri <[email protected]> escreveu: > Hi all, > > My findings pretty much confirm what Francisco reported. > > When GCC vectorises the benchmark loop, my algorithm (with L. > Schneider and U. Drepper) ) is ~20% faster than Rueffner's (I have no > idea why) but when GCC doesn't vectorise, then Hueffner's is more than > ~2x faster than mine. (Results at the bottom.) > > I had a glance at the assembly of the whole benchmark code (not only > the functions in isolation) to confirm the changes in codegen related > to vectorisation. > > My intuition (no science involved) is that is_leap is very unlikely to > be called in a loop as in the benchmark code, so that, in practice, > GCC will rarely vectorise is_leap. I believe Francisco's change will > improve performance in the most likely case. (I'm happy to see the > current algorithm/code being replaced.) > > My measurements follow. > > 1) Jonathan's benchmark code compiled with -O3: > > > ------------------------------------------------------------------------------------------ > Benchmark Time CPU > Iterations UserCounters... > > ------------------------------------------------------------------------------------------ > BM_all_years_neri 9185 ns 9177 ns > 75196 items_per_second=3.05124G/s > BM_all_years_hueffner 11060 ns 11021 ns > 63871 items_per_second=2.54058G/s > BM_only_leap_years_neri 7155 ns 7143 ns > 97960 items_per_second=2.96951G/s > BM_only_leap_years_hueffner 8466 ns 8457 ns > 82658 items_per_second=2.50801G/s > BM_only_non_leap_years_neri 2269 ns 2266 ns > 311720 items_per_second=2.99582G/s > BM_only_non_leap_years_hueffner 2637 ns 2634 ns > 265893 items_per_second=2.57747G/s > > 2) With -O3 -fno-tree-vectorize: > > > ------------------------------------------------------------------------------------------ > Benchmark Time CPU > Iterations UserCounters... > > ------------------------------------------------------------------------------------------ > BM_all_years_neri 43187 ns 43145 ns > 16299 items_per_second=648.975M/s > BM_all_years_hueffner 21190 ns 21165 ns > 32295 items_per_second=1.32296G/s > BM_only_leap_years_neri 33077 ns 33042 ns > 21524 items_per_second=641.902M/s > BM_only_leap_years_hueffner 15691 ns 15677 ns > 44499 items_per_second=1.35296G/s > BM_only_non_leap_years_neri 10568 ns 10558 ns > 65968 items_per_second=643.128M/s > BM_only_non_leap_years_hueffner 5133 ns 5127 ns > 134345 items_per_second=1.32431G/s > > 3) Replacing the inner loop with the one below, also turns vectorisation > off. > > for (auto y : years) > { > auto b = is_leap(y); > benchmark::DoNotOptimize(b); > } > > With -O3 (adding -fno-tree-vectorize doesn't change much): > > > ------------------------------------------------------------------------------------------ > Benchmark Time CPU > Iterations UserCounters... > > ------------------------------------------------------------------------------------------ > BM_all_years_neri 46917 ns 46874 ns > 14923 items_per_second=597.348M/s > BM_all_years_hueffner 17727 ns 17690 ns > 38571 items_per_second=1.58282G/s > BM_only_leap_years_neri 35551 ns 35518 ns > 19535 items_per_second=597.161M/s > BM_only_leap_years_hueffner 13404 ns 13375 ns > 51384 items_per_second=1.58574G/s > BM_only_non_leap_years_neri 11479 ns 11455 ns > 59135 items_per_second=592.774M/s > BM_only_non_leap_years_hueffner 4406 ns 4401 ns > 159766 items_per_second=1.54284G/s > > > On Fri, 31 Jul 2026 at 07:46, Jonathan Wakely <[email protected]> wrote: > > > > On Thu, 30 Jul 2026 at 17:51, Cassio Neri <[email protected]> wrote: > > > > > > Hi Jonathan and Francisco. > > > > > > I would also expect Hüffner's algorithm to be faster than mine. > According to Ben Joffe's benchmark (*), it should be between 4% and 14% > faster. > > > > Yes, the generated code looks like it should be faster. > > > > > I'll have a look during the weekend. > > > > That would be much appreciated, thanks! > > > > > > > > (*) https://www.benjoffe.com/fast-leap-year > > > > > > FWIW: I wouldn't mind at all if my code is replaced :-) > > > > > > Cheers, > > > Cassio > > > > > > > > > > > > > > > On Thu, 30 Jul 2026, 13:26 Jonathan Wakely, <[email protected]> > wrote: > > >> > > >> CC Cassio (see > https://gcc.gnu.org/pipermail/libstdc++/2026-July/067388.html > > >> for the start of the thread, also quoted below). > > >> > > >> On Thu, 30 Jul 2026 at 16:53, Francisco Muniz <[email protected]> > wrote: > > >> > > > >> > Thanks, I had only checked scalar code generation initially. In an > isolated scalar test on x86_64, the new form is 7 instructions versus 10 > for the existing form, but I can reproduce that benchmark results depend on > optimization context. With GCC 17 -O3 generic, the loop favors the > existing form, while with -fno-tree-vectorize or -march=native the new form > is faster on my machine. > > >> > > > >> > Given that, I have to do more benchmarking before claiming this is > a performance improvement, focus was mostly on number of instructions > > >> > > >> > > >> I don't get consistent results, but for the benchmark code below > > >> (using Google Benchmark) I see worse performance for is_leap_hueffner. > > >> > > >> #include <algorithm> > > >> #include <chrono> > > >> #include <functional> > > >> #include <random> > > >> #include <vector> > > >> #include <span> > > >> #include <benchmark/benchmark.h> > > >> > > >> namespace { > > >> > > >> bool is_leap_neri(short y) > > >> { > > >> return (y & (y % 25 == 0 ? 15 : 3)) == 0; > > >> } > > >> > > >> bool is_leap_hueffner(short y) > > >> { > > >> const auto y32 = static_cast<unsigned int>(y) + 32800u; > > >> return ((y32 * 1073750999u) & 3221352463u) <= 126976u; > > >> } > > >> > > >> constexpr int firstYear = -3999; > > >> constexpr int lastYear = 24000; > > >> std::mt19937 rng(0x1234); > > >> > > >> std::vector<short> makeYears() > > >> { > > >> std::vector<short> years; > > >> years.reserve(lastYear - firstYear + 1); > > >> > > >> for (auto y = firstYear; y <= lastYear; ++y) > > >> years.push_back(y); > > >> std::shuffle(years.begin(), years.end(), rng); > > >> > > >> return years; > > >> } > > >> > > >> const std::vector<short> years = makeYears(); > > >> const std::vector<short> leap_years = [](auto v){ > > >> std::erase_if(v, is_leap_neri); > > >> std::shuffle(v.begin(), v.end(), rng); > > >> return v; > > >> }(years); > > >> const std::vector<short> non_leap_years = [](auto v){ > > >> std::erase_if(v, std::not_fn(is_leap_neri)); > > >> std::shuffle(v.begin(), v.end(), rng); > > >> return v; > > >> }(years); > > >> > > >> template<auto is_leap> > > >> void run_benchmark(std::span<const short> years, benchmark::State& > state) > > >> { > > >> for (auto _ : state) > > >> { > > >> unsigned count = 0; > > >> for (auto y : years) > > >> count += is_leap(y); > > >> benchmark::DoNotOptimize(count); > > >> } > > >> > > >> state.SetItemsProcessed(state.iterations() * years.size()); > > >> } > > >> > > >> } // namespace > > >> > > >> static void BM_all_years_neri(benchmark::State& state) > > >> { > > >> run_benchmark<is_leap_neri>(years, state); > > >> } > > >> > > >> BENCHMARK(BM_all_years_neri); > > >> > > >> static void BM_all_years_hueffner(benchmark::State& state) > > >> { > > >> run_benchmark<is_leap_hueffner>(years, state); > > >> } > > >> > > >> BENCHMARK(BM_all_years_hueffner); > > >> > > >> static void BM_only_leap_years_neri(benchmark::State& state) > > >> { > > >> run_benchmark<is_leap_neri>(leap_years, state); > > >> } > > >> > > >> BENCHMARK(BM_only_leap_years_neri); > > >> > > >> static void BM_only_leap_years_hueffner(benchmark::State& state) > > >> { > > >> run_benchmark<is_leap_hueffner>(leap_years, state); > > >> } > > >> > > >> BENCHMARK(BM_only_leap_years_hueffner); > > >> > > >> static void BM_only_non_leap_years_neri(benchmark::State& state) > > >> { > > >> run_benchmark<is_leap_neri>(non_leap_years, state); > > >> } > > >> > > >> BENCHMARK(BM_only_non_leap_years_neri); > > >> > > >> static void BM_only_non_leap_years_hueffner(benchmark::State& state) > > >> { > > >> run_benchmark<is_leap_hueffner>(non_leap_years, state); > > >> } > > >> > > >> BENCHMARK(BM_only_non_leap_years_hueffner); > > >> > > >> BENCHMARK_MAIN(); > > >> > > >> > ------------------------------------------------------------------------------------------ > > >> Benchmark Time CPU > > >> Iterations UserCounters... > > >> > ------------------------------------------------------------------------------------------ > > >> BM_all_years_neri 7172 ns 7156 ns > > >> 98499 items_per_second=3.913G/s > > >> BM_all_years_hueffner 9585 ns 9565 ns > > >> 72842 items_per_second=2.92731G/s > > >> BM_only_leap_years_neri 5424 ns 5408 ns > > >> 131330 items_per_second=3.92231G/s > > >> BM_only_leap_years_hueffner 7417 ns 7380 ns > > >> 95061 items_per_second=2.87408G/s > > >> BM_only_non_leap_years_neri 1746 ns 1742 ns > > >> 394463 items_per_second=3.89825G/s > > >> BM_only_non_leap_years_hueffner 2330 ns 2325 ns > > >> 303924 items_per_second=2.92068G/s > > >> > > >> > > >> But with variations on the benchmark code which shouldn't really > > >> affect the performance, I see the new code being faster. > > >> > > >> > > > >> > > > >> > Em qui., 30 de jul. de 2026 às 12:11, Jonathan Wakely < > [email protected]> escreveu: > > >> >> > > >> >> On Thu, 30 Jul 2026 at 01:58, Francisco Muniz wrote: > > >> >> > > > >> >> > Use Falk Hueffner's leap-year test for year::is_leap after > shifting > > >> >> > the valid std::chrono::year range by a multiple of 400. The shift > > >> >> > preserves divisibility by 4, 100, and 400, and the unsigned > conversion > > >> >> > gives the intended modulo 2^32 arithmetic. > > >> >> > > > >> >> > Idea by Cassio Neri: add 32800, which is 82 * 400, to shift the > signed > > >> >> > year range into the supported non-negative range. > > >> >> > > >> >> This is interesting, but your new code is slower than Cassio's code > > >> >> when I benchmark it. Have you done your own benchmarking? > > >> >> > > >> >> > > > >> >> > Tested on x86_64-pc-linux-gnu: > > >> >> > make -j$(nproc) all-gcc > > >> >> > make -j$(nproc) all-target-libstdc++-v3 > > >> >> > make check RUNTESTFLAGS='conformance.exp=std/time/year/1.cc' > > >> >> > make check RUNTESTFLAGS='conformance.exp=std/time/year/2.cc' > > >> >> > > > >> >> > libstdc++-v3/ChangeLog: > > >> >> > > > >> >> > * include/std/chrono (year::is_leap): Use Hueffner > leap-year > > >> >> > test after biasing the year by a multiple of 400. > > >> >> > > > >> >> > Signed-off-by: Francisco Muniz <[email protected]> > > >> >> > --- > > >> >> > libstdc++-v3/include/std/chrono | 31 > ++++++++----------------------- > > >> >> > 1 file changed, 8 insertions(+), 23 deletions(-) > > >> >> > > > >> >> > diff --git a/libstdc++-v3/include/std/chrono > b/libstdc++-v3/include/std/chrono > > >> >> > index 692fd6025e7..4483914c08b 100644 > > >> >> > --- a/libstdc++-v3/include/std/chrono > > >> >> > +++ b/libstdc++-v3/include/std/chrono > > >> >> > @@ -904,29 +904,14 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION > > >> >> > constexpr bool > > >> >> > is_leap() const noexcept > > >> >> > { > > >> >> > - // Testing divisibility by 100 first gives better > performance [1], i.e., > > >> >> > - // return _M_y % 100 == 0 ? _M_y % 400 == 0 : _M_y % > 16 == 0; > > >> >> > - // Furthermore, if _M_y % 100 == 0, then _M_y % 400 == 0 > is equivalent > > >> >> > - // to _M_y % 16 == 0, so we can simplify it to > > >> >> > - // return _M_y % 100 == 0 ? _M_y % 16 == 0 : _M_y % > 4 == 0. // #1 > > >> >> > - // Similarly, we can replace 100 with 25 (which is good > since > > >> >> > - // _M_y % 25 == 0 requires one fewer instruction than > _M_y % 100 == 0 > > >> >> > - // [2]): > > >> >> > - // return _M_y % 25 == 0 ? _M_y % 16 == 0 : _M_y % 4 > == 0. // #2 > > >> >> > - // Indeed, first assume _M_y % 4 != 0. Then _M_y % 16 > != 0 and hence, > > >> >> > - // _M_y % 4 == 0 and _M_y % 16 == 0 are both false. > Therefore, #2 > > >> >> > - // returns false as it should (regardless of _M_y % 25.) > Now assume > > >> >> > - // _M_y % 4 == 0. In this case, _M_y % 25 == 0 if, and > only if, > > >> >> > - // _M_y % 100 == 0, that is, #1 and #2 are equivalent. > Finally, #2 is > > >> >> > - // equivalent to > > >> >> > - // return (_M_y & (_M_y % 25 == 0 ? 15 : 3)) == 0. > > >> >> > - > > >> >> > - // References: > > >> >> > - // [1] https://github.com/cassioneri/calendar > > >> >> > - // [2] https://godbolt.org/z/55G8rn77e > > >> >> > - // [3] > https://gcc.gnu.org/pipermail/libstdc++/2021-June/052815.html > > >> >> > - > > >> >> > - return (_M_y & (_M_y % 25 == 0 ? 15 : 3)) == 0; > > >> >> > + // Shift into the range supported by Falk Hueffner's > leap-year test: > > >> >> > + // > hueffner.de/falk/blog/a-leap-year-check-in-three-instructions.html > > >> >> > + // Adding a multiple of 400 preserves divisibility by 4, > 100, and 400. > > >> >> > + // Idea by Cassio Neri: add 32800 (82 * 400). > > >> >> > + // The conversion to uint32_t gives the algorithm's > intended modulo 2^32 > > >> >> > + // arithmetic. > > >> >> > + const auto __y = static_cast<uint32_t>(_M_y) + 32800u; > > >> >> > + return ((__y * 1073750999u) & 3221352463u) <= 126976u; > > >> >> > } > > >> >> > > > >> >> > explicit constexpr > > >> >> > -- > > >> >> > 2.47.3 > > >> >> > > > >> >> > > >> > > >