From aaeeecb69dcebeaaaa76bcbb04e6735d1dd9fab4 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Tue, 15 Sep 2026 09:23:58 +1000 Subject: [PATCH] ifcparse: sort inverse records by radix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The inverse index is sorted once after a parse (and after a parallel merge). std::sort on 1.7–5 million 12-byte records was the largest serial phase left after the parse itself. sort_records() now does a stable LSD radix sort on referenced_id, 11 bits per pass and as many passes as the largest id needs, then applies record_less within each run of equal ids, so the order is exactly what std::sort produced. Inputs under 4096 records still use std::sort. Experiment on top of the series; measured in isolation against the previous commit, five models, best of three (strict parse 1 / 12 threads, lazy open 1 / 12 threads): TXG 58 MB −7% / −16% / −10% / −19%, 210_King 147 MB −8% / −17% / −15% / −22%, OKgate22 231 MB −6% / −12% / −11% / −17%, a 107 MB model −8% / −18% / −12% / −19%, a 523 MB model of few large instances −3% / −4% / 0% / −5%. 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/storage.h | 47 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/src/ifcparse/storage.h b/src/ifcparse/storage.h index 9f3f1728f4..7645082434 100644 --- a/src/ifcparse/storage.h +++ b/src/ifcparse/storage.h @@ -467,9 +467,54 @@ namespace ifcopenshell { invalidate_materialized(); } + // Sorts records into record_less order. Large inputs go through a + // stable LSD radix sort on referenced_id (11 bits per pass, as many + // passes as the largest id needs) followed by record_less within + // each run of equal ids, which is the same order std::sort gives + // and several times faster on millions of records. + static void sort_records(std::vector& records) { + if (records.size() < 4096) { + std::sort(records.begin(), records.end(), record_less); + return; + } + uint32_t max_id = 0; + for (const auto& r : records) { + max_id = (std::max)(max_id, r.referenced_id); + } + std::vector buffer(records.size()); + constexpr unsigned bits = 11; + std::vector counts((size_t)1 << bits); + for (unsigned shift = 0; shift < 32 && (max_id >> shift) != 0; shift += bits) { + std::fill(counts.begin(), counts.end(), 0); + for (const auto& r : records) { + ++counts[(r.referenced_id >> shift) & ((1u << bits) - 1)]; + } + size_t sum = 0; + for (auto& c : counts) { + const size_t n = c; + c = sum; + sum += n; + } + for (const auto& r : records) { + buffer[counts[(r.referenced_id >> shift) & ((1u << bits) - 1)]++] = r; + } + records.swap(buffer); + } + for (auto run = records.begin(); run != records.end();) { + auto end = run + 1; + while (end != records.end() && end->referenced_id == run->referenced_id) { + ++end; + } + if (end - run > 1) { + std::sort(run, end, record_less); + } + run = end; + } + } + void sort() const { if (!sorted_) { - std::sort(base_.begin(), base_.end(), record_less); + sort_records(base_); base_.shrink_to_fit(); sorted_ = true; invalidate_materialized();