Closed oowekyala closed 2 years ago
Cool! Can you share any details about the dependency graph structure that you used for your experiments?
The dependency graph looks like this:
It is a chain of small identical subgraphs, which each contain a long path and a short path from the source to the exit. The number of these subgraphs is the parameter for the test (in the screenshot, 3, but in the benchmark 10 to 120). The old algorithm is bad with this structure because it performs breadth first search, and its frontier set grows much more than necessary. It starts with the root, but after 3 iterations the frontier set contains Reaction(r[0]/1)
and Port(r[0]/out)
(red nodes), because the shortest path from the root to these nodes has length 3. The successors of each of these will be explored separately, which duplicates work. After 6 iterations the frontier set is all the turquoise nodes (nodes reachable in three steps from any red node), and it keeps growing.
I suspected the algorithm is exponential for some types of graph, and measured that this is the case. This PR replaces the algo with something that stays linear. I also compared with the algorithm used by the C++ runtime (ported to Rust). The Rust algorithm of this PR scales better on this specific type of graph, but I didn't do any tests on more normal graphs. This is anyway not a performance critical part of the runtime, but this TODO comment had been irking me for a while....
Here's a graph produced by the benhmarking framework. The "old" curve is the current algorithm, the "new" one is the one introduced in this PR. The graph is similar to the chain of diamonds described in the removed TODO comment (see changes). The chain length is the x axis of the graph.