Skip to content

Add benchmark suite for shortest path algorithms on weighted graphs#8059

Merged
MridulS merged 9 commits intonetworkx:mainfrom
amcandio:weighted_benchmarks
Oct 18, 2025
Merged

Add benchmark suite for shortest path algorithms on weighted graphs#8059
MridulS merged 9 commits intonetworkx:mainfrom
amcandio:weighted_benchmarks

Conversation

@amcandio
Copy link
Copy Markdown
Contributor

@amcandio amcandio commented May 21, 2025

This PR introduces a new benchmarking suite for evaluating shortest path algorithms on weighted graphs. This benchmark was used to evaluate #8023. It includes the following changes:

Graphs added

  • WeightedGraphBenchmark class:
    • Benchmarks networkx.single_source_dijkstra on various weighted graphs.
    • Supports both path_graph (sizes: 1,000–20,000) and erdos_renyi_graph (sizes: 10, 100, 1000; probabilities: 0.1, 0.5, 0.9).
    • Graphs are generated with consistent edge weights using a fixed random seed for reproducibility.

Utilities added

  • Graph utility functions in benchmarks/utils.py:
    • weighted_graph: Utility to assign random integer weights to edges.
    • benchmark_name_from_func_call: Generates human-readable graph names from function calls for better benchmark output labeling.

Example

(networkx-dev) bash-3.2$ asv compare 092f50e5 a1575792
Couldn't load asv.plugins._mamba_helpers because
No module named 'libmambapy'

All benchmarks:

| Change   | Before [092f50e5]             | After [a1575792]           |   Ratio | Benchmark (Parameter)                                                                                                                        |
|----------|-------------------------------|----------------------------|---------|----------------------------------------------------------------------------------------------------------------------------------------------|
| -        | 124±2μs                       | 107±0.5μs                  |    0.87 | benchmark_algorithms.WeightedGraphBenchmark.time_weighted_single_source_dijkstra('dijkstra_relaxation_worst_case(100)')                      |
| -        | 2.50±0.1ms                    | 1.26±0ms                   |    0.5  | benchmark_algorithms.WeightedGraphBenchmark.time_weighted_single_source_dijkstra('dijkstra_relaxation_worst_case(1000)')                     |
| -        | 306±8ms                       | 15.0±0.02ms                |    0.05 | benchmark_algorithms.WeightedGraphBenchmark.time_weighted_single_source_dijkstra('dijkstra_relaxation_worst_case(10000)')                    |
| -        | 1.19±0.01s                    | 31.6±0.3ms                 |    0.03 | benchmark_algorithms.WeightedGraphBenchmark.time_weighted_single_source_dijkstra('dijkstra_relaxation_worst_case(20000)')                    |
|          | 3.51±0.01μs                   | 3.58±0.02μs                |    1.02 | benchmark_algorithms.WeightedGraphBenchmark.time_weighted_single_source_dijkstra('weighted_graph(42, gnp_random_graph, 10, 0.1, seed=42)')   |
|          | 8.17±0.1μs                    | 7.94±0.01μs                |    0.97 | benchmark_algorithms.WeightedGraphBenchmark.time_weighted_single_source_dijkstra('weighted_graph(42, gnp_random_graph, 10, 0.5, seed=42)')   |
|          | 9.36±0.09μs                   | 8.74±0.01μs                |    0.93 | benchmark_algorithms.WeightedGraphBenchmark.time_weighted_single_source_dijkstra('weighted_graph(42, gnp_random_graph, 10, 0.9, seed=42)')   |
|          | 162±0.3μs                     | 156±0.4μs                  |    0.96 | benchmark_algorithms.WeightedGraphBenchmark.time_weighted_single_source_dijkstra('weighted_graph(42, gnp_random_graph, 100, 0.1, seed=42)')  |
|          | 574±4μs                       | 549±0.2μs                  |    0.96 | benchmark_algorithms.WeightedGraphBenchmark.time_weighted_single_source_dijkstra('weighted_graph(42, gnp_random_graph, 100, 0.5, seed=42)')  |
|          | 546±30μs                      | 503±10μs                   |    0.92 | benchmark_algorithms.WeightedGraphBenchmark.time_weighted_single_source_dijkstra('weighted_graph(42, gnp_random_graph, 100, 0.9, seed=42)')  |
|          | 12.0±0.06ms                   | 11.6±0.4ms                 |    0.97 | benchmark_algorithms.WeightedGraphBenchmark.time_weighted_single_source_dijkstra('weighted_graph(42, gnp_random_graph, 1000, 0.1, seed=42)') |
|          | 82.4±1ms                      | 83.7±2ms                   |    1.02 | benchmark_algorithms.WeightedGraphBenchmark.time_weighted_single_source_dijkstra('weighted_graph(42, gnp_random_graph, 1000, 0.5, seed=42)') |
|          | 53.8±4ms                      | 52.2±3ms                   |    0.97 | benchmark_algorithms.WeightedGraphBenchmark.time_weighted_single_source_dijkstra('weighted_graph(42, gnp_random_graph, 1000, 0.9, seed=42)') |
| -        | 58.0±1μs                      | 50.4±0.2μs                 |    0.87 | benchmark_algorithms.WeightedGraphBenchmark.time_weighted_single_source_dijkstra('weighted_graph(42, path_graph, 100)')                      |
| -        | 1.22±0.06ms                   | 484±5μs                    |    0.4  | benchmark_algorithms.WeightedGraphBenchmark.time_weighted_single_source_dijkstra('weighted_graph(42, path_graph, 1000)')                     |
| -        | 144±8ms                       | 4.91±0.05ms                |    0.03 | benchmark_algorithms.WeightedGraphBenchmark.time_weighted_single_source_dijkstra('weighted_graph(42, path_graph, 10000)')                    |
| -        | 527±8ms                       | 9.82±0.04ms                |    0.02 | benchmark_algorithms.WeightedGraphBenchmark.time_weighted_single_source_dijkstra('weighted_graph(42, path_graph, 20000)')                    |

