Skip to content

Use small-vector dedup in box scanners - #2456

Merged
klayoutmatthias merged 2 commits into
KLayout:parallelizefrom
nikosavola:nikosavola/pr-boxscanner
Oct 2, 2026
Merged

klayoutmatthias merged 2 commits into
KLayout:parallelizefrom
nikosavola:nikosavola/pr-boxscanner

Conversation

@nikosavola

Copy link
Copy Markdown
Contributor
  • replaces the std::set of seen pairs in box_scanner and box_scanner2 with a per-object inline array of up to 4 partners, promoted to a hash set only for high-degree objects; entries are dropped at the same band-exit points as before
  • drops seen2 in box_scanner2, which was never queried
  • reported pairs and callback order are unchanged; new tests compare against a brute-force pair search for sparse, dense and very dense sets
  • an alternative to Hash-based dedup in BoxScanner instead of std::set<pair> #2377 that keeps its speed on dense scans without the extra memory for lonely pairs raised in that review
  • the full db suite still crashes on this branch alone because of the hierarchical connectivity race fixed in Build hierarchical cluster connections serially #2453; with both applied it passes
case std::set ms #2377 ms this ms std::set MB #2377 MB this MB
sparse/200000 40.9 37.1 36.0 29.5 29.3 29.1
lonely_pairs/200000 66.0 53.4 30.4 47.5 80.8 53.5
medium/200000 202 105 85.6 29.6 29.7 29.4
dense/50000 389 154 145 27.0 26.0 26.0
very_dense/5000 225 90.3 93.2 33.6 30.0 30.0
box_scanner2_dense/30000 743 385 200 34.0 32.0 29.7

Peak RSS is per process, one case per process (about 24 MB is the benchmark binary). Pair counts are identical for all three.

The benchmark uses the Google Benchmark suite from parallelize-rebased on my fork, which is not part of this PR. Numbers: 2x AMD EPYC 7713 (128 cores), g++ 13 -O2, OpenMP, median of 5 repetitions x 2 interleaved passes.

Benchmark code
#include "dbBoxScanner.h"

#include <cmath>
#if defined(__linux__) || defined(__APPLE__)
#include <sys/resource.h>
#endif

//  --- box scanner ---

struct BoxScannerPairReceiver
{
  BoxScannerPairReceiver () : pairs (0) { }

  void finish (const db::Box *, size_t) { }
  bool stop () const { return false; }
  void initialize () { }
  void finalize (bool) { }

  void add (const db::Box *, size_t, const db::Box *, size_t) { ++pairs; }

  size_t pairs;
};

struct BoxScanner2PairReceiver
{
  BoxScanner2PairReceiver () : pairs (0) { }

  void finish1 (const db::Box *, size_t) { }
  void finish2 (const db::SimplePolygon *, int) { }
  bool stop () const { return false; }
  void initialize () { }
  void finalize (bool) { }

  void add (const db::Box *, size_t, const db::SimplePolygon *, int) { ++pairs; }

  size_t pairs;
};

//  deterministic LCG so both the geometry and the pair count are reproducible;
//  use the high bits since the low bits of an LCG have short periods
uint32_t box_scanner_rand (uint32_t &state)
{
  state = state * 1664525u + 1013904223u;
  return state >> 13;
}

const int box_scanner_box_size = 100;

//  a square spread is chosen so a box sees about "overlaps" other boxes on average
db::Coord box_scanner_spread (int n, double overlaps)
{
  const double side = double (2 * box_scanner_box_size);
  return db::Coord (std::sqrt (double (n) * side * side / overlaps) + 0.5);
}

std::vector<db::Box> box_scanner_boxes (int n, db::Coord spread, uint32_t seed)
{
  std::vector<db::Box> boxes;
  boxes.reserve (size_t (n));
  uint32_t state = seed;
  for (int i = 0; i < n; ++i) {
    const db::Coord x = db::Coord (box_scanner_rand (state) % uint32_t (spread));
    const db::Coord y = db::Coord (box_scanner_rand (state) % uint32_t (spread));
    boxes.push_back (db::Box (x, y, x + box_scanner_box_size, y + box_scanner_box_size));
  }
  return boxes;
}

