New defects (all PASS): - exim-0001: same_hosts() MX-segment O(H²) → AVL set O(H log H), 10.5x at H=20 - minecraft-0001: DependencySorter.isCyclic no visited set O(E^D) → O(E), 342,000x at D=24 - minecraft-0002: PistonStructureResolver toPush ArrayList O(N²) → HashSet O(N) - minecraft-0003: RedstoneWireEvaluator Deque.contains O(N²) → HashSet O(N) - minecraft-0004: MoveThroughVillageGoal visited List O(N²) → HashSet O(N) - mpich-0001: group_lpid_to_rank O(N²) → HashMap O(N), 313x at N=1000 - ompi-0001: group_overlap process-name scan O(N×M) → HashMap O(N+M), 2048x - pcl-0001: RegionGrowing::getSegmentFromPoint O(C×S) → point_labels[] O(1), 50000x CLEAN confirmed: esbuild, express, koa, ktor, lucene, mpich-recvq, ompi-startup, prosody, roda, rust/rustc-wave2, signal-server, solana, wiredtiger, wireguard-tools, linux-kernel (pointer to linux/)
3.1 KiB
Classification
| Field | Value |
|---|---|
| CWE | CWE-407 Inefficient Algorithmic Complexity |
| Severity | HIGH |
| Component | segmentation/include/pcl/segmentation/impl/region_growing.hpp:594 |
| Also | segmentation/include/pcl/segmentation/impl/region_growing_rgb.hpp:726 |
| Function | RegionGrowing::getSegmentFromPoint() / RegionGrowingRGB::getSegmentFromPoint() |
| Hot path | Public API — called once per query point; any loop over N query points = O(N²) |
| Status | PATCHED (unit test PASS) |
Defect
getSegmentFromPoint() locates the cluster containing a given point index by
scanning all clusters with std::find:
// region_growing.hpp:594-605
for (const auto& i_segment : clusters_)
{
const auto it = std::find (i_segment.indices.cbegin (), i_segment.indices.cend (), index);
if (it != i_segment.indices.cend())
{
cluster.indices = i_segment.indices; // copy
break;
}
}
This is O(C × S) per call, where:
- C = number of clusters (e.g. 1,000 for a 100K-point LiDAR scan)
- S = average points per cluster (e.g. 100)
The lookup cost is O(C × S) = O(N) when C × S ≈ N.
The fix is already in the data structure. point_labels_ is a dense array
populated by applySmoothRegionGrowingAlgorithm() and used verbatim in
assembleRegions():
// assembleRegions():538
const auto segment_index = point_labels_[i_point];
clusters_[segment_index].indices[point_index] = i_point;
So clusters_[point_labels_[index]] is an O(1) direct array lookup that
gives exactly the same result as the O(C × S) scan.
RGB variant
RegionGrowingRGB::getSegmentFromPoint() inherits the same pattern. After
applyRegionMergingAlgorithm(), the two-level mapping is:
point_labels_[point] → initial segment index, then
segment_labels_[seg] → merged homogeneous region index.
The RGB assembleRegions() uses this at lines 566–567:
int index = point_labels_[point_index];
index = segment_labels_[index];
clusters_[index].indices[counter[index]] = point_index;
After the compaction sweep that removes empty entries, region_idx remains a
valid index for non-empty clusters (empty slots are swapped out, but any point
belonging to a surviving cluster still maps correctly via the two-level index).
Complexity
| Scenario | Before | After |
|---|---|---|
| Single lookup, C=1K clusters × S=100pts | O(50,000) avg | O(1) |
| Query all N=100K points in a loop | O(N × C × S) = O(5 × 10⁹) | O(N) = O(100K) |
| Speedup ratio (N=100K query loop) | baseline | ~50,000× |
At N=1,000 query points with C=100 clusters × S=100:
| Metric | Before | After |
|---|---|---|
| Op count | ~5,000,000 | ~1,000 |
| Ratio | 5000× | 1× |
Patch
See pcl-0001-region-growing-get-segment-linear-scan.patch.
Base class fix: replace the for...std::find loop with a direct index
clusters_[point_labels_[index]].
RGB class fix: replace the for...std::find loop with the two-level index
clusters_[segment_labels_[point_labels_[index]]], guarded by bounds checks.
Date
2026-03-29