From 644dee8c293b4beab7f9e40e3ad6e19d461a1f45 Mon Sep 17 00:00:00 2001 From: Marco Elver Date: Mon, 3 Aug 2026 10:50:58 -0700 Subject: [PATCH] Introduce Cycles32 for compact 32-bit cycle timestamp tracking Introduces class Cycles32 to encapsulate 32-bit timestamp tracking by right-shifting clock.now() by kShift=20 bits (~1M cycles, ~0.35 ms granularity at 3 GHz). This extends the 32-bit epoch to ~17.3 days before wraparound. PiperOrigin-RevId: 958462641 --- tcmalloc/internal/BUILD | 17 +++- tcmalloc/internal/clock.h | 78 +++++++++++++++- tcmalloc/internal/clock_test.cc | 154 ++++++++++++++++++++++++++++++++ 3 files changed, 246 insertions(+), 3 deletions(-) create mode 100644 tcmalloc/internal/clock_test.cc diff --git a/tcmalloc/internal/BUILD b/tcmalloc/internal/BUILD index e3cc03109..d12bb80ca 100644 --- a/tcmalloc/internal/BUILD +++ b/tcmalloc/internal/BUILD @@ -159,7 +159,22 @@ cc_library( visibility = [ "//tcmalloc:__subpackages__", ], - deps = [":config"], + deps = [ + ":config", + "@com_google_absl//absl/base", + "@com_google_absl//absl/time", + ], +) + +cc_test( + name = "clock_test", + srcs = ["clock_test.cc"], + copts = TCMALLOC_DEFAULT_COPTS, + deps = [ + ":clock", + "@com_google_absl//absl/time", + "@com_google_googletest//:gtest_main", + ], ) cc_library( diff --git a/tcmalloc/internal/clock.h b/tcmalloc/internal/clock.h index 65c765203..bf3dffbb6 100644 --- a/tcmalloc/internal/clock.h +++ b/tcmalloc/internal/clock.h @@ -17,6 +17,10 @@ #include +#include + +#include "absl/base/internal/cycleclock.h" +#include "absl/time/time.h" #include "tcmalloc/internal/config.h" GOOGLE_MALLOC_SECTION_BEGIN @@ -28,10 +32,80 @@ namespace tcmalloc_internal { // used implementations. Tests can use this interface to mock out the clock. struct Clock { // Returns the current time in ticks (relative to an arbitrary time base). - int64_t (*now)(); + int64_t (*now)() = absl::base_internal::CycleClock::Now; // Returns the number of ticks per second. - double (*freq)(); + double (*freq)() = absl::base_internal::CycleClock::Frequency; + + struct Snapshot { + int64_t now; + double freq; + }; + + Snapshot GetSnapshot() const { return Snapshot{now(), freq()}; } +}; + +// Encapsulates a 32-bit cycle timestamp by right-shifting clock.now() by +// kShift bits. +// +// Granularity and Epoch: +// - At 2 GHz: 1 tick = ~0.524 ms; 32-bit epoch = ~26.0 days before wraparound. +// - At 3 GHz: 1 tick = ~0.349 ms; 32-bit epoch = ~17.3 days before wraparound. +// - At 4 GHz: 1 tick = ~0.262 ms; 32-bit epoch = ~13.0 days before wraparound. +class Cycles32 { + public: + static constexpr int kShift = 20; + + constexpr Cycles32() = default; + explicit constexpr Cycles32(uint32_t val) : val_(val) {} + + // Updates the timestamp using clock.now() >> kShift. + void Update(Clock clock = Clock{}) { + uint32_t now_32 = static_cast(clock.now() >> kShift); + if (now_32 == 0) now_32 = 1; // Reserve 0 as the uninitialized sentinel. + val_.store(now_32, std::memory_order_relaxed); + } + + // Resets the timestamp to 0 (uninitialized sentinel). + void Reset() { val_.store(0, std::memory_order_relaxed); } + + // Returns true if the timestamp is initialized (val_ != 0). + explicit operator bool() const { + return val_.load(std::memory_order_relaxed) != 0; + } + + // Returns elapsed time since the recorded tick using a pre-taken snapshot. + // Returns absl::InfiniteDuration() if val_ == 0 (uninitialized). + absl::Duration AsDuration(Clock::Snapshot snap) const { + const uint32_t last = val_.load(std::memory_order_relaxed); + if (last == 0) return absl::InfiniteDuration(); + const uint32_t now_32 = static_cast(snap.now >> kShift); + // Unsigned 32-bit modular subtraction across wraparound is safe. + const uint32_t elapsed_ticks = now_32 - last; + const double elapsed_cycles = + static_cast(elapsed_ticks) * (1 << kShift); + return absl::Seconds(elapsed_cycles / snap.freq); + } + + // Convenience overload: takes a fresh clock snapshot. + absl::Duration AsDuration(Clock clock = Clock{}) const { + return AsDuration(clock.GetSnapshot()); + } + + // Returns true if this timestamp occurred after or at the same time as other + // across 32-bit wraparound boundaries (valid within half the epoch). + bool TimeAfterOrEqual(const Cycles32& other) const { + return TimeAfterOrEqual(raw(), other.raw()); + } + + uint32_t raw() const { return val_.load(std::memory_order_relaxed); } + + private: + static bool TimeAfterOrEqual(uint32_t a, uint32_t b) { + return static_cast(a - b) >= 0; + } + + std::atomic val_{0}; }; } // namespace tcmalloc_internal diff --git a/tcmalloc/internal/clock_test.cc b/tcmalloc/internal/clock_test.cc new file mode 100644 index 000000000..7756b5f73 --- /dev/null +++ b/tcmalloc/internal/clock_test.cc @@ -0,0 +1,154 @@ +// Copyright 2026 The TCMalloc Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "tcmalloc/internal/clock.h" + +#include + +#include "gtest/gtest.h" +#include "absl/time/time.h" + +namespace tcmalloc { +namespace tcmalloc_internal { +namespace { + +class Cycles32Test : public ::testing::Test { + protected: + void SetUp() override { + g_ticks_ = 0; + g_freq_ = 1e9; // 1 GHz -> 1 ns per cycle. + } + + void TearDown() override { g_ticks_ = 0; } + + static int64_t MockNow() { return g_ticks_; } + static double MockFreq() { return g_freq_; } + + Clock mock_clock_{.now = MockNow, .freq = MockFreq}; + + static inline int64_t g_ticks_ = 0; + static inline double g_freq_ = 1e9; +}; + +TEST_F(Cycles32Test, Uninitialized) { + Cycles32 c; + EXPECT_EQ(c.raw(), 0); + EXPECT_EQ(c.AsDuration(mock_clock_), absl::InfiniteDuration()); +} + +TEST_F(Cycles32Test, UpdateAndAsDuration) { + g_ticks_ = (100LL << Cycles32::kShift); + + Cycles32 c; + c.Update(mock_clock_); + EXPECT_EQ(c.raw(), 100); + + g_ticks_ = (250LL << Cycles32::kShift); + EXPECT_EQ(c.AsDuration(mock_clock_), + absl::Nanoseconds(150LL << Cycles32::kShift)); +} + +TEST_F(Cycles32Test, WraparoundModularSubtraction) { + Cycles32 c(0xFFFFFFF0u); + + g_ticks_ = (10LL << Cycles32::kShift); + EXPECT_EQ(c.AsDuration(mock_clock_), + absl::Nanoseconds(26LL << Cycles32::kShift)); +} + +TEST_F(Cycles32Test, TimeAfterOrEqual) { + Cycles32 c10(10); + Cycles32 c20(20); + EXPECT_TRUE(c10.TimeAfterOrEqual(c10)); + EXPECT_TRUE(c20.TimeAfterOrEqual(c10)); + EXPECT_FALSE(c10.TimeAfterOrEqual(c20)); + + Cycles32 c_before(0xFFFFFFF0u); + Cycles32 c_after(10); + EXPECT_TRUE(c_after.TimeAfterOrEqual(c_before)); + EXPECT_FALSE(c_before.TimeAfterOrEqual(c_after)); +} + +TEST_F(Cycles32Test, SentinelAliasingAtTickZero) { + g_ticks_ = 0; // Tick 0 right-shifts to 0. + Cycles32 c; + c.Update(mock_clock_); + EXPECT_EQ(c.raw(), + 1); // Clamped to 1 to reserve 0 as uninitialized sentinel. + EXPECT_NE(c.AsDuration(mock_clock_), absl::InfiniteDuration()); +} + +TEST_F(Cycles32Test, ExactHalfEpochSignInversion) { + Cycles32 base(0); + Cycles32 max_forward(0x7FFFFFFFu); + Cycles32 sign_flip(0x80000000u); + + EXPECT_TRUE(max_forward.TimeAfterOrEqual(base)); + EXPECT_FALSE(sign_flip.TimeAfterOrEqual(base)); + + Cycles32 w_base(10); + Cycles32 w_forward(10 + 0x7FFFFFFFu); + Cycles32 w_flip(10 + 0x80000000u); + EXPECT_TRUE(w_forward.TimeAfterOrEqual(w_base)); + EXPECT_FALSE(w_flip.TimeAfterOrEqual(w_base)); +} + +TEST_F(Cycles32Test, MaximumElapsedDurationExactness) { + Cycles32 c(1); + g_ticks_ = (0xFFFFFFFFLL << Cycles32::kShift); + // 0xFFFFFFFFu - 1 = 0xFFFFFFFEu ticks elapsed. + EXPECT_EQ(c.AsDuration(mock_clock_), + absl::Nanoseconds(0xFFFFFFFELL << Cycles32::kShift)); +} + +TEST_F(Cycles32Test, ZeroElapsedDuration) { + g_ticks_ = (500LL << Cycles32::kShift); + Cycles32 c; + c.Update(mock_clock_); + EXPECT_EQ(c.AsDuration(mock_clock_), absl::ZeroDuration()); +} + +TEST_F(Cycles32Test, SnapshotImmuneToDrift) { + g_ticks_ = (100LL << Cycles32::kShift); + Cycles32 c; + c.Update(mock_clock_); + + g_ticks_ = (200LL << Cycles32::kShift); + auto snap = mock_clock_.GetSnapshot(); + + // Advance clock after snapshot was taken. + g_ticks_ = (500LL << Cycles32::kShift); + + // AsDuration(snap) must use the snapshot (100 ticks elapsed), not g_ticks_ + // (400 ticks). + EXPECT_EQ(c.AsDuration(snap), absl::Nanoseconds(100LL << Cycles32::kShift)); +} + +TEST_F(Cycles32Test, ResetAndBoolOperator) { + Cycles32 c; + EXPECT_FALSE(static_cast(c)); + EXPECT_EQ(c.AsDuration(mock_clock_), absl::InfiniteDuration()); + + c.Update(mock_clock_); + EXPECT_TRUE(static_cast(c)); + EXPECT_NE(c.AsDuration(mock_clock_), absl::InfiniteDuration()); + + c.Reset(); + EXPECT_FALSE(static_cast(c)); + EXPECT_EQ(c.AsDuration(mock_clock_), absl::InfiniteDuration()); +} + +} // namespace +} // namespace tcmalloc_internal +} // namespace tcmalloc