double peak_rss_mb ()
{
#if defined(__linux__)
  struct rusage usage;
  getrusage (RUSAGE_SELF, &usage);
  return double (usage.ru_maxrss) / 1024.0;
#elif defined(__APPLE__)
  struct rusage usage;
  getrusage (RUSAGE_SELF, &usage);
  return double (usage.ru_maxrss) / (1024.0 * 1024.0);
#else
  return 0.0;
#endif
}

void box_scanner_fixture (benchmark::State &state, double overlaps)
{
  const int n = int (state.range (0));
  const db::Coord spread = box_scanner_spread (n, overlaps);
  const std::vector<db::Box> boxes = box_scanner_boxes (n, spread, 42u);

  size_t pairs = 0;
  for (auto _ : state) {
    db::box_scanner<db::Box, size_t> scanner;
    scanner.reserve (boxes.size ());
    for (size_t i = 0; i < boxes.size (); ++i) {
      scanner.insert (&boxes [i], i);
    }
    BoxScannerPairReceiver receiver;
    db::box_convert<db::Box> bc;
    scanner.process (receiver, 1, bc);
    pairs = receiver.pairs;
  }

  if (pairs == 0) {
    state.SkipWithError ("box scanner did not report any pairs");
  }
  state.counters["pairs"] = benchmark::Counter (double (pairs));
  state.counters["peak_rss_mb"] = benchmark::Counter (peak_rss_mb ());
  state.SetItemsProcessed (int64_t (state.iterations ()) * int64_t (boxes.size ()));
}

//  N isolated overlapping pairs in one row: every box stays in the same y band, which
//  is the worst case for dedup storage held per band (PR 2377 review concern)
void box_scanner_lonely_pairs (benchmark::State &state)
{
  const int n = int (state.range (0));
  std::vector<db::Box> boxes;
  boxes.reserve (size_t (2 * n));
  for (int i = 0; i < n; ++i) {
    const db::Coord x = db::Coord (i) * 3 * box_scanner_box_size;
    boxes.push_back (db::Box (x, 0, x + box_scanner_box_size, box_scanner_box_size));
    boxes.push_back (db::Box (x + box_scanner_box_size / 2, 0, x + box_scanner_box_size * 3 / 2, box_scanner_box_size));
  }

  size_t pairs = 0;
  for (auto _ : state) {
    db::box_scanner<db::Box, size_t> scanner;
    scanner.reserve (boxes.size ());
    for (size_t i = 0; i < boxes.size (); ++i) {
      scanner.insert (&boxes [i], i);
    }
    BoxScannerPairReceiver receiver;
    db::box_convert<db::Box> bc;
    scanner.process (receiver, 1, bc);
    pairs = receiver.pairs;
  }

  if (pairs != size_t (n)) {
    state.SkipWithError ("unexpected pair count");
  }
  state.counters["pairs"] = benchmark::Counter (double (pairs));
  state.counters["peak_rss_mb"] = benchmark::Counter (peak_rss_mb ());
  state.SetItemsProcessed (int64_t (state.iterations ()) * int64_t (boxes.size ()));
}

void box_scanner_sparse (benchmark::State &state)
{
  box_scanner_fixture (state, 0.3);
}

void box_scanner_medium (benchmark::State &state)
{
  box_scanner_fixture (state, 6.0);
}

void box_scanner_dense (benchmark::State &state)
{
  box_scanner_fixture (state, 50.0);
}

void box_scanner_very_dense (benchmark::State &state)
{
  box_scanner_fixture (state, 300.0);
}

