Skip to content

Perf: full-model traversal (IFC105/IFC106) visited-set is O(n^2); plus gaps and a bug in IVS-879 #527

Description

@jmecosta

Summary

While profiling the engine in-process (in-memory parser) on a 54 MB IFC model against IFC106 (Resource entities should be referenced by rooted entity — identical logic to IFC105), the rule took ~128 s, almost entirely in two framework-level issues that affect every rule. I see IVS-879_Improve_Performance_of_ifc-gherkin-rules already targets part of this, so this issue is to (a) share the profiling data, (b) flag two gaps and one bug in that branch, and (c) point out the biggest win it doesn't yet touch. Happy to send PRs for any/all of these.

Profile (cProfile, sorted by self time)

Step-level timing for IFC106 on the 54 MB model, before vs. after the fixes below:

Step Before After
Given a traversal over the full model … 57.4 s 17.9 s
Given an entity instance 5.7 s 5.6 s
Given [its entity type] is not 'IfcRoot' … 42.1 s 19.6 s
Then it must be referenced by … IfcRoot … 13.9 s 9.4 s
rule total 128.1 s 59.7 s (2.1×)

Top self-time before: numpy._unique_hash 24.0 s, then ~12 s across inspect._signature_from_function / inspect.getargs / compile. Both vanish after the changes; outputs are byte-identical (24,863 outcomes).


1. model_traversal.pyvisited set is O(n²) (not addressed by IVS-879)

features/steps/steps/model_traversal.py#L80 uses misc.ContiguousSet as the visited set for the full-model traversal. ContiguousSet.commit() runs np.union1d(self._arr, pend_arr) over the entire growing array on every batch (misc.py#L303) — effectively O(n²/batch). On this model that's the 24 s of numpy._unique_hash and the dominant cost of the 57 s traversal step.

The traversal only needs membership (inst.id() not in context.visited_instances), so a plain set[int] is O(1) per op. Swapping it took the traversal step 57 s → 18 s, identical results:

    visited = set()

    def visit(inst):
        inst_id = inst.id()
        if inst_id in visited:
            return
        visited.add(inst_id)
        ...
    for inst in context.model.by_type(entity_name):
        visit(inst)
    context.visited_instances = visited   # drop visited.commit()

(Also lets you drop the now-unused path accumulation in visit.) If ContiguousSet is kept elsewhere for the rocksdb/lazy path, this single call site can still use a plain set — it's membership-only.

2. IVS-879 signature cache misses the inspect.getargs sites

The branch adds _SIGNATURE_CACHE / get_cached_signature(fn) for the apply_operation site (#L170) — nice. But three other per-call introspection sites remain uncached and run per-instance:

These together were ~half of the ~12 s introspection cost. A small memoized helper covers all sites (signature + getargs), keyed on fn / fn.__code__:

@functools.lru_cache(maxsize=None)
def _fn_accepts_param(fn, name):
    try: return name in inspect.signature(fn).parameters
    except (TypeError, ValueError): return False

@functools.lru_cache(maxsize=None)
def _code_accepts_arg(code, name):
    try: return name in inspect.getargs(code).args
    except (TypeError, ValueError): return False

3. Bug in IVS-879's ContiguousSet.commit()

The branch's rewrite of commit() assigns the result of numpy.ndarray.sort(), which sorts in place and returns None:

# IVS-879 version
self._arr = pend_arr.sort()                                  # -> None
...
self._arr = np.unique(np.concatenate((self._arr, pend_arr))).sort()  # -> None

After the first commit self._arr is None, which will break subsequent _in_array / __contains__. np.unique(...) already returns sorted, so:

if self._arr.size == 0:
    self._arr = np.sort(pend_arr)            # or np.unique(pend_arr)
else:
    self._arr = np.unique(np.concatenate((self._arr, pend_arr)))

(Note: even fixed, commit() per batch is still O(n log n) over the whole array; for the membership-only visited use in #1, a plain set avoids this entirely.)


Repro / notes

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions