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.
This commit is contained in:
Petru Conduraru
2026-07-21 14:41:12 +03:00
parent e52e5e2e58
commit 484b2e9930
4 changed files with 149 additions and 43 deletions
+18 -35
View File
@@ -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<FullBufferImpl>(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<MMapImpl>(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<PushedSequentialImpl>();
init_contiguous_();
}
IfcParse::FileReader::FileReader(const std::string& content, const caller_fed_tag&)
{
impl_ = std::make_shared<PushedSequentialImpl>();
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<PagedFileImpl>(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);
}
+42 -8
View File
@@ -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> impl_;
size_t cursor_ = 0;
const char* contiguous_ = nullptr;
size_t contiguous_size_ = 0;
void init_contiguous_();
char peek_paged_() const;
};
} // namespace IfcParse
+29
View File
@@ -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<unsigned char>(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;
+60
View File
@@ -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()) {