void box_scanner2_dense (benchmark::State &state)
{
  const int n = int (state.range (0));
  const db::Coord spread = box_scanner_spread (n, 50.0);
  const std::vector<db::Box> boxes1 = box_scanner_boxes (n, spread, 42u);
  const std::vector<db::Box> boxes2 = box_scanner_boxes (n, spread, 4242u);
  std::vector<db::SimplePolygon> polygons;
  polygons.reserve (boxes2.size ());
  for (const db::Box &box : boxes2) {
    polygons.push_back (db::SimplePolygon (box));
  }

  size_t pairs = 0;
  for (auto _ : state) {
    db::box_scanner2<db::Box, size_t, db::SimplePolygon, int> scanner;
    scanner.reserve1 (boxes1.size ());
    scanner.reserve2 (polygons.size ());
    for (size_t i = 0; i < boxes1.size (); ++i) {
      scanner.insert1 (&boxes1 [i], i);
    }
    for (size_t i = 0; i < polygons.size (); ++i) {
      scanner.insert2 (&polygons [i], int (i));
    }
    BoxScanner2PairReceiver receiver;
    db::box_convert<db::Box> bc1;
    db::box_convert<db::SimplePolygon> bc2;
    scanner.process (receiver, 1, bc1, bc2);
    pairs = receiver.pairs;
  }

  if (pairs == 0) {
    state.SkipWithError ("twofold box scanner did not report any pairs");
  }
  state.counters["pairs"] = benchmark::Counter (double (pairs));
  state.counters["peak_rss_mb"] = benchmark::Counter (peak_rss_mb ());
  state.SetItemsProcessed (int64_t (state.iterations ()) * int64_t (2 * n));
}

BENCHMARK (box_scanner_lonely_pairs)->Arg (20000)->Arg (200000)->UseRealTime ()->Unit (benchmark::kMillisecond);
BENCHMARK (box_scanner_sparse)->Arg (20000)->Arg (200000)->UseRealTime ()->Unit (benchmark::kMillisecond);
BENCHMARK (box_scanner_medium)->Arg (20000)->Arg (200000)->UseRealTime ()->Unit (benchmark::kMillisecond);
BENCHMARK (box_scanner_dense)->Arg (20000)->Arg (50000)->UseRealTime ()->Unit (benchmark::kMillisecond);
BENCHMARK (box_scanner_very_dense)->Arg (2000)->Arg (5000)->UseRealTime ()->Unit (benchmark::kMillisecond);
BENCHMARK (box_scanner2_dense)->Arg (10000)->Arg (30000)->UseRealTime ()->Unit (benchmark::kMillisecond);

This PR was carried out with AI assistance (open-weight GLM 5.3 and DeepSeek V4.1 Flash) and reviewed before submission.

nikosavola and others added 2 commits September 28, 2026 14:01
Co-Authored-By: GLM 5.3 <noreply@z.ai>
- replace the std::set of seen pairs with a per-object inline array of up to 4 partners, promoted to a hash set only for high-degree objects; entries are dropped at the same band-exit points as before
- drop seen2 in box_scanner2, which was never queried
- reported pairs and callback order are unchanged

Compared with the old set and with the dedup from PR 2377 (one process per case for peak RSS):

| case | std::set ms | PR 2377 ms | this ms | std::set MB | PR 2377 MB | this MB |
|---|---:|---:|---:|---:|---:|---:|
| sparse/200000 | 40.9 | 37.1 | 36.0 | 29.5 | 29.3 | 29.1 |
| lonely_pairs/200000 | 66.0 | 53.4 | 30.4 | 47.5 | 80.8 | 53.5 |
| medium/200000 | 202 | 105 | 85.6 | 29.6 | 29.7 | 29.4 |
| dense/50000 | 389 | 154 | 145 | 27.0 | 26.0 | 26.0 |
| very_dense/5000 | 225 | 90.3 | 93.2 | 33.6 | 30.0 | 30.0 |
| box_scanner2_dense/30000 | 743 | 385 | 200 | 34.0 | 32.0 | 29.7 |

On the vexriscv LVS flow at 1 thread, the combined fix branches (this plus the connectivity and cache fixes, which are serial-neutral) run 26.7 s against 29.0 s on parallelize-rebased.

Measured on a 2x AMD EPYC 7713 (128 cores) Linux machine, g++ 13 -O2 with OpenMP and TBB, median of 5 repetitions x 2 interleaved passes.

Co-Authored-By: GLM 5.3 <noreply@z.ai>
Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
@nikosavola
nikosavola marked this pull request as ready for review September 29, 2026 10:19
@klayoutmatthias
klayoutmatthias merged commit 4c73d6c into KLayout:parallelize Oct 2, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants