Compare commits

...

2 Commits

Author SHA1 Message Date
Petru Conduraru eca82d1d18 IfcGeom: make no-parallel-mapping and permissive-shape-reuse usable with threads #6712
Iterator::initialize() disabled mapping caching whenever the iterator
was constructed with more than one thread, including in
no-parallel-mapping mode where all mapping happens upfront on the
calling thread. Without the cache, the shared representation of a
mapped item was remapped once per product (9210 times in the first
attachment of #6712), so initialize() appeared to hang, and the
permissive-shape-reuse folding, which merges tasks by cached item
pointer identity, silently never folded. Both effects were reported in
the issue thread.

Caching is now only disabled when parallel mapping will actually run
in the worker threads. In no-parallel-mapping mode the iterator falls
back to sequential processing, since cache-shared taxonomy items are
not yet safe to convert concurrently (cf. the immutability todo).
Additionally, permissive-shape-reuse now implies no-parallel-mapping
inside the iterator itself, mirroring what IfcConvert already forced,
so library users get the folding without knowing about the coupling.

With this, the reporter's configuration (permissive-shape-reuse with
hardware_concurrency threads) completes the 12 MB attachment in 1.4 s
instead of hanging; output is identical to the previously working
single-threaded configuration.

Generated with the assistance of an AI coding tool.
2026-07-21 14:41:22 +03:00
Petru Conduraru 484b2e9930 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.
2026-07-21 14:41:12 +03:00
5 changed files with 167 additions and 45 deletions
+18 -2
View File
@@ -23,10 +23,26 @@ bool IfcGeom::Iterator::initialize() {
}
time_points[0] = high_resolution_clock::now();
if (settings_.get<ifcopenshell::geometry::settings::PermissiveShapeReuse>().get() && !settings_.get<ifcopenshell::geometry::settings::NoParallelMapping>().get()) {
// Folding tasks based on permissive shape reuse requires the upfront mapping
settings_.get<ifcopenshell::geometry::settings::NoParallelMapping>().value = true;
logger_.Notice("SYS", 36, "Enabled no-parallel-mapping due to permissive-shape-reuse");
}
std::vector<ifcopenshell::geometry::geometry_conversion_task> reps;
if (num_threads_ != 1) {
// @todo this shouldn't be necessary with properly immutable taxonomy items
converter_->mapping()->use_caching() = false;
if (!settings_.get<ifcopenshell::geometry::settings::NoParallelMapping>().get()) {
// @todo this shouldn't be necessary with properly immutable taxonomy items
converter_->mapping()->use_caching() = false;
} else {
// The upfront mapping below runs on this thread with caching, which is
// what makes instance reuse (and the permissive-shape-reuse folding)
// effective, but cache-shared taxonomy items cannot be converted
// concurrently yet, cf. the immutability @todo above
num_threads_ = 1;
logger_.Notice("SYS", 37, "Processing sequentially due to no-parallel-mapping");
}
}
try {
converter_->mapping()->get_representations(reps, filters_);
+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()) {