-
Notifications
You must be signed in to change notification settings - Fork 55
Attempt to add ExecuteDFS to main (Generic depth first search) #737
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
So far I have refactored the iterative solution of However, this has implications for I believe removing this behavior and using recursion would also allow forward edges to be recognised during the DFS if this is useful at all. |
… to improve performance
…formance of running ExecuteDFS multiple times in a function
… config for DigraphLongestDistanceFromVertex
I have found that using the
|
Implementation | Min Time (ms) | Memory Allocated (Bytes) |
---|---|---|
HashMaps |
13,597 | 925,563,519 |
PList with BitArray |
10,255 | 923,069,485 |
PList no BitArray |
10,205 | 923,070,766 |
There is not much difference in terms of memory but this is because the current implementation uses much more memory in the non DFS part of the procedure. The HashMap
implementation uses 2,099,704 memory for regular DFS on the same graph, and the list implementation uses 323,142. The worse time for HashMaps
appears to be mainly caused by lookups. When I was reducing the amount of lookups I noticed significant improvements.
DigraphTopologicalSort
for BinaryTree(21)
Implementation | Min Time (ms) | Memory Allocated (Bytes) |
---|---|---|
HashMaps |
1365 | 469,969,923 |
PList with BitArray |
363 | 107,881,074 |
PList no BitArray |
365 | 107,882,284 |
All connected components are explored, so all the nodes have information stored in the hashmaps. I think the large memory increase is due to how HashMaps
have plain lists for both values and keys, and there are four of these HashMaps
in the DFS record. DigraphTopologicalSort
uses an AncestorFunc
, meaning more lookups are necessary to check if a neighbor is backtracked determining whether to call the AncestorFunc
or not. The time increase for HashMaps
is worse for this function as a result.
DominatorTree(d, 1)
for CompleteDigraph(5000)
Implementation | Min Time (ms) | Memory Allocated (Bytes) |
---|---|---|
HashMaps |
23,642 | 1,463,632,881 |
PList with BitArray |
12,203 | 1,461,462,329 |
PList no BitArray |
12,005 | 1,461,462,065 |
This is another case of the DFS memory being dominated by the memory used by the procedure itself (which I may try and look into to see if this can be improved).
ExecuteDFS
(including the record creation)
I also tested only running ExecuteDFS
to remove the interference of the work each function does itself.
On BinaryTree(21)
Implementation | Min Time (ms) | Memory Allocated (Bytes) |
---|---|---|
HashMaps |
0 | 1692 |
PList with BitArray |
122 | 67,111,802 |
PList no BitArray |
122 | 67,111,802 |
This example shows the advantage of HashMaps
. Since only one node is explored in the DFS search, the time taken is dominated by the initialisation of data structures rather than the recursive procedure. The whole lists are being initialised, which is not the case for the key and value lists of the HashMap
On ChainDigraph(20000)
Implementation | Memory Allocated (Bytes) |
---|---|
HashMaps |
4,197,140 |
PList with BitArray |
642,987 |
The memory usage is worse for the HashMap
due to the two plain lists each HashMap
stores. I have not figured out why it is so much more than 2x worse though.
On BinaryTree(21)
exploring all connected components (record.config.forest := true
)
Implementation | Min Time (ms) | Memory Allocated (Bytes) |
---|---|---|
HashMaps |
1198 | 402,655,646 |
PList with BitArray |
200 | 67,111,959 |
As opposed to the case without forest := true
, this does force all nodes to be explored (the worst case for HashMaps
).
Thoughts on which data structure to choose
I think generally, HashMaps
are not preferable to PList
for this algorithm. The worst case for PList
, where search search starts from a node with no children is probably not a common case, and the worst case of a HashMap
seems much worse. There was only a 122 ms difference for the setup of the PList
versions with a graph of 2,097,151 vertices (ExecuteDFS
for BinaryTree(21)
). The memory usage was much higher than for HashMaps
, but this only grows with the number of vertices with no further multiplier (the memory usage for HashMaps
grows faster due to two PLists
per HashMap
, but it seems to be more than 2x worse).
I will keep the BitArrays for now in case I remove the usage of some record fields given certain configuration options to reduce memory usage if they are not needed. The memory usage is barely different since only 1 bit is stored per vertex for both the backtracking and visited arrays.
…. ChainDigraph(200000))
…ingForest, add options to turn off specific record fields when not necessary
…y record elements
…re tests for regular ExecuteDFS and improve performance using macros for calling PreorderFunc, etc.
… and use_postorder is false, uses a bit array)
Below is an overview of the current state of generic DFS performance for selected examples. All benchmarking was done as an average of 100 runs. General ExecuteDFS PerformancePerformance of
|
Some other notes on the state of this procedure
Overview of what has been implemented
|
This pull request will build on #505 and #499 and attempt to add
ExecuteDFS
to the main branch again.My aim is to add back the fixed versions of functions using the generic DFS, adding more tests to ensure they are correct, and to look at improving the performance of these functions.
I will also look at improving the performance of
ExecuteDFS
itself.