diff --git a/benchmark/CMakeLists.txt b/benchmark/CMakeLists.txt index 894306b..f6afaf6 100644 --- a/benchmark/CMakeLists.txt +++ b/benchmark/CMakeLists.txt @@ -9,6 +9,7 @@ CPMAddPackage("gh:google/benchmark@1.9.5") add_executable(timeq_benchmark time_queue.cpp + realistic_time_queue.cpp ) set_target_properties(timeq_benchmark PROPERTIES diff --git a/benchmark/realistic_time_queue.cpp b/benchmark/realistic_time_queue.cpp new file mode 100644 index 0000000..de95b01 --- /dev/null +++ b/benchmark/realistic_time_queue.cpp @@ -0,0 +1,51 @@ +#include "time_queue_bench.hpp" + +#include + +#include +#include + +struct MockPayloadData +{ + std::uint64_t id; + std::uint8_t priority; + std::uint8_t action; + std::uint64_t tick_microseconds; + + std::shared_ptr> data = std::make_shared>(1000); +}; + +using namespace timeq; + +BENCHMARK(Construct>)->Arg(300)->Arg(1'000'000); +BENCHMARK(Construct>)->Arg(300)->Arg(1'000'000); + +BENCHMARK(Push>)->Iterations(kIterations)->Arg(300)->Arg(1'000'000); +BENCHMARK(Push>)->Iterations(kIterations)->Arg(300)->Arg(1'000'000); + +BENCHMARK(FirstPushAfterIdle>)->Arg(300)->Arg(1'000'000); +BENCHMARK(FirstPushAfterIdle>)->Arg(300)->Arg(1'000'000); + +BENCHMARK(Pop>)->Iterations(kIterations)->Arg(300)->Arg(1'000'000); +BENCHMARK(Pop>)->Iterations(kIterations)->Arg(300)->Arg(1'000'000); + +BENCHMARK(Front>)->Iterations(kIterations)->Arg(300)->Arg(1'000'000); +BENCHMARK(Front>)->Iterations(kIterations)->Arg(300)->Arg(1'000'000); + +BENCHMARK(PopFront>)->Iterations(kIterations)->Arg(300)->Arg(1'000'000); +BENCHMARK(PopFront>)->Iterations(kIterations)->Arg(300)->Arg(1'000'000); + +BENCHMARK(Size>)->Iterations(kIterations); +BENCHMARK(Size>)->Iterations(kIterations); + +BENCHMARK(Empty>)->Iterations(kIterations); +BENCHMARK(Empty>)->Iterations(kIterations); + +BENCHMARK(PushAndPopLoaded>)->Arg(5000)->Arg(1'000'000); +BENCHMARK(PushAndPopLoaded>)->Arg(5000)->Arg(1'000'000); + +BENCHMARK(PushAndPop_Interval_1ms>)->Arg(5000)->Arg(1'000'000); +BENCHMARK(PushAndPop_Interval_1ms>)->Arg(5000)->Arg(1'000'000); + +BENCHMARK(PushAndPop_Interval_125ms>)->Arg(5000)->Arg(1'000'000); +BENCHMARK(PushAndPop_Interval_125ms>)->Arg(5000)->Arg(1'000'000); diff --git a/benchmark/time_queue.cpp b/benchmark/time_queue.cpp index d25615d..d5bda32 100644 --- a/benchmark/time_queue.cpp +++ b/benchmark/time_queue.cpp @@ -1,227 +1,10 @@ +#include "time_queue_bench.hpp" + #include #include #include -#include - -struct mock_tick_service : timeq::tick_service -{ - explicit mock_tick_service(std::chrono::milliseconds ticks) - : ticks(ticks) - { - } - - std::chrono::microseconds get() const override { return ticks; } - - std::chrono::microseconds ticks; -}; - -static auto service = std::make_shared(); - -constexpr size_t kIterations = 100'000'000; - -template -static void -Construct(benchmark::State& state) -{ - const std::size_t duration = state.range(0); - for (auto _ : state) { - auto tq = TimeQueue(duration, 1, service); - - std::size_t size = tq.size(); - benchmark::DoNotOptimize(size); - benchmark::ClobberMemory(); - } -} - -template -static void -Push(benchmark::State& state) -{ - TimeQueue tq(state.range(0), 1, service, kIterations); - int64_t items_count = 0; - - for (auto _ : state) { - ++items_count; - tq.push({}, 20); - } - - state.SetItemsProcessed(items_count); -} - -template -static void -FirstPushAfterIdle(benchmark::State& state) -{ - const std::size_t duration = state.range(0); - auto aged_service = std::make_shared(std::chrono::milliseconds(duration - 1)); - TimeQueue tq(duration, 1, aged_service, 1); - - for (auto _ : state) { - tq.push({}, duration); - - std::size_t size = tq.size(); - benchmark::DoNotOptimize(size); - benchmark::ClobberMemory(); - } -} - -template -static void -Pop(benchmark::State& state) -{ - TimeQueue tq(state.range(0), 1, service, kIterations); - for (size_t i = 0; i < kIterations; ++i) { - tq.push({}, 10); - } - - int64_t items_count = 0; - for (auto _ : state) { - ++items_count; - tq.pop(); - } - - state.SetItemsProcessed(items_count); -} - -template -static void -Front(benchmark::State& state) -{ - TimeQueue tq(state.range(0), 1, service, kIterations); - for (size_t i = 0; i < kIterations; ++i) { - tq.push({}, 15); - } - - int64_t items_count = 0; - for (auto _ : state) { - ++items_count; - auto value = tq.front(); - benchmark::DoNotOptimize(value); - benchmark::ClobberMemory(); - } - - state.SetItemsProcessed(items_count); -} - -template -static void -PopFront(benchmark::State& state) -{ - TimeQueue tq(state.range(0), 1, service, kIterations); - for (size_t i = 0; i < kIterations; ++i) { - tq.push({}, 15); - } - - int64_t items_count = 0; - for (auto _ : state) { - ++items_count; - auto value = tq.pop_front(); - benchmark::DoNotOptimize(value); - benchmark::ClobberMemory(); - } - - state.SetItemsProcessed(items_count); -} - -template -static void -Size(benchmark::State& state) -{ - TimeQueue tq(300, 1, service, kIterations); - for (size_t i = 0; i < kIterations; ++i) { - tq.push({}, 10); - } - - for (auto _ : state) { - std::size_t size = tq.size(); - benchmark::DoNotOptimize(size); - benchmark::ClobberMemory(); - } -} - -template -static void -Empty(benchmark::State& state) -{ - TimeQueue tq(300, 1, service, kIterations); - for (size_t i = 0; i < kIterations; ++i) { - tq.push({}, 10); - } - - for (auto _ : state) { - std::size_t size = tq.empty(); - benchmark::DoNotOptimize(size); - benchmark::ClobberMemory(); - } -} - -template -static void -PushAndPopLoaded(benchmark::State& state) -{ - TimeQueue tq(state.range(0), 1, service, kIterations); - int64_t items_count = 0; - - for (auto _ : state) { - ++items_count; - tq.push({}, 1000); - - // Simulate load by not popping all items - if (items_count % 100 == 0) { - auto elem = tq.front(); - tq.pop(); - benchmark::DoNotOptimize(elem); - benchmark::ClobberMemory(); - } - } - - state.SetItemsProcessed(items_count); -} - -template -static void -PushAndPop_Interval_1ms(benchmark::State& state) -{ - TimeQueue tq(state.range(0), 1, service, kIterations); - int64_t items_count = 0; - - for (auto _ : state) { - ++items_count; - tq.push({}, 1000); - - auto elem = tq.front(); - tq.pop(); - - benchmark::DoNotOptimize(elem); - benchmark::ClobberMemory(); - } - - state.SetItemsProcessed(items_count); -} - -template -static void -PushAndPop_Interval_125ms(benchmark::State& state) -{ - TimeQueue tq(state.range(0), 125, service, kIterations); - int64_t items_count = 0; - - for (auto _ : state) { - ++items_count; - tq.push({}, 1000); - - auto elem = tq.front(); - tq.pop(); - - benchmark::DoNotOptimize(elem); - benchmark::ClobberMemory(); - } - - state.SetItemsProcessed(items_count); -} - struct TrivialType {}; diff --git a/benchmark/time_queue_bench.hpp b/benchmark/time_queue_bench.hpp new file mode 100644 index 0000000..be9b997 --- /dev/null +++ b/benchmark/time_queue_bench.hpp @@ -0,0 +1,232 @@ +#pragma once + +#include +#include + +#include + +struct mock_tick_service : timeq::tick_service +{ + explicit mock_tick_service(std::chrono::milliseconds ticks) + : ticks(ticks) + { + } + + std::chrono::microseconds get() const override { return ticks; } + + std::chrono::microseconds ticks; +}; + +static auto service = std::make_shared(); + +constexpr size_t kIterations = 1'000'000; + +template +static void +Construct(benchmark::State& state) +{ + const std::size_t duration = state.range(0); + for (auto _ : state) { + auto tq = TimeQueue(duration, 1, service); + + std::size_t size = tq.size(); + benchmark::DoNotOptimize(size); + benchmark::ClobberMemory(); + } +} + +template +static void +Push(benchmark::State& state) +{ + TimeQueue tq(state.range(0), 1, service, kIterations); + int64_t items_count = 0; + + for (auto _ : state) { + ++items_count; + tq.push({}, 20); + } + + state.SetItemsProcessed(items_count); +} + +template +static void +FirstPushAfterIdle(benchmark::State& state) +{ + const std::size_t duration = state.range(0); + auto aged_service = std::make_shared(std::chrono::milliseconds(duration - 1)); + TimeQueue tq(duration, 1, aged_service, 1); + + for (auto _ : state) { + tq.push({}, duration); + + std::size_t size = tq.size(); + benchmark::DoNotOptimize(size); + benchmark::ClobberMemory(); + } +} + +template +static void +Pop(benchmark::State& state) +{ + TimeQueue tq(state.range(0), 1, service, kIterations); + for (size_t i = 0; i < kIterations; ++i) { + tq.push({}, 10); + } + + int64_t items_count = 0; + for (auto _ : state) { + ++items_count; + tq.pop(); + } + + state.SetItemsProcessed(items_count); +} + +template +static void +Front(benchmark::State& state) +{ + TimeQueue tq(state.range(0), 1, service, kIterations); + for (size_t i = 0; i < kIterations; ++i) { + tq.push({}, 15); + } + + int64_t items_count = 0; + for (auto _ : state) { + ++items_count; + auto value = tq.front(); + benchmark::DoNotOptimize(value); + benchmark::ClobberMemory(); + } + + state.SetItemsProcessed(items_count); +} + +template +static void +PopFront(benchmark::State& state) +{ + TimeQueue tq(state.range(0), 1, service, kIterations); + for (size_t i = 0; i < kIterations; ++i) { + tq.push({}, 15); + } + + int64_t items_count = 0; + for (auto _ : state) { + ++items_count; + auto value = tq.pop_front(); + benchmark::DoNotOptimize(value); + benchmark::ClobberMemory(); + } + + state.SetItemsProcessed(items_count); +} + +template +static void +Size(benchmark::State& state) +{ + TimeQueue tq(300, 1, service, kIterations); + for (size_t i = 0; i < kIterations; ++i) { + tq.push({}, 10); + } + + for (auto _ : state) { + std::size_t size = tq.size(); + benchmark::DoNotOptimize(size); + benchmark::ClobberMemory(); + } +} + +template +static void +Empty(benchmark::State& state) +{ + TimeQueue tq(300, 1, service, kIterations); + for (size_t i = 0; i < kIterations; ++i) { + tq.push({}, 10); + } + + for (auto _ : state) { + std::size_t size = tq.empty(); + benchmark::DoNotOptimize(size); + benchmark::ClobberMemory(); + } +} + +template +static void +PushAndPopLoaded(benchmark::State& state) +{ + TimeQueue tq(state.range(0), 1, service, kIterations); + int64_t items_count = 0; + + using T = TimeQueue::value_type::type; + const T data{}; + + for (auto _ : state) { + ++items_count; + tq.push(data, 1000); + + // Simulate load by not popping all items + if (items_count % 100 == 0) { + auto elem = tq.front(); + tq.pop(); + benchmark::DoNotOptimize(elem); + benchmark::ClobberMemory(); + } + } + + state.SetItemsProcessed(items_count); +} + +template +static void +PushAndPop_Interval_1ms(benchmark::State& state) +{ + TimeQueue tq(state.range(0), 1, service, kIterations); + int64_t items_count = 0; + + using T = TimeQueue::value_type::type; + const T data{}; + + for (auto _ : state) { + ++items_count; + tq.push(data, 1000); + + auto elem = tq.front(); + tq.pop(); + + benchmark::DoNotOptimize(elem); + benchmark::ClobberMemory(); + } + + state.SetItemsProcessed(items_count); +} + +template +static void +PushAndPop_Interval_125ms(benchmark::State& state) +{ + TimeQueue tq(state.range(0), 125, service, kIterations); + int64_t items_count = 0; + + using T = TimeQueue::value_type::type; + const T data{}; + + for (auto _ : state) { + ++items_count; + tq.push(data, 1000); + + auto elem = tq.front(); + tq.pop(); + + benchmark::DoNotOptimize(elem); + benchmark::ClobberMemory(); + } + + state.SetItemsProcessed(items_count); +} diff --git a/include/timeq/fast_time_queue.h b/include/timeq/fast_time_queue.h index 6d07c00..df8afd3 100644 --- a/include/timeq/fast_time_queue.h +++ b/include/timeq/fast_time_queue.h @@ -67,6 +67,8 @@ namespace timeq { template struct element { + using type = U; + /// Value of front object std::optional value; @@ -77,6 +79,8 @@ namespace timeq { template struct element { + using type = U; + /// Value of front object std::optional> value; @@ -171,7 +175,7 @@ namespace timeq { */ FORCE_INLINE void push(T&& value, std::size_t ttl, std::size_t delay_ttl = 0) { - internal_push(std::move(value), ttl, delay_ttl); + internal_push(std::forward(value), ttl, delay_ttl); } /** @@ -266,7 +270,7 @@ namespace timeq { } _queue_index = 0; - _last_tick_queue_cleared = _current_ticks; + _last_tick_queue_cleared = _last_advance_tick = _current_ticks; } protected: @@ -280,7 +284,7 @@ namespace timeq { { const tick_type new_tick_count = std::chrono::duration_cast(_tick_service->get()).count(); - const tick_type delta = new_tick_count - _current_ticks; + const tick_type delta = new_tick_count - _last_advance_tick; _current_ticks = new_tick_count; if (delta < _interval) { @@ -292,6 +296,8 @@ namespace timeq { return _current_ticks; } + _last_advance_tick = _current_ticks; + if (_current_ticks - _last_tick_queue_cleared > _duration && !_queue.empty()) { compact_consumed_queue(); _last_tick_queue_cleared = _current_ticks; @@ -341,7 +347,7 @@ namespace timeq { return; } - const std::size_t logical_size = _queue.size() - _queue_index; + const std::size_t logical_size = this->size(); if constexpr (std::is_trivially_copyable_v) { std::memmove(_queue.data(), _queue.data() + _queue_index, logical_size * sizeof(queue_value_type)); @@ -369,6 +375,9 @@ namespace timeq { /// Last calculated tick value when queue was cleared. tick_type _last_tick_queue_cleared{ 0 }; + /// Last tick when advance actually advanced. + tick_type _last_advance_tick{ 0 }; + /// The FIFO ordered queue of values as they were inserted. queue_type _queue; diff --git a/include/timeq/time_queue.h b/include/timeq/time_queue.h index 7c55bf2..a1ecd8d 100644 --- a/include/timeq/time_queue.h +++ b/include/timeq/time_queue.h @@ -84,6 +84,8 @@ namespace timeq { template struct element { + using type = U; + /// Value of front object std::optional value; @@ -94,6 +96,8 @@ namespace timeq { template struct element { + using type = std::reference_wrapper; + /// Value of front object std::optional> value;