From 0534572c451b112a87d604d8d43b41ff88e5af57 Mon Sep 17 00:00:00 2001 From: LTLA Date: Sat, 6 Apr 2024 23:21:27 -0700 Subject: [PATCH] Began streamlining the oracle-aware caching. --- include/tatami_chunked/OracleSlabCache.hpp | 210 +++++++----------- .../custom_chunk_coordinator.hpp | 4 +- include/tatami_chunked/typical_slab_cache.hpp | 2 +- tests/CMakeLists.txt | 6 +- tests/src/OracleSlabCache.cpp | 118 +++++----- 5 files changed, 141 insertions(+), 199 deletions(-) diff --git a/include/tatami_chunked/OracleSlabCache.hpp b/include/tatami_chunked/OracleSlabCache.hpp index 16aef0d..06d4ad1 100644 --- a/include/tatami_chunked/OracleSlabCache.hpp +++ b/include/tatami_chunked/OracleSlabCache.hpp @@ -27,44 +27,34 @@ namespace tatami_chunked { */ template class OracleSlabCache { +private: std::shared_ptr > oracle; - size_t max_predictions; - size_t max_slabs; + size_t total; size_t counter = 0; -private: - std::list slab_cache, tmp_cache, free_cache; - - typedef typename std::list::iterator cache_iterator; - std::unordered_map > slab_exists, past_exists; - - std::vector > predictions_made; - size_t predictions_fulfilled = 0; - - std::vector slab_pointers; + Index_ last_slab_id = 0; + Slab_* last_slab = NULL; - std::vector > unassigned_slabs; - std::vector > slabs_to_populate; + size_t max_slabs; + std::vector all_slabs; + std::unordered_map current_cache, future_cache; + std::vector > to_populate; + std::vector to_reassign; + size_t refresh_point = 0; public: /** * @param ora Pointer to an `tatami::Oracle` to be used for predictions. - * @param per_iteration Maximum number of predictions to make per iteration. * @param num_slabs Maximum number of slabs to store. */ - OracleSlabCache(std::shared_ptr > ora, [[maybe_unused]] size_t per_iteration, size_t num_slabs) : + OracleSlabCache(std::shared_ptr > ora, size_t num_slabs) : oracle(std::move(ora)), - max_predictions(oracle->total()), - max_slabs(num_slabs) + total(oracle->total()), + max_slabs(num_slabs) { - slab_exists.reserve(max_slabs); - past_exists.reserve(max_slabs); - - predictions_made.reserve(max_predictions); - - slab_pointers.reserve(max_slabs); - unassigned_slabs.reserve(max_slabs); - slabs_to_populate.reserve(max_slabs); + all_slabs.reserve(max_slabs); + current_cache.reserve(max_slabs); + future_cache.reserve(max_slabs); } /** @@ -76,12 +66,6 @@ class OracleSlabCache { * @endcond */ -private: - std::pair fetch(size_t i) const { - const auto& current = predictions_made[i]; - return std::pair(slab_pointers[current.first], current.second); - } - public: /** * This method is intended to be called when `num_slabs = 0`, to provide callers with the oracle predictions for non-cached extraction of data. @@ -108,116 +92,92 @@ class OracleSlabCache { * For example, if each chunk takes up 10 rows, attempting to access row 21 would require retrieval of slab 2 and an offset of 1. * @param create Function that accepts no arguments and returns a `Slab_` object with sufficient memory to hold a slab's contents when used in `populate()`. * This may also return a default-constructed `Slab_` object if the allocation is done dynamically per slab in `populate()`. - * @param populate Function that accepts two arguments, `slabs_in_need` and `slab_data`. - * (1) `slabs_in_need` is a `const std::vector >&` specifying the slabs to be populated. + * @param populate Function that accepts a `std::vector >&` specifying the slabs to be populated. * The first `Id_` element of each pair contains the slab identifier, i.e., the first element returned by the `identify` function. - * The second `Index_` element specifies the index in `slab_data` in which to store the contents of each slab. - * (2) `slab_data` is a `std::vector&` containing pointers to the cached slab contents to be populated. - * This function should iterate over the `slabs_in_need` and populate the corresponding entries in `slab_data`. + * The second `Slab_*` element contains a pointer to a `Slab_` returned by `create()`. + * This function should iterate over the vector and populate each slab. + * Note that the vector is not guaranteed to be sorted. * * @return Pair containing (1) a pointer to a slab's contents and (2) the index of the next predicted row/column inside the retrieved slab. */ template std::pair next(Ifunction_ identify, Cfunction_ create, Pfunction_ populate) { - if (predictions_made.size() > predictions_fulfilled) { - return fetch(predictions_fulfilled++); + Index_ index = this->next(); + auto slab_info = identify(index); + if (slab_info.first == last_slab_id && last_slab) { + return std::make_pair(last_slab, slab_info.second); } - - predictions_made.clear(); - size_t used = 0; - - // Iterators in the unordered_map should remain valid after swapping the containers, - // see https://stackoverflow.com/questions/4124989/does-stdvectorswap-invalidate-iterators - tmp_cache.swap(slab_cache); - - past_exists.swap(slab_exists); - slab_exists.clear(); - - slab_pointers.clear(); - unassigned_slabs.clear(); - slabs_to_populate.clear(); - - while (counter < max_predictions) { - Index_ current = this->next(); - - auto slab_id = identify(current); - auto curslab = slab_id.first; - auto curindex = slab_id.second; - - auto it = slab_exists.find(curslab); - if (it != slab_exists.end()) { - predictions_made.emplace_back((it->second).first, curindex); - - } else if (used < max_slabs) { - auto past = past_exists.find(curslab); - if (past != past_exists.end()) { - auto sIt = (past->second).second; - slab_cache.splice(slab_cache.end(), tmp_cache, sIt); - slab_pointers.push_back(&(*sIt)); - slab_exists[curslab] = std::make_pair(used, sIt); - - } else { - if (free_cache.empty()) { - // We might be able to recycle an existing slab from tmp_cache - // to populate 'curslab'... but we don't know if we can do so at - // this moment, as those slabs might be needed by later predictions. - // So we just defer the creation of a new slab until we've run - // through the set of predictions for this round. - auto ins = slab_exists.insert(std::make_pair(curslab, std::make_pair(used, slab_cache.end()))); - unassigned_slabs.emplace_back(used, &(ins.first->second.second)); - slab_pointers.push_back(NULL); - - } else { - auto sIt = free_cache.begin(); - slab_cache.splice(slab_cache.end(), free_cache, sIt); - slab_pointers.push_back(&(*sIt)); - slab_exists[curslab] = std::make_pair(used, sIt); + last_slab_id = slab_info.first; + + // Updating the cache if we hit the refresh point. + if (counter - 1 == refresh_point) { + requisition_slab(slab_info.first, create); + size_t used_slabs = 1; + auto last_future_slab_id = slab_info.first; + + while (++refresh_point < total) { + auto future_index = oracle->get(refresh_point); + auto future_slab_info = identify(future_index); + if (last_future_slab_id != future_slab_info.first) { + if (future_cache.find(future_slab_info.first) == future_cache.end()) { + if (used_slabs == max_slabs) { + break; + } + requisition_slab(future_slab_info.first, create); + ++used_slabs; } - - slabs_to_populate.emplace_back(curslab, used); } - - predictions_made.emplace_back(used, curindex); - ++used; - - } else { - --counter; - break; } - } - while (!unassigned_slabs.empty()) { - cache_iterator it; - if (!tmp_cache.empty()) { - it = tmp_cache.begin(); - slab_cache.splice(slab_cache.end(), tmp_cache, it); - } else { - slab_cache.emplace_back(create()); - it = slab_cache.end(); - --it; + auto cIt = current_cache.begin(); + for (auto a : to_reassign) { + to_populate.emplace_back(a, cIt->second); + future_cache[a] = cIt->second; + ++cIt; } - auto& last = unassigned_slabs.back(); - slab_pointers[last.first] = &(*it); - - // This changes the value in the slab_exists map without having to do a look-up, see: - // https://stackoverflow.com/questions/16781886/can-we-store-unordered-maptiterator - *(last.second) = it; - - unassigned_slabs.pop_back(); + // We always fill future_cache to the brim so every entry of + // all_slabs should be referenced by a pointer in future_cache. + // There shouldn't be any free cache entries remaining in + // current_cache i.e., at this point, cIt should equal + // current_cache.end(), as we transferred everything to + // future_cache. Thus it is safe to clear current_cache without + // worrying about leaking memory. The only exception is if we're at + // the end of the predictions, in which case it doesn't matter. + current_cache.clear(); + to_reassign.clear(); + + populate(to_populate); + + current_cache.swap(future_cache); + to_populate.clear(); } - while (!tmp_cache.empty()) { - free_cache.splice(free_cache.end(), tmp_cache, tmp_cache.begin()); - } + // We know it must exist, so no need to check ccIt's validity. + auto ccIt = current_cache.find(slab_info.first); + last_slab = ccIt->second; + return std::make_pair(last_slab, slab_info.second); + } - if (!slabs_to_populate.empty()) { - populate(slabs_to_populate, slab_pointers); +private: + template + void requisition_slab(Id_ slab_id, Cfunction_ create) { + auto ccIt = current_cache.find(slab_id); + if (ccIt != current_cache.end()) { + auto slab_ptr = ccIt->second; + future_cache[slab_id] = slab_ptr; + current_cache.erase(ccIt); + + } else if (all_slabs.size() < max_slabs) { + all_slabs.emplace_back(create()); + auto slab_ptr = &(all_slabs.back()); + future_cache[slab_id] = slab_ptr; + to_populate.emplace_back(slab_id, slab_ptr); + + } else { + future_cache[slab_id] = NULL; + to_reassign.push_back(slab_id); } - - // Well, because we just used one. - predictions_fulfilled = 1; - return fetch(0); } }; diff --git a/include/tatami_chunked/custom_chunk_coordinator.hpp b/include/tatami_chunked/custom_chunk_coordinator.hpp index 2726f79..de8844f 100644 --- a/include/tatami_chunked/custom_chunk_coordinator.hpp +++ b/include/tatami_chunked/custom_chunk_coordinator.hpp @@ -606,9 +606,9 @@ class ChunkCoordinator { /* create = */ [&]() -> Slab { return Slab(alloc); }, - /* populate =*/ [&](const std::vector >& in_need, auto& data) -> void { + /* populate =*/ [&](const std::vector >& to_populate) -> void { for (const auto& p : in_need) { - fetch_block(p.first, 0, get_primary_chunkdim(p.first), *(data[p.second])); + fetch_block(p.first, 0, get_primary_chunkdim(p.first), *(p.second)); } } ); diff --git a/include/tatami_chunked/typical_slab_cache.hpp b/include/tatami_chunked/typical_slab_cache.hpp index 9d5fe17..5202584 100644 --- a/include/tatami_chunked/typical_slab_cache.hpp +++ b/include/tatami_chunked/typical_slab_cache.hpp @@ -61,7 +61,7 @@ struct TypicalSlabCacheWorkspace { if constexpr(!oracle_) { cache = LruSlabCache(num_slabs_in_cache); } else if constexpr(!subset_) { - cache = OracleSlabCache(std::move(oracle), 10000, num_slabs_in_cache); + cache = OracleSlabCache(std::move(oracle), num_slabs_in_cache); } else { cache = SubsettedOracleSlabCache(std::move(oracle), 10000, num_slabs_in_cache); } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 10a70c5..7ccfab5 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -18,11 +18,11 @@ add_executable( libtest src/LruSlabCache.cpp src/OracleSlabCache.cpp - src/SubsettedOracleSlabCache.cpp +# src/SubsettedOracleSlabCache.cpp src/mock_dense_chunk.cpp src/mock_sparse_chunk.cpp - src/CustomDenseChunkedMatrix.cpp - src/CustomSparseChunkedMatrix.cpp +# src/CustomDenseChunkedMatrix.cpp +# src/CustomSparseChunkedMatrix.cpp ) set(CODE_COVERAGE OFF CACHE BOOL "Enable coverage testing") diff --git a/tests/src/OracleSlabCache.cpp b/tests/src/OracleSlabCache.cpp index 4512de5..e5d7531 100644 --- a/tests/src/OracleSlabCache.cpp +++ b/tests/src/OracleSlabCache.cpp @@ -20,11 +20,11 @@ class OracleSlabCacheTestMethods { ++nalloc; return TestSlab(); }, - [&](std::vector >& in_need, std::vector& data) -> void { + [&](std::vector >& in_need) -> void { for (auto& x : in_need) { - auto& current = data[x.second]; - current->chunk_id = x.first; - current->populate_number = counter++; + auto& current = *(x.second); + current.chunk_id = x.first; + current.populate_number = counter++; } } ); @@ -46,7 +46,7 @@ TEST_F(OracleSlabCacheTest, Consecutive) { 99 }; - tatami_chunked::OracleSlabCache cache(std::make_shared >(std::move(predictions)), 100, 3); + tatami_chunked::OracleSlabCache cache(std::make_shared >(std::move(predictions)), 3); int counter = 0; int nalloc = 0; @@ -78,7 +78,7 @@ TEST_F(OracleSlabCacheTest, AllPredictions) { 15 }; - tatami_chunked::OracleSlabCache cache(std::make_unique >(std::move(predictions)), 100, 3); + tatami_chunked::OracleSlabCache cache(std::make_unique >(std::move(predictions)), 3); int counter = 0; int nalloc = 0; @@ -155,25 +155,23 @@ TEST_F(OracleSlabCacheTest, AllPredictions) { EXPECT_EQ(nalloc, 3); // respects the max cache size. } -TEST_F(OracleSlabCacheTest, LimitedPredictions) { +TEST_F(OracleSlabCacheTest, ShortCircuit) { std::vector predictions{ - 11, // Cycle 1 - 22, + 11, // Cycle 1. 12, - 31, // Cycle 2 (forced by limited predictions) - 23, - 14, - 45, // Cycle 3 - 51, 32, - 34, // Cycle 4 (forced by limited predictions) - 15, + 33, + 21, + 23, + 10, 12, - 23, // Cycle 5 (forced by limited predictions) - 25 + 44, // Cycle 2. + 46, + 23, + 24 }; - tatami_chunked::OracleSlabCache cache(std::make_unique >(std::move(predictions)), 3, 3); + tatami_chunked::OracleSlabCache cache(std::make_unique >(std::move(predictions)), 3); int counter = 0; int nalloc = 0; @@ -182,89 +180,76 @@ TEST_F(OracleSlabCacheTest, LimitedPredictions) { EXPECT_EQ(out.first->populate_number, 0); EXPECT_EQ(out.second, 1); - out = next(cache, counter, nalloc); // fetching 22. - EXPECT_EQ(out.first->chunk_id, static_cast(2)); - EXPECT_EQ(out.first->populate_number, 1); - EXPECT_EQ(out.second, 2); - out = next(cache, counter, nalloc); // fetching 12. EXPECT_EQ(out.first->chunk_id, static_cast(1)); EXPECT_EQ(out.first->populate_number, 0); EXPECT_EQ(out.second, 2); - out = next(cache, counter, nalloc); // fetching 31. + out = next(cache, counter, nalloc); // fetching 32. EXPECT_EQ(out.first->chunk_id, static_cast(3)); + EXPECT_EQ(out.first->populate_number, 1); + EXPECT_EQ(out.second, 2); + + out = next(cache, counter, nalloc); // fetching 33. + EXPECT_EQ(out.first->chunk_id, static_cast(3)); + EXPECT_EQ(out.first->populate_number, 1); + EXPECT_EQ(out.second, 3); + + out = next(cache, counter, nalloc); // fetching 21. + EXPECT_EQ(out.first->chunk_id, static_cast(2)); EXPECT_EQ(out.first->populate_number, 2); EXPECT_EQ(out.second, 1); out = next(cache, counter, nalloc); // fetching 23. EXPECT_EQ(out.first->chunk_id, static_cast(2)); - EXPECT_EQ(out.first->populate_number, 1); + EXPECT_EQ(out.first->populate_number, 2); EXPECT_EQ(out.second, 3); - out = next(cache, counter, nalloc); // fetching 14. + out = next(cache, counter, nalloc); // fetching 10. EXPECT_EQ(out.first->chunk_id, static_cast(1)); EXPECT_EQ(out.first->populate_number, 0); - EXPECT_EQ(out.second, 4); - - out = next(cache, counter, nalloc); // fetching 45. - EXPECT_EQ(out.first->chunk_id, static_cast(4)); - EXPECT_EQ(out.first->populate_number, 3); - EXPECT_EQ(out.second, 5); + EXPECT_EQ(out.second, 0); - out = next(cache, counter, nalloc); // fetching 51. - EXPECT_EQ(out.first->chunk_id, static_cast(5)); - EXPECT_EQ(out.first->populate_number, 4); - EXPECT_EQ(out.second, 1); - - out = next(cache, counter, nalloc); // fetching 32. - EXPECT_EQ(out.first->chunk_id, static_cast(3)); - EXPECT_EQ(out.first->populate_number, 2); + out = next(cache, counter, nalloc); // fetching 12. + EXPECT_EQ(out.first->chunk_id, static_cast(1)); + EXPECT_EQ(out.first->populate_number, 0); EXPECT_EQ(out.second, 2); - out = next(cache, counter, nalloc); // fetching 34. - EXPECT_EQ(out.first->chunk_id, static_cast(3)); - EXPECT_EQ(out.first->populate_number, 2); + out = next(cache, counter, nalloc); // fetching 44. + EXPECT_EQ(out.first->chunk_id, static_cast(4)); + EXPECT_EQ(out.first->populate_number, 3); EXPECT_EQ(out.second, 4); - out = next(cache, counter, nalloc); // fetching 15. - EXPECT_EQ(out.first->chunk_id, static_cast(1)); - EXPECT_EQ(out.first->populate_number, 5); - EXPECT_EQ(out.second, 5); - - out = next(cache, counter, nalloc); // fetching 12. - EXPECT_EQ(out.first->chunk_id, static_cast(1)); - EXPECT_EQ(out.first->populate_number, 5); - EXPECT_EQ(out.second, 2); + out = next(cache, counter, nalloc); // fetching 46. + EXPECT_EQ(out.first->chunk_id, static_cast(4)); + EXPECT_EQ(out.first->populate_number, 3); + EXPECT_EQ(out.second, 6); out = next(cache, counter, nalloc); // fetching 23. EXPECT_EQ(out.first->chunk_id, static_cast(2)); - EXPECT_EQ(out.first->populate_number, 6); + EXPECT_EQ(out.first->populate_number, 2); EXPECT_EQ(out.second, 3); - out = next(cache, counter, nalloc); // fetching 25. + out = next(cache, counter, nalloc); // fetching 24. EXPECT_EQ(out.first->chunk_id, static_cast(2)); - EXPECT_EQ(out.first->populate_number, 6); - EXPECT_EQ(out.second, 5); + EXPECT_EQ(out.first->populate_number, 2); + EXPECT_EQ(out.second, 4); EXPECT_EQ(nalloc, 3); // respects the max cache size. } -class OracleSlabCacheStressTest : public ::testing::TestWithParam >, public OracleSlabCacheTestMethods {}; +class OracleSlabCacheStressTest : public ::testing::TestWithParam, public OracleSlabCacheTestMethods {}; TEST_P(OracleSlabCacheStressTest, Stressed) { - auto param = GetParam(); - auto max_pred = std::get<0>(param); - auto cache_size = std::get<1>(param); + auto cache_size = GetParam(); - std::mt19937_64 rng(max_pred * cache_size + cache_size + 1); + std::mt19937_64 rng(cache_size + 1); std::vector predictions(10000); for (size_t i = 0; i < predictions.size(); ++i) { predictions[i] = rng() % 50 + 10; } - // Using limited predictions to force more cache interations. - tatami_chunked::OracleSlabCache cache(std::make_unique >(predictions.data(), predictions.size()), max_pred, cache_size); + tatami_chunked::OracleSlabCache cache(std::make_unique >(predictions.data(), predictions.size()), cache_size); int counter = 0; int nalloc = 0; @@ -280,8 +265,5 @@ TEST_P(OracleSlabCacheStressTest, Stressed) { INSTANTIATE_TEST_SUITE_P( OracleSlabCache, OracleSlabCacheStressTest, - ::testing::Combine( - ::testing::Values(3, 5, 10), // max predictions - ::testing::Values(3, 5, 10) // max cache size - ) + ::testing::Values(3, 5, 10) // max cache size );