ifcparse: sort inverse records by radix

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wcN7XquTfUi4vsKQ4KchL
This commit is contained in:
Dion Moult
2026-09-15 09:23:58 +10:00
parent 17ec5d4504
commit aaeeecb69d
+46 -1
View File
@@ -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<inverse_record>& 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<inverse_record> buffer(records.size());
constexpr unsigned bits = 11;
std::vector<size_t> 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();