Skip to content

Speed up device comparisons in netlist compare - #2467

Open
nikosavola wants to merge 2 commits into
KLayout:masterfrom
nikosavola:nikosavola/pr-device-compare
Open

nikosavola wants to merge 2 commits into
KLayout:masterfrom
nikosavola:nikosavola/pr-device-compare

Conversation

@nikosavola

@nikosavola nikosavola commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor
  • device comparison in netlist compare (EqualDeviceParameters::less, DeviceClass::equal via Transition::operator< / ==) was about 15% of LVS cycles on the vexriscv testdata
  • new three-way DeviceCompare::compare (category first, then DeviceClass::less both ways); Transition::operator< and operator== use it instead of calling equals and then the less functor
  • identical-pointer shortcut in DeviceCompare::equals and compare; Transition::operator== checks the id field first
  • Device::parameter_value fast path is inline; DeviceClass::less and equal share one delegate lookup
  • new test checks that DeviceCompare::compare agrees with less / equals for the default delegate, tolerances, ignored and non-primary parameters
benchmark (1 thread, 6 rounds) before (ms) after (ms) speedup
netlist_compare_mos4_params/4096 1099 ± 14 831 ± 12 1.31x
netlist_compare_mos4_params/16384 5030 ± 150 3726 ± 95 1.33x
netlist_compare/2048 39.3 ± 0.5 35.4 ± 0.2 1.11x
netlist_compare/512 8.71 ± 0.06 8.00 ± 0.04 1.09x

The whole vexriscv LVS flow shows no measurable change (per-round ratios 0.93x to 1.07x).

The benchmarks use the Google Benchmark suite from parallelize-rebased on my fork, which is not part of this PR. Measured on the parallelize branch, where the changed code is the same as on master. Numbers: Intel Xeon Gold 6230, both builds alternating on one pinned core, g++ 13 -O2, mean ± standard deviation.

Benchmark code
void netlist_compare_mos4_params (benchmark::State &state)
{
  const int devices = int (state.range (0));
  //  pairwise coprime periods so every device gets a unique parameter tuple
  std::string source = "circuit ARRAY (G=GN,S=SN,B=BULK);\n";
  for (int i = 0; i < devices; ++i) {
    const double l = 0.13 + 0.05 * (i % 4);
    const double w = 0.5 + 0.25 * (i % 7);
    const double as = 0.26 * w + 0.02 * (i % 11);
    const double ad = 0.26 * w + 0.03 * (i % 13);
    const double ps = 2.0 * w + 0.1 * (i % 17);
    const double pd = 2.0 * w + 0.1 * (i % 19);
    source += "  device NMOS4 $" + std::to_string (i + 1) + " (S=SN,G=GN,D=ND" + std::to_string (i + 1) + ",B=BULK)"
      + " (L=" + std::to_string (l) + ",W=" + std::to_string (w)
      + ",AS=" + std::to_string (as) + ",AD=" + std::to_string (ad)
      + ",PS=" + std::to_string (ps) + ",PD=" + std::to_string (pd) + ");\n";
  }
  source += "end;\n";
  db::Netlist left, right;
  db::DeviceClass *left_mos = new db::DeviceClassMOS4Transistor ();
  db::DeviceClass *right_mos = new db::DeviceClassMOS4Transistor ();
  left_mos->set_name ("NMOS4");
  right_mos->set_name ("NMOS4");
  //  compare all six parameters (only L and W are primary, so the default
  //  delegate would not look at AS/AD/PS/PD)
  db::EqualDeviceParameters *params = new db::EqualDeviceParameters (db::DeviceClassMOS3Transistor::param_id_L, 0.0, 0.0);
  *params += db::EqualDeviceParameters (db::DeviceClassMOS3Transistor::param_id_W, 0.0, 0.0);
  *params += db::EqualDeviceParameters (db::DeviceClassMOS3Transistor::param_id_AS, 0.0, 0.0);
  *params += db::EqualDeviceParameters (db::DeviceClassMOS3Transistor::param_id_AD, 0.0, 0.0);
  *params += db::EqualDeviceParameters (db::DeviceClassMOS3Transistor::param_id_PS, 0.0, 0.0);
  *params += db::EqualDeviceParameters (db::DeviceClassMOS3Transistor::param_id_PD, 0.0, 0.0);
  left_mos->set_parameter_compare_delegate (params);
  left.add_device_class (left_mos);
  right.add_device_class (right_mos);
  left.from_string (source.c_str ());
  right.from_string (source.c_str ());

  for (auto _ : state) {
    db::NetlistComparer comparer;
    comparer.set_dont_consider_net_names (true);
    const bool equal = comparer.compare (&left, &right);
    benchmark::DoNotOptimize (equal);
    if (! equal) {
      state.SkipWithError ("identical netlists did not compare equal");
      break;
    }
  }
  state.SetItemsProcessed (int64_t (state.iterations ()) * devices);
}

BENCHMARK (netlist_compare_mos4_params)->Arg (4096)->Arg (16384)->UseRealTime ()->Unit (benchmark::kMillisecond);

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

- EqualDeviceParameters::less no longer builds a std::set per call, Device::parameter_value is inline
- new three-way DeviceClass::compare used by Transition::operator< / ==, sharing one delegate lookup with less and equal
- identical-pointer early-out in DeviceCompare::equals

| benchmark, 1 thread | before (ms) | after (ms) |
|---|---:|---:|
| netlist_compare_mos4_params/16384 (n=6) | 2759 ± 14 | 1199 ± 11 |
| netlist_compare/2048 (n=20) | 22.00 ± 0.19 | 18.03 ± 0.14 |
| vexriscv LVS compare phase (n=10) | 12130 ± 70 | 11200 ± 80 |

Measured on 2x Intel Xeon Gold 6148 (40 cores), g++ 13 -O2, mean ± standard deviation.

Co-Authored-By: GLM 5.3 <noreply@z.ai>
@nikosavola
nikosavola force-pushed the nikosavola/pr-device-compare branch from bb16b00 to 49151a1 Compare September 30, 2026 11:02
@nikosavola
nikosavola changed the base branch from parallelize to master September 30, 2026 11:02
@nikosavola
nikosavola marked this pull request as ready for review September 30, 2026 11:08
@klayoutmatthias

Copy link
Copy Markdown
Collaborator

Why not simply extending the DeviceCompare functor with a three-way-compare and using that one in ``Transition::operator<`. That would be much less intrusive and there is reuse potential.

Replacing a set by a linear search IMHO is not an optimization. Maybe in small cases, but not in general.

- the three-way compare moves from a new DeviceClass::compare into the
  DeviceCompare functor where the less/equal predicates live
- Transition::make_key uses the functor compare; same single-call win
- EqualDeviceParameters::less keeps the std::set membership test
@nikosavola

Copy link
Copy Markdown
Contributor Author

Good idea, done. DeviceCompare now has a three-way compare and make_key uses it. I removed DeviceClass::compare again. I also put the std::set back in EqualDeviceParameters::less, as you suggested. I will update the numbers in the description.

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