From d647050bcf801b65ee72417320cd0cb5e41bcc16 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Fri, 11 Sep 2026 20:17:42 +1000 Subject: [PATCH] ifcparse: cache the current page in the paged file reader Every peek(), get() and SWAR word read on a paged reader went through the page cache's hash map and LRU list, so tokenizing through pages ran at a fifth of the speed of the in-memory buffer. The reader now remembers the page its cursor was last on and serves reads that fall inside it from the pointer, revalidated against an eviction counter on the implementation so a page that left the cache is never read through a stale pointer. Reads that straddle a page boundary take the existing paths. The whole tokenizer over each file, 64 KB pages, 64 cached (4 MB): in-memory buffer paged before paged after TXG 58 MB 200 MB/s 42 MB/s 142 MB/s 210_King 148 MB 181 MB/s 34 MB/s 118 MB/s OKgate22 232 MB 197 MB/s 38 MB/s 128 MB/s This is the step that makes reading in pages a candidate for the default path rather than a fallback; the remaining gap is the page fetch itself. This commit was written by an AI coding tool and has not been verified by a human. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_013wcN7XquTfUi4vsKQ4KchL --- src/ifcparse/file_reader.cpp | 1 + src/ifcparse/file_reader.h | 63 ++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/src/ifcparse/file_reader.cpp b/src/ifcparse/file_reader.cpp index 549b7de335..fa739cb280 100644 --- a/src/ifcparse/file_reader.cpp +++ b/src/ifcparse/file_reader.cpp @@ -209,6 +209,7 @@ void paged_file_impl::evict_() const { const size_t victim = lru_.back(); lru_.pop_back(); map_.erase(victim); + ++evictions_; } #ifdef USE_MMAP diff --git a/src/ifcparse/file_reader.h b/src/ifcparse/file_reader.h index 66748b91f3..50322125be 100644 --- a/src/ifcparse/file_reader.h +++ b/src/ifcparse/file_reader.h @@ -140,6 +140,9 @@ public: if (cursor_ >= size()) { throw std::out_of_range("peek at EOF"); } + if (const char* p = cached_(cursor_, 1)) { + return *p; + } return impl_->get(cursor_); } @@ -147,6 +150,11 @@ public: if (remaining() < sizeof(uint64_t)) { throw std::out_of_range("peek_u64 at EOF"); } + if (const char* p = cached_(cursor_, sizeof(uint64_t))) { + uint64_t value; + std::memcpy(&value, p, sizeof(value)); + return value; + } return impl_->get_u64(cursor_); } @@ -154,6 +162,11 @@ public: if (remaining() < sizeof(uint32_t)) { throw std::out_of_range("peek_u32 at EOF"); } + if (const char* p = cached_(cursor_, sizeof(uint32_t))) { + uint32_t value; + std::memcpy(&value, p, sizeof(value)); + return value; + } return impl_->get_u32(cursor_); } @@ -187,12 +200,49 @@ public: } char get(size_t position) const { + if (const char* p = cached_(position, 1)) { + return *p; + } return impl_->get(position); } private: std::shared_ptr impl_; size_t cursor_ = 0; + + // For the paged implementation: the page the cursor was last on, so + // consecutive reads don't each go through the page cache. The pointer + // is revalidated against the cache's eviction count. + mutable const char* cached_data_ = nullptr; + mutable size_t cached_begin_ = 0; + mutable size_t cached_end_ = 0; + mutable size_t cached_evictions_ = 0; + + // A pointer to `count` bytes at `position` if they lie in one page, + // else nullptr. Always nullptr for a contiguous implementation, whose + // get() is already direct. + const char* cached_(size_t position, size_t count) const { + if constexpr (std::is_same_v) { + if (cached_data_ != nullptr && position >= cached_begin_ && position + count <= cached_end_ && cached_evictions_ == impl_->evictions()) { + return cached_data_ + (position - cached_begin_); + } + const size_t page_size = impl_->page_size(); + const size_t index = position / page_size; + const auto page = impl_->page(index); + cached_data_ = page.first; + cached_begin_ = index * page_size; + cached_end_ = cached_begin_ + page.second; + cached_evictions_ = impl_->evictions(); + if (position + count <= cached_end_) { + return cached_data_ + (position - cached_begin_); + } + return nullptr; + } else { + (void)position; + (void)count; + return nullptr; + } + } }; class IFC_PARSE_API full_buffer_impl { @@ -243,6 +293,18 @@ public: paged_file_impl(const std::string& path, size_t page_size, size_t page_capacity); ~paged_file_impl(); + // One page's bytes; the page stays valid until capacity() further pages + // have been fetched. + std::pair page(size_t index) const { + const auto& p = fetchPage_(index); + return {p.data.data(), p.data.size()}; + } + size_t page_size() const { return page_size_; } + size_t capacity() const { return capacity_; } + const std::string& path() const { return fn_; } + // Incremented whenever a page leaves the cache, so a pointer into a + // page can be checked for validity cheaply. + size_t evictions() const { return evictions_; } size_t size() const; char get(size_t position) const; uint32_t get_u32(size_t position) const; @@ -262,6 +324,7 @@ private: size_t capacity_ = 8; mutable std::list lru_; mutable std::unordered_map map_; + mutable size_t evictions_ = 0; }; #ifdef USE_MMAP