ifcparse: parse instances in parallel

The DATA section is split into one chunk per thread and each worker runs
the same per-instance reader as the serial parse over its own reader,
storage, inverse records and simple-type list; the results are merged in
file order, so instance order, GlobalId precedence and inverse records
are identical to the serial parse. Reference resolution then splits over
the same threads: each instance's slots are its own and the name table
is complete and read-only by then. The default is one thread per core,
capped at 16; IFCOPENSHELL_PARSE_THREADS or file::parse_threads()
overrides it, and 1 parses as before.

The instance headers are read by one loop, for_each_instance_header(),
shared with the lazy index: it looks declarations up once per keyword,
passes over a bypassed instance's attribute list and slides past a stray
keyword the way the serial reader does (the lazy index therefore no
longer falls back on one).

Finding the split points is the one place that looks at raw bytes rather
than tokens, because tokenizing the file serially first would leave
nothing to parallelise. It applies three rules: a string starts and ends
at a quote and cannot span a line, and a comment runs from /* to */; a
split is a '#' that starts a line outside both. Getting a string's end
wrong can only lose a candidate, never accept a wrong one, since no
string contains a newline. The equality test puts a comment holding a
fake instance and a string holding "/*" between the chunks.

file_reader gains for_each_span(), which hands a byte range out span by
span (one span for a buffer, one per page for the paged reader), and
reopen(), a reader over the same file for another thread.

TXG 58 MB / 210_King 147 MB / OKgate22 231 MB, 12 threads: 0.44 / 1.24 /
2.01 s against 1.07 / 2.75 / 5.12 s on one thread; memory after the parse
within 1–4%, peak +5–4%.

This commit was written by an AI coding tool and has not been verified by
a human.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wcN7XquTfUi4vsKQ4KchL
This commit is contained in:
Dion Moult
2026-09-14 07:30:18 +10:00
parent a92bebd73e
commit b1cba757b8
6 changed files with 590 additions and 94 deletions
+25
View File
@@ -454,6 +454,19 @@ namespace ifcopenshell {
}
// Finalizes bulk loading. Subsequent add() calls go to the delta.
// Takes over another index's records, e.g. one built by a parser
// worker. Both must still be in bulk-load mode (no delta).
void append(inverse_index&& other) {
if (base_.empty()) {
base_ = std::move(other.base_);
} else {
base_.insert(base_.end(), other.base_.begin(), other.base_.end());
}
sorted_ = false;
other.clear();
invalidate_materialized();
}
void sort() const {
if (!sorted_) {
std::sort(base_.begin(), base_.end(), record_less);
@@ -602,6 +615,18 @@ namespace ifcopenshell {
std::vector<unsigned> lazy_bypassed_;
std::vector<std::pair<uint32_t, uint64_t>> lazy_offsets_;
bool index_lazily(const std::string& path, const ifcopenshell::schema_definition*& schema, unsigned int& max_id, const std::set<std::string>& types_to_bypass);
// Number of threads read_from_stream() may use to parse instances;
// 1 parses serially. Set by file::initialize().
unsigned parse_threads = 1;
// Parses the DATA section with `threads` workers, each running the
// same per-instance reader over its own chunk, and merges the
// results in file order. Returns false, without side effects, when
// the file is too small to be worth it or no split points were
// found; the caller then parses serially.
template <typename Reader>
bool read_instances_parallel(Reader* stream, const ifcopenshell::schema_definition* schema, const std::set<std::string>& types_to_bypass, unsigned int& max_id, unsigned threads, std::vector<unsigned>& bypassed, unresolved_references& mixed_references, std::vector<shared_pointer_type>& instances);
void materialize(instance_data* data);
typedef std::map<const ifcopenshell::declaration*, std::vector<express::base>> entities_by_type;