From 484b2e993099c426aaaff9a79083269554d38442 Mon Sep 17 00:00:00 2001 From: Petru Conduraru Date: Tue, 21 Jul 2026 14:41:12 +0300 Subject: [PATCH] IfcParse: contiguous fast paths for file scanning #6712 The FileReader rework routed every character read through two or three virtual calls (eof, size, get), which dominated profiles of IfcParse::IfcFile construction on large models. Backends with stable in memory storage (full buffer, mmap) now expose their buffer so the per character accessors inline into plain array reads, and the three hottest consumers (IfcSpfLexer::Next, IfcSpfLexer::TokenString and the string decoder) scan that buffer in bulk. Strings of printable ASCII without escapes, the overwhelmingly common case, now decode with a single append instead of a per character state machine; anything else falls back to the existing code path unchanged. Opening the 237 MB attachment of #6712 drops from 6.2 s to 3.8 s on an M-series laptop, with byte identical parse results on all repository fixture files and the issue attachments. Generated with the assistance of an AI coding tool. --- src/ifcparse/FileReader.cpp | 53 +++++++++--------------- src/ifcparse/FileReader.h | 50 +++++++++++++++++++---- src/ifcparse/IfcCharacterDecoder.cpp | 29 ++++++++++++++ src/ifcparse/IfcParse.cpp | 60 ++++++++++++++++++++++++++++ 4 files changed, 149 insertions(+), 43 deletions(-) diff --git a/src/ifcparse/FileReader.cpp b/src/ifcparse/FileReader.cpp index e16d26e641..8f9243dbf2 100644 --- a/src/ifcparse/FileReader.cpp +++ b/src/ifcparse/FileReader.cpp @@ -57,6 +57,7 @@ struct FullBufferImpl final : FileReader::Impl { if (pos >= buf_.size()) throw std::out_of_range("get out of range"); return buf_[pos]; } + const char* contiguous_data() const override { return buf_.data(); } }; struct PagedFileImpl final : FileReader::Impl { @@ -166,6 +167,8 @@ struct MMapImpl final : FileReader::Impl { if (pos >= size_) throw std::out_of_range("get out of range"); return map_.data()[pos]; } + + const char* contiguous_data() const override { return map_.data(); } }; #endif @@ -237,6 +240,7 @@ IfcParse::FileReader::FileReader(const std::string& fn) : cursor_(0) { impl_ = std::make_shared(fn); + init_contiguous_(); } IfcParse::FileReader::FileReader(const std::string& fn, const mmap_tag&) @@ -244,6 +248,7 @@ IfcParse::FileReader::FileReader(const std::string& fn, const mmap_tag&) { #ifdef USE_MMAP impl_ = std::make_shared(fn); + init_contiguous_(); #else (void)fn; throw std::runtime_error("IfcParse::FileReader: mmap_tag specified but library not compiled with USE_MMAP"); @@ -254,18 +259,31 @@ IfcParse::FileReader::FileReader(const caller_fed_tag&) : cursor_(0) { impl_ = std::make_shared(); + init_contiguous_(); } IfcParse::FileReader::FileReader(const std::string& content, const caller_fed_tag&) { impl_ = std::make_shared(); impl_->pushNextPage(content); + init_contiguous_(); } IfcParse::FileReader::FileReader(const std::string& fn, size_t page_size, size_t page_capacity) : cursor_(0) { impl_ = std::make_shared(fn, page_size, page_capacity); + init_contiguous_(); +} + +void IfcParse::FileReader::init_contiguous_() { + contiguous_ = impl_->contiguous_data(); + contiguous_size_ = contiguous_ != nullptr ? impl_->size() : 0; +} + +char IfcParse::FileReader::peek_paged_() const { + if (cursor_ >= impl_->size()) throw std::out_of_range("peek at EOF"); + return impl_->get(cursor_); } FileReader FileReader::clone() const { @@ -274,25 +292,6 @@ FileReader FileReader::clone() const { return c; } -void FileReader::seek(size_t pos) { - if (pos > impl_->size()) throw std::out_of_range("seek out of range"); - cursor_ = pos; -} - -size_t FileReader::tell() const { return cursor_; } - -size_t FileReader::size() const { return impl_->size(); } - -char FileReader::peek() const { - if (cursor_ >= impl_->size()) throw std::out_of_range("peek at EOF"); - return impl_->get(cursor_); -} - -void FileReader::increment(size_t n) { - if (cursor_ + n > impl_->size()) throw std::out_of_range("increment past EOF"); - cursor_ += n; -} - void IfcParse::FileReader::pushNextPage(const std::string& data) { impl_->pushNextPage(data); @@ -308,19 +307,3 @@ void IfcParse::FileReader::dropPages(size_t up_to_pos) impl_->dropPages(up_to_pos); } -bool IfcParse::FileReader::eof() const -{ - return cursor_ >= impl_->size(); -} - -char IfcParse::FileReader::read() -{ - auto c = peek(); - increment(1); - return c; -} - -char IfcParse::FileReader::get(size_t offset) const -{ - return impl_->get(offset); -} diff --git a/src/ifcparse/FileReader.h b/src/ifcparse/FileReader.h index 164c5826a6..b6000ff409 100644 --- a/src/ifcparse/FileReader.h +++ b/src/ifcparse/FileReader.h @@ -74,21 +74,33 @@ public: /// \brief Seek to an absolute byte position. /// \throws std::out_of_range if pos > size(). - void seek(size_t pos); + void seek(size_t pos) { + if (pos > size()) throw std::out_of_range("seek out of range"); + cursor_ = pos; + } /// \brief Return the current cursor position. - size_t tell() const; + size_t tell() const { return cursor_; } /// \brief Total file size in bytes. - size_t size() const; + size_t size() const { return contiguous_ ? contiguous_size_ : impl_->size(); } /// \brief Peek the byte at the current cursor. /// \throws std::out_of_range at EOF. - char peek() const; + char peek() const { + if (contiguous_) { + if (cursor_ >= contiguous_size_) throw std::out_of_range("peek at EOF"); + return contiguous_[cursor_]; + } + return peek_paged_(); + } /// \brief Advance the cursor by n bytes (default 1). /// \throws std::out_of_range if advancing crosses EOF. - void increment(size_t n = 1); + void increment(size_t n = 1) { + if (cursor_ + n > size()) throw std::out_of_range("increment past EOF"); + cursor_ += n; + } /// \brief Push the next sequential page (pushed backend only). /// \param data Contents of the page. @@ -102,18 +114,35 @@ public: /// \brief Returns true if the cursor is at or beyond the end of available data. /// For the pushed backend, EOF means all pushed bytes have been consumed. - bool eof() const; + bool eof() const { return cursor_ >= size(); } /// \brief Equivalent of peek() followed by increment(1) - char read(); + char read() { + auto c = peek(); + increment(1); + return c; + } /// \brief Equivalent of peek() followed by increment(1) - char get(size_t offset) const; + char get(size_t offset) const { + if (contiguous_) { + if (offset >= contiguous_size_) throw std::out_of_range("get out of range"); + return contiguous_[offset]; + } + return impl_->get(offset); + } + + /// \brief Entire backing buffer, indexed by absolute file offset, when the backend + /// is stable contiguous memory (full-buffer or mmap); nullptr otherwise. + const char* contiguous_buffer() const { return contiguous_; } struct Impl { virtual ~Impl() = default; virtual size_t size() const = 0; virtual char get(size_t pos) const = 0; + /// \brief Backends with a stable in-memory buffer return it here so that the + /// per-character accessors can bypass virtual dispatch; default nullptr. + virtual const char* contiguous_data() const { return nullptr; } /// \brief Backend may support pushing pages; default throws. virtual void pushNextPage(const std::string&) { throw std::logic_error("push_next_page: backend does not support pushed mode"); @@ -126,6 +155,11 @@ public: private: std::shared_ptr impl_; size_t cursor_ = 0; + const char* contiguous_ = nullptr; + size_t contiguous_size_ = 0; + + void init_contiguous_(); + char peek_paged_() const; }; } // namespace IfcParse diff --git a/src/ifcparse/IfcCharacterDecoder.cpp b/src/ifcparse/IfcCharacterDecoder.cpp index 019499a49e..453d8a2cf6 100644 --- a/src/ifcparse/IfcCharacterDecoder.cpp +++ b/src/ifcparse/IfcCharacterDecoder.cpp @@ -89,6 +89,35 @@ IfcCharacterDecoder::~IfcCharacterDecoder() { namespace { std::string read_string(IfcParse::FileReader& stream_, Logger& logger, IfcParse::IfcCharacterDecoder::ConversionMode mode, char substitution_character) { + // Fast path for strings of printable ASCII without escapes or doubled + // apostrophes, which decode identically in all conversion modes + if (const char* buf = stream_.contiguous_buffer()) { + const size_t begin = stream_.tell(); + const size_t n = stream_.size(); + size_t pos = begin; + bool simple = false; + while (pos < n) { + const auto character = static_cast(buf[pos]); + if (character == '\'') { + simple = true; + break; + } + if (character == '\\' || character < 0x20 || character >= 0x7f) { + break; + } + ++pos; + } + if (simple && pos + 1 < n && buf[pos + 1] != '\'') { + std::string result; + result.reserve(pos - begin + 2); + result.push_back('\''); + result.append(buf + begin, pos - begin); + result.push_back('\''); + stream_.seek(pos + 1); + return result; + } + } + std::u32string builder_; unsigned int parse_state = 0; diff --git a/src/ifcparse/IfcParse.cpp b/src/ifcparse/IfcParse.cpp index bc6df697af..66d5ae452f 100644 --- a/src/ifcparse/IfcParse.cpp +++ b/src/ifcparse/IfcParse.cpp @@ -190,6 +190,38 @@ Token IfcSpfLexer::Next() { if (character == '\'') { // If a string is encountered defer processing to the IfcCharacterDecoder str = *decoder_; + } else if (const char* buf = stream->contiguous_buffer()) { + // Bulk scan on contiguous storage to avoid per-character accessor calls + const size_t size = stream->size(); + size_t end = pos + 1; + bool has_whitespace = false; + while (end < size) { + character = buf[end]; + if (character == '(' || + character == ')' || + character == '=' || + character == ',' || + character == ';' || + character == '/') { + break; + } + if (character == ' ' || character == '\r' || character == '\n' || character == '\t') { + has_whitespace = true; + } + ++end; + } + if (!has_whitespace) { + str.assign(buf + pos, end - pos); + } else { + str.clear(); + for (size_t i = pos; i < end; ++i) { + character = buf[i]; + if (!(character == ' ' || character == '\r' || character == '\n' || character == '\t')) { + str.push_back(character); + } + } + } + stream->seek(end); } else { str.assign(&character, 1); @@ -219,6 +251,34 @@ Token IfcSpfLexer::Next() { // void IfcSpfLexer::TokenString(size_t offset, std::string& buffer) { buffer.clear(); + if (const char* buf = stream->contiguous_buffer()) { + const size_t size = stream->size(); + size_t pos = offset; + while (pos < size) { + char character = buf[pos]; + if (!buffer.empty() && (character == '(' || + character == ')' || + character == '=' || + character == ',' || + character == ';' || + character == '/')) { + break; + } + ++pos; + if (character == ' ' || + character == '\r' || + character == '\n' || + character == '\t') { + continue; + } + if (character == '\'') { + buffer = decoder_->get(pos); + break; + } + buffer.push_back(character); + } + return; + } auto local_stream = *this->stream; local_stream.seek(offset); while (!local_stream.eof()) {