-
Notifications
You must be signed in to change notification settings - Fork 211
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
Cycles detection: use cycles map of addresses to stack depth #79
Conversation
The current comparison fails on stack overflow when given a cyclic struct (such as a cyclic linked list for example). This fix adds a new option: DecetctCycles, which finds cycles by adding information to the path stack, when iterating over pointers. When a cycle is checked (every time a pointer kind is compared) the path stack is iterated to find cycles. The comparison of cycles is by they length. Fixes: google#74
1d7f5f3
to
ddbc44a
Compare
Enable cycles detection by default. This solution gives O(1) additional complexity for detecting cycles.
Thanks for prototyping this. I have some high-level thoughts:
|
An additional thought. The algorithm must be agnostic to traversal order. That is, the result of |
Thanks for the review,
This is the way to make difference between a 1 step cycle to a 2 step cycle to a 3 step cycle, as you gave in your example. So since it is a DFS we are doing, The stack length "represents" the length of the cycle - it was more accurate if we had a counter that counts only the pointer references (or the other case you mentioned) but I think that it is not necessary and the stack size does a good job for this comparison.
I must push and pop: for example, a diamond shape graph will be detected as a cycle without popping elements in a DFS traverse.
Really nice example! I'll add hooks also for those kind of cycles too so we can detect them!
Can you please explain when I will meet this? in the current implementation it looks to works fine.
Why should it? Currently we are doing a DFS and it works with that. The diff algorithm also saves a "stack" which is a DFS struct, and relies on the path to be correct by pushing and popping elements.
Do you want me to elaborate on the solution? create some sort of a doc for it? maybe add it to the cycles.go file? |
In order to reason through your PR, I took a very different approach. See #85 for my WIP. My approach uses two maps of type What's interesting is that my approach and yours are semantically equivalent. My definition of equality is if "both pointers were first encountered together as a pair". In order for this to be true, they must exist in the same path step, in which case they have the same depth. In fact, I think "depth" could be better thought as the index into the the I find your approach easier to explain in documentation on There are some nuanced differences though:
|
@dsnet nice code! Good points that you made, and the code looks good.
I'm not sure you are correct. Another thing: I understand that you want this to be a single commit in the repo. But - I would appreciate if you leave my commits and push your changes on top (I can squash them to a single commit if it helps). |
Consider the following examples. v := reflect.ValueOf(make([]int32, 4))
PrintPointer(v) // 0x10414020 []int32
PrintPointer(v.Index(0).Addr()) // 0x10414020 *int32
PrintPointer(v.Index(1).Addr()) // 0x10414024 *int32
PrintPointer(v.Index(2).Addr()) // 0x10414028 *int32
PrintPointer(v.Index(3).Addr()) // 0x1041402c *int32 Notice from this snippet that the elements of a slice are functionally pointers to them from the parent slice. For this reason, the "address" in the slice is equal to the address of the first element. v := reflect.ValueOf(make([]int32, 4))
PrintPointer(v.Slice(0, 0)) // 0x10414020 []int32
PrintPointer(v.Slice(0, 1)) // 0x10414020 []int32
PrintPointer(v.Slice(0, 2)) // 0x10414020 []int32
PrintPointer(v.Slice(0, 3)) // 0x10414020 []int32
PrintPointer(v.Slice(0, 4)) // 0x10414020 []int32 Notice from this snippet that all 5 sub-slices (i.e., v := make([]int32, 4)
v0 := v[:0]
v1 := v[:1]
p0 := (*reflect.SliceHeader)(unsafe.Pointer(&v0)).Data // 0x10414020
p1 := (*reflect.SliceHeader)(unsafe.Pointer(&v1)).Data // 0x10414020
reflect.DeepEqual(v0, v1) // false
p0 == p1 // true Just because
I'm not sure I understand. My PR in #85 is an entirely different approach with no shared code. |
Previously, trying to call Equal on a graph would result in a stack-overflow due to infinite recursion traversing cycles on a graph. While a vast majority of Go values are trees or acyclic graphs, there exist a small number of cases where graph equality is required. As such, we add cycle detection to Equal and define what it means for two graphs to be equal. Contrary to reflect.DeepEqual, which declares two graphs to be equal so long any cycle were encountered, we require two graphs to have equivalent graph structures. Mathematically speaking, a graph G is a tuple (V, E) consisting of the set of vertices and edges in that graph. Graphs G1 and G2 are equal if V1 == V2, E1 == E2, and both have the same root vertex (entry point into the graph). When traversing G1 and G2, we remember a stack of previously visited edges ES1 and ES2. If the current edge e1 is in ES1 or e2 is in ES2, then we know that a cycle exists. The graphs have the same structure when the previously encountered edge ep1 and ep2 were encountered together. Note that edges and vertices unreachable from the root vertex are ignored. In addition to adding cycle detection, we fix some other issues: * Fix formatting of cyclic data structures. It is not safe simply storing an uintptr as this assumes the runtime never has a moving GC. Instead, we should use unsafe.Pointer, which the GC knows how to scan. * Since AppEngine cannot use unsafe, we continue to use uintptr in the "purego" environment. The pointer package creates an abstraction over opaque pointers. Appreciation goes to Eyal Posener, who proposed a different (but semantically equivalent) approach in #79. Fixes #74
Sorry we are getting into this, and with all the respect, I can see a lot of similarities in the code and concepts. Also, quoting you from #74:
I think I deserve a credit here. |
Previously, trying to call Equal on a graph would result in a stack-overflow due to infinite recursion traversing cycles on a graph. While a vast majority of Go values are trees or acyclic graphs, there exist a small number of cases where graph equality is required. As such, we add cycle detection to Equal and define what it means for two graphs to be equal. Contrary to reflect.DeepEqual, which declares two graphs to be equal so long any cycle were encountered, we require two graphs to have equivalent graph structures. Mathematically speaking, a graph G is a tuple (V, E) consisting of the set of vertices and edges in that graph. Graphs G1 and G2 are equal if V1 == V2, E1 == E2, and both have the same root vertex (entry point into the graph). When traversing G1 and G2, we remember a stack of previously visited edges ES1 and ES2. If the current edge e1 is in ES1 or e2 is in ES2, then we know that a cycle exists. The graphs have the same structure when the previously encountered edge ep1 and ep2 were encountered together. Note that edges and vertices unreachable from the root vertex are ignored. In addition to adding cycle detection, we fix some other issues: * Fix formatting of cyclic data structures. It is not safe simply storing an uintptr as this assumes the runtime never has a moving GC. Instead, we should use unsafe.Pointer, which the GC knows how to scan. * Since AppEngine cannot use unsafe, we continue to use uintptr in the "purego" environment. The pointer package creates an abstraction over opaque pointers. Appreciation goes to Eyal Posener (@posener), who proposed a different (but semantically equivalent) approach in #79, which served as inspirition. Fixes #74
Previously, trying to call Equal on a graph would result in a stack-overflow due to infinite recursion traversing cycles on a graph. While a vast majority of Go values are trees or acyclic graphs, there exist a small number of cases where graph equality is required. As such, we add cycle detection to Equal and define what it means for two graphs to be equal. Contrary to reflect.DeepEqual, which declares two graphs to be equal so long any cycle were encountered, we require two graphs to have equivalent graph structures. Mathematically speaking, a graph G is a tuple (V, E) consisting of the set of vertices and edges in that graph. Graphs G1 and G2 are equal if V1 == V2, E1 == E2, and both have the same root vertex (entry point into the graph). When traversing G1 and G2, we remember a stack of previously visited edges ES1 and ES2. If the current edge e1 is in ES1 or e2 is in ES2, then we know that a cycle exists. The graphs have the same structure when the previously encountered edge ep1 and ep2 were encountered together. Note that edges and vertices unreachable from the root vertex are ignored. In addition to adding cycle detection, we fix some other issues: * Fix formatting of cyclic data structures. It is not safe simply storing an uintptr as this assumes the runtime never has a moving GC. Instead, we should use unsafe.Pointer, which the GC knows how to scan. * Since AppEngine cannot use unsafe, we continue to use uintptr in the "purego" environment. The pointer package creates an abstraction over opaque pointers. Appreciation goes to Eyal Posener (@posener), who proposed a different (but semantically equivalent) approach in #79, which served as inspirition. Fixes #74
Previously, trying to call Equal on a graph would result in a stack-overflow due to infinite recursion traversing cycles on a graph. While a vast majority of Go values are trees or acyclic graphs, there exist a small number of cases where graph equality is required. As such, we add cycle detection to Equal and define what it means for two graphs to be equal. Contrary to reflect.DeepEqual, which declares two graphs to be equal so long any cycle were encountered, we require two graphs to have equivalent graph structures. Mathematically speaking, a graph G is a tuple (V, E) consisting of the set of vertices and edges in that graph. Graphs G1 and G2 are equal if V1 == V2, E1 == E2, and both have the same root vertex (entry point into the graph). When traversing G1 and G2, we remember a stack of previously visited edges ES1 and ES2. If the current edge e1 is in ES1 or e2 is in ES2, then we know that a cycle exists. The graphs have the same structure when the previously encountered edge ep1 and ep2 were encountered together. Note that edges and vertices unreachable from the root vertex are ignored. Appreciation goes to Eyal Posener (@posener), who proposed a different (but semantically equivalent) approach in #79, which served as inspiration. Fixes #74
Previously, trying to call Equal on a graph would result in a stack-overflow due to infinite recursion traversing cycles on a graph. While a vast majority of Go values are trees or acyclic graphs, there exist a small number of cases where graph equality is required. As such, we add cycle detection to Equal and define what it means for two graphs to be equal. Contrary to reflect.DeepEqual, which declares two graphs to be equal so long any cycle were encountered, we require two graphs to have equivalent graph structures. Mathematically speaking, a graph G is a tuple (V, E) consisting of the set of vertices and edges in that graph. Graphs G1 and G2 are equal if V1 == V2, E1 == E2, and both have the same root vertex (entry point into the graph). When traversing G1 and G2, we remember a stack of previously visited edges ES1 and ES2. If the current edge e1 is in ES1 or e2 is in ES2, then we know that a cycle exists. The graphs have the same structure when the previously encountered edge ep1 and ep2 were encountered together. Note that edges and vertices unreachable from the root vertex are ignored. Appreciation goes to Eyal Posener (@posener), who proposed a different (but semantically equivalent) approach in #79, which served as inspiration. Fixes #74
Previously, trying to call Equal on a graph would result in a stack-overflow due to infinite recursion traversing cycles on a graph. While a vast majority of Go values are trees or acyclic graphs, there exist a small number of cases where graph equality is required. As such, we add cycle detection to Equal and define what it means for two graphs to be equal. Contrary to reflect.DeepEqual, which declares two graphs to be equal so long any cycle were encountered, we require two graphs to have equivalent graph structures. Mathematically speaking, a graph G is a tuple (V, E) consisting of the set of vertices and edges in that graph. Graphs G1 and G2 are equal if V1 == V2, E1 == E2, and both have the same root vertex (entry point into the graph). When traversing G1 and G2, we remember a stack of previously visited edges ES1 and ES2. If the current edge e1 is in ES1 or e2 is in ES2, then we know that a cycle exists. The graphs have the same structure when the previously encountered edge ep1 and ep2 were encountered together. Note that edges and vertices unreachable from the root vertex are ignored. Appreciation goes to Eyal Posener (@posener), who proposed a different (but semantically equivalent) approach in #79, which served as inspiration. Fixes #74
Previously, trying to call Equal on a graph would result in a stack-overflow due to infinite recursion traversing cycles on a graph. While a vast majority of Go values are trees or acyclic graphs, there exist a small number of cases where graph equality is required. As such, we add cycle detection to Equal and define what it means for two graphs to be equal. Contrary to reflect.DeepEqual, which declares two graphs to be equal so long any cycle were encountered, we require two graphs to have equivalent graph structures. Mathematically speaking, a graph G is a tuple (V, E) consisting of the set of vertices and edges in that graph. Graphs G1 and G2 are equal if V1 == V2, E1 == E2, and both have the same root vertex (entry point into the graph). When traversing G1 and G2, we remember a stack of previously visited edges ES1 and ES2. If the current edge e1 is in ES1 or e2 is in ES2, then we know that a cycle exists. The graphs have the same structure when the previously encountered edge ep1 and ep2 were encountered together. Note that edges and vertices unreachable from the root vertex are ignored. Appreciation goes to Eyal Posener (@posener), who proposed a different (but semantically equivalent) approach in #79, which served as inspiration. Fixes #74
Previously, trying to call Equal on a graph would result in a stack-overflow due to infinite recursion traversing cycles on a graph. While a vast majority of Go values are trees or acyclic graphs, there exist a small number of cases where graph equality is required. As such, we add cycle detection to Equal and define what it means for two graphs to be equal. Contrary to reflect.DeepEqual, which declares two graphs to be equal so long any cycle were encountered, we require two graphs to have equivalent graph structures. Mathematically speaking, a graph G is a tuple (V, E) consisting of the set of vertices and edges in that graph. Graphs G1 and G2 are equal if V1 == V2, E1 == E2, and both have the same root vertex (entry point into the graph). When traversing G1 and G2, we remember a stack of previously visited edges ES1 and ES2. If the current edge e1 is in ES1 or e2 is in ES2, then we know that a cycle exists. The graphs have the same structure when the previously encountered edge ep1 and ep2 were encountered together. Note that edges and vertices unreachable from the root vertex are ignored. Appreciation goes to Eyal Posener (@posener), who proposed a different (but semantically equivalent) approach in #79, which served as inspiration. Fixes #74
Previously, trying to call Equal on a graph would result in a stack-overflow due to infinite recursion traversing cycles on a graph. While a vast majority of Go values are trees or acyclic graphs, there exist a small number of cases where graph equality is required. As such, we add cycle detection to Equal and define what it means for two graphs to be equal. Contrary to reflect.DeepEqual, which declares two graphs to be equal so long any cycle were encountered, we require two graphs to have equivalent graph structures. Mathematically speaking, a graph G is a tuple (V, E) consisting of the set of vertices and edges in that graph. Graphs G1 and G2 are equal if V1 == V2, E1 == E2, and both have the same root vertex (entry point into the graph). When traversing G1 and G2, we remember a stack of previously visited edges ES1 and ES2. If the current edge e1 is in ES1 or e2 is in ES2, then we know that a cycle exists. The graphs have the same structure when the previously encountered edge ep1 and ep2 were encountered together. Note that edges and vertices unreachable from the root vertex are ignored. Appreciation goes to Eyal Posener (@posener), who proposed a different (but semantically equivalent) approach in #79, which served as inspiration. Fixes #74
Enable cycles detection by default.
This solution gives O(1) additional complexity for detecting cycles.
Fixes #74
Instead of #78