diff --git a/src/utils/LruCounter.cxxtest b/src/utils/LruCounter.cxxtest new file mode 100644 index 000000000..cbecefadd --- /dev/null +++ b/src/utils/LruCounter.cxxtest @@ -0,0 +1,293 @@ +/** \copyright + * Copyright (c) 2020, Balazs Racz + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * \file LruCounter.cxxtest + * + * Unit tests for LruCounter. + * + * @author Balazs Racz + * @date 18 Sep 2020 + */ + +#include "utils/LruCounter.hxx" + +#include "utils/test_main.hxx" + +class LruCounterTest : public ::testing::Test +{ +protected: + GlobalLruCounter global_; + + void tick_n(unsigned n) + { + for (unsigned i = 0; i < n; ++i) + { + global_.tick(); + cb1.tick(global_); + cb2.tick(global_); + cb3.tick(global_); + cb4.tick(global_); + cs1.tick(global_); + cs2.tick(global_); + cs3.tick(global_); + cs4.tick(global_); + } + } + + void set_bits_per_bit(unsigned bpb) + { + new (&global_) GlobalLruCounter(bpb); + } + + /// Runs the sequence test on a given set of counters. + /// @param entries the counters + /// @param num_tick how much to wait between resetting each counter. + template + void sequence_test(std::initializer_list entries, unsigned num_tick) + { + for (T *e : entries) + { + EXPECT_EQ(0u, e->value()); + } + for (unsigned i = 1; i < entries.size(); i++) + { + tick_n(num_tick); + entries.begin()[i]->touch(); + } + tick_n(num_tick); + + for (unsigned i = 1; i < entries.size(); i++) + { + EXPECT_GT( + entries.begin()[i - 1]->value(), entries.begin()[i]->value()); + } + } + + /// Expects that an entry is going to flip forward to the next value in + /// num_tick counts. + template void next_increment(T *entry, unsigned num_tick) + { + LOG(INFO, "Next increment from %u", entry->value()); + unsigned current = entry->value(); + tick_n(num_tick - 1); + EXPECT_EQ(current, entry->value()); + tick_n(1); + EXPECT_EQ(current + 1, entry->value()); + } + + /// Byte sized LRU counters for testing. + LruCounter cb1, cb2, cb3, cb4; + /// Short sized LRU counters for testing. + LruCounter cs1, cs2, cs3, cs4; +}; + +TEST_F(LruCounterTest, create) +{ +} + +/// Tests that the initial value is zero and the reset value is zero. +TEST_F(LruCounterTest, initial) +{ + EXPECT_EQ(0u, cb1.value()); + EXPECT_EQ(0u, cs1.value()); + + cb1.touch(); + cs1.touch(); + + EXPECT_EQ(0u, cb1.value()); + EXPECT_EQ(0u, cs1.value()); +} + +/// Increments a counter through the first few values, which take exponentially +/// increasing tick count. +TEST_F(LruCounterTest, simple_increment) +{ + set_bits_per_bit(1); + EXPECT_EQ(0u, cb1.value()); + tick_n(1); // 1 + EXPECT_EQ(1u, cb1.value()); + tick_n(1); // 2 + EXPECT_EQ(2u, cb1.value()); + tick_n(2); // 4 + EXPECT_EQ(3u, cb1.value()); + tick_n(4); // 8 + EXPECT_EQ(4u, cb1.value()); + tick_n(8); // 16 + EXPECT_EQ(5u, cb1.value()); +} + +/// Increments a 16-bit counter through the first few values, which take +/// exponentially increasing tick count. +TEST_F(LruCounterTest, simple_increment_short) +{ + set_bits_per_bit(1); + EXPECT_EQ(0u, cs1.value()); + tick_n(1); // 1 + EXPECT_EQ(1u, cs1.value()); + tick_n(1); // 2 + EXPECT_EQ(2u, cs1.value()); + tick_n(2); // 4 + EXPECT_EQ(3u, cs1.value()); + tick_n(4); // 8 + EXPECT_EQ(4u, cs1.value()); + tick_n(8); // 16 + EXPECT_EQ(5u, cs1.value()); +} + +/// Increments a 2 bit/bit counter through the first few values, which take +/// exponentially increasing tick count. +TEST_F(LruCounterTest, simple_increment_2bit) +{ + EXPECT_EQ(0u, cb1.value()); + next_increment(&cb1, 1); // old value = 0, next tick = 1 + next_increment(&cb1, 3); // old value = 1, next tick = 4 + next_increment(&cb1, 12); // old value = 2, next tick = 16 + next_increment(&cb1, 16); // old value = 3, next tick = 32 + next_increment(&cb1, 32); // old value = 4, next tick = 64 + next_increment(&cb1, 64); // old value = 5, next tick = 128 + next_increment(&cb1, 64); // old value = 6, next tick = 192 + next_increment(&cb1, 64); // old value = 7, next tick = 256 + next_increment(&cb1, 256); // old value = 8, next tick = 512 +} + +/// Increments a 16-bit 2 bit/bit counter through the first few values, which +/// take exponentially increasing tick count. +TEST_F(LruCounterTest, simple_increment_short_2bit) +{ + EXPECT_EQ(0u, cs1.value()); + next_increment(&cs1, 1); // old value = 0, next tick = 1 + next_increment(&cs1, 3); // old value = 1, next tick = 4 + next_increment(&cs1, 12); // old value = 2, next tick = 16 + next_increment(&cs1, 16); // old value = 3, next tick = 32 + next_increment(&cs1, 32); // old value = 4, next tick = 64 + next_increment(&cs1, 64); // old value = 5, next tick = 128 + next_increment(&cs1, 64); // old value = 6, next tick = 192 + next_increment(&cs1, 64); // old value = 7, next tick = 256 + next_increment(&cs1, 256); // old value = 8, next tick = 512 +} + +/// Saturates a byte sized counter and expects that no overflow has happened. +TEST_F(LruCounterTest, no_overflow) +{ + set_bits_per_bit(1); + EXPECT_EQ(0u, cb1.value()); + tick_n(100000); + EXPECT_EQ(255u, cb1.value()); + tick_n(1); + EXPECT_EQ(255u, cb1.value()); + tick_n(100000); + EXPECT_EQ(255u, cb1.value()); +} + +/// Checks that a 2 bit/bit exponent bytes sized counter can count more than a +/// few 100k ticks. +TEST_F(LruCounterTest, byte_range) +{ + set_bits_per_bit(2); + EXPECT_EQ(0u, cb1.value()); + tick_n(100000); + EXPECT_EQ(52u, cb1.value()); + tick_n(100000); + EXPECT_EQ(67u, cb1.value()); +} + +/// Tests resetting the counter, then incrementing. +TEST_F(LruCounterTest, reset) +{ + set_bits_per_bit(1); + EXPECT_EQ(0u, cb1.value()); + tick_n(16); + EXPECT_EQ(5u, cb1.value()); + + cb1.touch(); + EXPECT_EQ(0u, cb1.value()); + tick_n(1); // 1 + EXPECT_EQ(1u, cb1.value()); + tick_n(1); // 2 + EXPECT_EQ(2u, cb1.value()); + tick_n(2); // 4 + EXPECT_EQ(3u, cb1.value()); + tick_n(4); // 8 + EXPECT_EQ(4u, cb1.value()); + tick_n(8); // 16 + EXPECT_EQ(5u, cb1.value()); +} + +/// Tests several counters that were reset at different times. Their values +/// should be monotonic from their reset time. +TEST_F(LruCounterTest, sequence) +{ + set_bits_per_bit(1); + EXPECT_EQ(0u, cb1.value()); + EXPECT_EQ(0u, cb2.value()); + EXPECT_EQ(0u, cb3.value()); + EXPECT_EQ(0u, cb4.value()); + + cb1.touch(); + tick_n(50); + cb2.touch(); + tick_n(50); + cb3.touch(); + tick_n(50); + cb4.touch(); + tick_n(50); + + EXPECT_GT(cb1.value(), cb2.value()); + EXPECT_GT(cb2.value(), cb3.value()); + EXPECT_GT(cb3.value(), cb4.value()); +} + +/// Tests several counters that were reset at different times. Their values +/// should be monotonic from their reset time. 1-byte, 1-bit-per-bit exponent +TEST_F(LruCounterTest, sequence_byte_1) +{ + set_bits_per_bit(1); + sequence_test({&cb1, &cb2, &cb3, &cb4}, 50); +} + +/// Tests several counters that were reset at different times. Their values +/// should be monotonic from their reset time. 2-byte, 1-bit-per-bit exponent +TEST_F(LruCounterTest, sequence_short_1) +{ + set_bits_per_bit(1); + sequence_test({&cs1, &cs2, &cs3, &cs4}, 50); +} + +/// Tests several counters that were reset at different times. Their values +/// should be monotonic from their reset time. 1-byte 2-bit-per-bit exponent +TEST_F(LruCounterTest, sequence_byte_2) +{ + set_bits_per_bit(2); + sequence_test({&cb1, &cb2, &cb3, &cb4}, 400); +} + +/// Tests several counters that were reset at different times. Their values +/// should be monotonic from their reset time. 2-byte 2-bit-per-bit exponent +TEST_F(LruCounterTest, sequence_short_2) +{ + set_bits_per_bit(2); + sequence_test({&cs1, &cs2, &cs3, &cs4}, 400); +} diff --git a/src/utils/LruCounter.hxx b/src/utils/LruCounter.hxx new file mode 100644 index 000000000..aec479ff0 --- /dev/null +++ b/src/utils/LruCounter.hxx @@ -0,0 +1,169 @@ +/** \copyright + * Copyright (c) 2020, Balazs Racz + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * \file LruCounter.hxx + * + * A monotonic counter that is usable for approximate LRU age determination. + * + * @author Balazs Racz + * @date 18 Sep 2020 + */ + +#ifndef _UTILS_LRUCOUNTER_HXX_ +#define _UTILS_LRUCOUNTER_HXX_ + +#include + +template class LruCounter; + +/// The GlobalLruCounter and a set of LruCounter<> objects cooperate in order +/// to create an approximate LRU order over a set of objects. The particular +/// optimization criterion is that the memory storage per object should be very +/// low. (Target is one byte.) Touching an object is constant time, but there +/// is linear time background maintenance operations that need to run +/// regularly. Picking the oldest object is linear time. The oldest concept is +/// an approximation in that the older an object becomes the less time +/// granularity is available to distinguish exact age. This is generally fine +/// in applications. +/// +/// How to use: +/// +/// Create one GlobalLruCounter. Create for each tracked object an +/// LruCounter or LruCounter. +/// +/// Periodically call the tick() function once on the GlobalLruCounter, then +/// for each live object the tick(global) function. This is linear cost in the +/// number of tracked objects, so do it rather rarely (e.g. once per second). +/// +/// When a specific object is used, call the touch() function on it. +/// +/// When the oldest object needs to be selected, pick the one which has the +/// highest returned value() from its LruCounter<>. +/// +/// Theory of operation: +/// +/// The GlobalLruCounter maintains a global tick count. It gets incremented by +/// one in each tick. In the per-object local counter we only increment the +/// counter for a subset of the global ticks. How many global ticks we skip +/// between two local counter increments depends on the age of the object. The +/// older an object becomes the more rarely we increment the object's counter. +/// +/// Specifically, if the object counter has reached to be k bits long, then we +/// only increment it, when the global counter's bottom k bits are all +/// zero. Example: if the object counter is 35 (6 bits long), then we increment +/// it to 36 when the global counter is divisible by 64 (all 6 bottom bits are +/// zero). In a variant we double the zero-bits requirement, needing that the +/// bottom 12 bits are all zero. +/// +/// Example calculations, assuming 1 tick per second: +/// +/// +-------------------------------------------------------------------+ +/// | Exponent 1 bit/bit 2 bits/bit | +/// +------------+------------------------------------------------------+ +/// | data type: | | +/// | | | +/// | uint8_t | max count: ~43k max count: 9.5M | +/// | | (0.5 days) (110 days) | +/// | | end granularity: 256 end granularity: 64k | +/// | | (4 min) (0.5 days) | +/// +------------+------------------------------------------------------+ +/// | uint16_t | max count: ~2.8B max count: 161T | +/// | | (100 years) (5M years) | +/// | | end granularity: 64k end granularity: 4B | +/// | | (0.5 days) (136 years) | +/// +------------+------------------------------------------------------+ +class GlobalLruCounter +{ +public: + /// Constructor. + /// @param bits_per_bit How aggressive the exponential downsampling should + /// be. Meaningful values are 1 and 2. + GlobalLruCounter(unsigned bits_per_bit = 2) + : bitsPerBit_(bits_per_bit) + { + } + void tick() + { + ++tick_; + } + +private: + template friend class LruCounter; + /// Setting defining the exponent. + unsigned bitsPerBit_; + /// Rolling counter of global ticks. This is used by the local counters to + /// synchronize their increments. + unsigned tick_ {0}; +}; + +/// Create an instance of this type for each object whose age needs to be +/// measured with the GlobalLruCounter. For further details, see +/// { \link GlobalLruCounter }. +/// @param T is the storage type, typically uint8_t or uint16_t. +template class LruCounter +{ +public: + /// @return A value monotonic in the age of the current counter. + unsigned value() + { + return counter_; + } + + /// Increments the local counter. + /// @param global reference to the global tick counter. All calls must use + /// the same global counter. + void tick(const GlobalLruCounter &global) + { + if (!counter_) + { + ++counter_; + return; + } + if (counter_ == std::numeric_limits::max()) + { + // Counter is saturated. + return; + } + int nlz = __builtin_clz((unsigned)counter_); + int needzero = (32 - nlz) * global.bitsPerBit_; + if ((global.tick_ & ((1U << needzero) - 1)) == 0) + { + ++counter_; + } + } + + /// Signals that the object has been used now. + void touch() + { + counter_ = 0; + } + +private: + /// Internal counter. + T counter_ {0}; +}; + +#endif // _UTILS_LRUCOUNTER_HXX_