Copy link
Copy Markdown
Contributor

@rossbar rossbar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm personally not a huge fan of the data class indirection - why not just add the benchmark cases explicitly? IMO this is harder to follow!

@amcandio
Copy link
Copy Markdown
Contributor Author

amcandio commented May 24, 2025

I'm personally not a huge fan of the data class indirection - why not just add the benchmark cases explicitly? IMO this is harder to follow!

The current approach has the problem of loading all graphs in memory, even if you are not running that benchmark. So for example, if you have a big graph, then all benchmarks have to load it into memory.

Indirection allows you to not have to hardcode the name for the benchmark. That is particularly useful when you want to use many different graphs for benchmarking.

So for example, you just need to pass the function+args and then asv method will show weighted_graph(42, path_graph, 20000) automatically.

@amcandio
Copy link
Copy Markdown
Contributor Author

amcandio commented May 27, 2025

Oops, posted on wrong PR, sorry for the noise :) See #8059

@amcandio amcandio force-pushed the weighted_benchmarks branch 2 times, most recently from aa5a13f to ccccfc1 Compare July 20, 2025 01:19
@amcandio
Copy link
Copy Markdown
Contributor Author

@rossbar @dschult I removed indirection!

dschult
dschult previously approved these changes Jul 21, 2025
Copy link
Copy Markdown
Member

@dschult dschult left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good to me!
Thanks @amcandio

@amcandio
Copy link
Copy Markdown
Contributor Author

@rossbar friendly ping! Are you good here?

@amcandio amcandio force-pushed the weighted_benchmarks branch from a25405f to 9c0d413 Compare August 31, 2025 04:48
@amcandio
Copy link
Copy Markdown
Contributor Author

amcandio commented Aug 31, 2025

I was able to construct a complete graph that produces relaxations for each edge when running Dijkstra from node 0. This case puts a lot of pressure on the heap and forces a path/predecesor recomputation for each discovered edge.

For such a graph of 1000 nodes, previous version of nx.shortest_path takes ~1.18 seconds while newest optimized version takes 670ms (45% faster).

@dschult dschult dismissed their stale review August 31, 2025 14:47

important changes since review

Copy link
Copy Markdown
Member

@dschult dschult left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the new weighted complete graph with lots of work for Dijkstra. :) nice construction of weights!

The changes in code to shortest_path seem to consist of three changes:

  1. storing dict values to avoid double lookups
  2. using get instead of in+subscript
  3. using setdefault in one place

Can we tease these apart? All cases of 1) are clearly good. Cases of 2 are arguably less readable. Case of 3 is perhaps not even needed (or helpful, see comment below).

The relative timing of these changes with versions of Python. So as Python has worked to speed up function calls, get has become more competitive against in+subscript. But that may change. The readability of if u in dist: is hard to beat. So I'm hoping that our speedup are mostly from 1) and somewhat from 2) and maybe not at all from 3). :)

@amcandio
Copy link
Copy Markdown
Contributor Author

amcandio commented Aug 31, 2025

Oh wait I added the Dijkstra's change by mistake! This PR was only intended to have the benchmark changes.

The speed up I was referring to is from the already merged PR on bidirectional Dijkstra.

@amcandio
Copy link
Copy Markdown
Contributor Author

Fixed! I reverted uninteded changes

Copy link
Copy Markdown
Member

@dschult dschult left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me!

@amcandio
Copy link
Copy Markdown
Contributor Author

amcandio commented Sep 9, 2025

@rossbar I changed the worst case graph generation. You can see how it generates the worst case where all edges produce a new relaxation (#8218 (comment)). Let me know if you have other comments on the PR!

@amcandio
Copy link
Copy Markdown
Contributor Author

Friendly ping! It would be great to merge this one so we track the benchmarks used for shortest paths algos

Copy link
Copy Markdown
Member

@MridulS MridulS left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, I like the dijkstra construction! Maybe we can make it directed to be even more "strict"? But good to go for me :)

@MridulS MridulS merged commit 341f711 into networkx:main Oct 18, 2025
45 checks passed
@MridulS MridulS added this to the 3.6 milestone Oct 18, 2025
@amcandio amcandio deleted the weighted_benchmarks branch October 18, 2025 21:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Development

Successfully merging this pull request may close these issues.

4 participants