Skip to content
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

atonement for compiler abuse (type stable cursors!) #120

Merged
merged 6 commits into from
Oct 19, 2022

Conversation

ExpandingMan
Copy link
Contributor

@ExpandingMan ExpandingMan commented Sep 24, 2022

This PR does the following:

  • All cursors now have type parameters for parent which only give the unwrapped (non-cursor) type of the parent. This should eliminate the compiler catastrophe of Tree traversal really slow due to ridiculous ammounts of compilation #117.
  • Cursor type signatures have been re-arranged slightly to allow for easier typing of parents. I'm not really taking advantage of this yet since it still requires one to define a bunch of confusing and annoying conversions, but in the long run it will make it easier to narrow cursor parent types somewhat.
  • StableCursor has been added to give type-stable iteration over trees with HasNodeType.
  • StableIndexedCursor has been added which allows really, really type-stable iteration over trees with both HasNodeType and IndexedChildren. That is, unlike StableCursor, it does not rely on any crazy inference shenanigans like Iterators.approx_iter_type. If you want really fast tree traversal, you should define a node type that can use this.

I have not comprehensively benchmarked this, but so far according to @code_warntype and @btime it looks like this is doing more-or-less what we want. In particular, it definitely is type stable for HasNodeType (though if you repeatedly call StableCursor it will repeatedly call Iterators.approx_iter_type which is slow, so it's not perfect).

I have a bunch more things I'd like to do in this PR, but I wanted to get it in front of maintainers since this is all very tricky and confusing. There is a fair amount of code duplication here, far more than I would permit in most circumstances. Arguably we should find a way to combine ImplicitCursor and StableCursor. However, I find all this so subtle, confusing and tricky that I think it will be a boon rather than a burden to the ongoing maintenance of this package to allow for a bit of code duplication. If I were to really follow all the way through and combine everything as much as possible the result would be far too clever and would be much more difficult to maintain than what's here now (to me at least).

TODO

  • Add AbstractNode (Add an abstract type for AbstractTrees to AbstractTrees.jl #115). Fortunately we will have 2 types in the package which can inherit from this.
  • Add some convenience functions for making non-type-stable trees type-stable. For example, nested arrays with leaves of a common type should be easy to turn into type-stable trees with node-values of type Union{Nothing,T} where T is the array eltype. Since this is the only way to get fast tree traversal we should make it as easy as possible. This shouldn't be hard, it will merely involve creating some simple recursive constructors for a new StableNode type.
  • Add some more unit tests for cursors.
  • Clean up docs and docstrings.

Breakage?

Technically anything we do to fix #117 will be breaking because it's impossible to fix without changing the type signatures of all TreeCursors. However, I rate it extremely unlikely that it will actually break any packages. In fact, it shouldn't even break unit tests. Therefore it's worth discussing whether its' worth tagging 0.5. Because of the ubiquity of this package in the ecosystem it might be worth running automated tests on dependencies to see if we can manage it.

@codecov-commenter
Copy link

codecov-commenter commented Sep 24, 2022

Codecov Report

Merging #120 (8f31889) into master (7285f40) will increase coverage by 3.63%.
The diff coverage is 66.66%.

@@            Coverage Diff             @@
##           master     #120      +/-   ##
==========================================
+ Coverage   66.40%   70.04%   +3.63%     
==========================================
  Files           8        8              
  Lines         384      434      +50     
==========================================
+ Hits          255      304      +49     
- Misses        129      130       +1     
Impacted Files Coverage Δ
src/AbstractTrees.jl 100.00% <ø> (ø)
src/iteration.jl 77.09% <ø> (+1.54%) ⬆️
src/base.jl 59.45% <61.53%> (+1.12%) ⬆️
src/cursors.jl 61.64% <67.07%> (+12.12%) ⬆️
src/traits.jl 79.16% <100.00%> (+16.66%) ⬆️

📣 We’re building smart automated test selection to slash your CI/CD build times. Learn more

@oscardssmith
Copy link
Member

from an initial glance, this looks good! I'll try it out this week and see if it also works well.

@ExpandingMan ExpandingMan changed the title [in progress] atonement for compiler abuse (type stable cursors!) atonement for compiler abuse (type stable cursors!) Sep 26, 2022
@ExpandingMan
Copy link
Contributor Author

Alright, this should be done now. I have also added AbstractNode as in #115 from which both MapNode and the new StableNode inherit. I've added a method for StableNode which makes it as easy as I can make it for users to create type-stable trees (that use StableIndexedCursor) from trees that have no such guarantees.

@oscardssmith
Copy link
Member

This is a decent amount slower for me than #118 (roughly 2x) in the ImplicitCursor case. not fully sure why. That said, it definitely does seem to fix the non-termination of compilation on master.

@ExpandingMan
Copy link
Contributor Author

I'll look into it. The ImplicitCursor is pretty similar in this PR, the biggest difference is that you constrain the parent cursor by nodetype though I'm not sure why that would make a difference in performance.

Just to be clear, it's slower to run, slower to compile, or both?

@oscardssmith
Copy link
Member

On my branch, for the first 2 runs, I'm seeing

 16.640872 seconds (23.87 M allocations: 1.610 GiB, 2.82% gc time, 96.19% compilation time)
  0.492575 seconds (6.26 M allocations: 491.616 MiB, 20.58% gc time)

and on yours

 24.948324 seconds (29.12 M allocations: 1.944 GiB, 2.57% gc time, 96.56% compilation time)
  0.457518 seconds (5.86 M allocations: 468.161 MiB, 18.22% gc time)

so runtime is similar, but compile time on my pr is slightly better.

@ExpandingMan
Copy link
Contributor Author

ExpandingMan commented Sep 27, 2022

I really don't think we should be chasing down compile time differences like that unless there is a simple and obvious fix. Not only is it hard to optimize but it will not even be the same across different versions of Julia. As long as we can eliminate any pathological compiler behavior like what I had before I'd ask that we not worry about it. At the very least, I don't think it's a great idea to design the packages data structures around this, if someone wants to come in and optimize it away later they can.

Also, for what it's worth, there's not going to be any way around the fact that this package will be more taxing on the compiler than most due to the recursive and unpredictable nature of type-unstable trees.

@oscardssmith
Copy link
Member

oscardssmith commented Sep 27, 2022

agreed. As one last thing, can you add my definition of prevsibling(::ImplicitCursor) to this PR?

@ExpandingMan
Copy link
Contributor Author

Will do. Also, any thoughts on version bump? I really think we can get away without tagging this as breaking. It didn't even break our own unit tests. It's unlikely that anyone is even using cursors directly let alone relying on their type signature or concrete type. At most maybe we can solicit opinions on discourse or something.

@oscardssmith
Copy link
Member

I think this doesn't need a major version bump. From what I've seen, no one relies on this (and I went through most of the dependents in the bump to 0.4 to update project.tomls).

@ExpandingMan
Copy link
Contributor Author

@oscardssmith that prevsibling definition makes me a bit nervous because it relies on checking equality. When I did 0.4 I tried pretty hard to avoid ever doing that and I don't think there are any left. I don't know if we can assume that == will always be properly implemented. There is no useful default definition for mutable structs. Also, there is no reason you can't have multiple identical nodes (including identical children) as distinct instances of children of the same node (so they would be == even if it's properly implemented).

I just did a quick search and I don't think there's anywhere we currently rely on == and if there is I'd argue we should remove it. I think it's fair for not all TreeCursor to implement prevsibling... it's really hard to navigate siblings so if a user finds it so important they should probably ensure they have ChildIndexing.

I bumped the version to 0.4.3.

@oscardssmith
Copy link
Member

I don't really care about the implementation, but I do think it's important to have ImplicitCursors provide forward and reverse iteration over siblings. Another way to do it would be to make the SiblingState be an Int of which number child we are currently at.

@ExpandingMan
Copy link
Contributor Author

ExpandingMan commented Sep 27, 2022

Another way to do it would be to make the SiblingState be an Int of which number child we are currently at.

To do that we'd have to somehow store multiple completely distinct states because it can't be guaranteed in the general case that the state used by Base.iterate is any particular type. That's pretty much the whole reason ChildIndexing exists. I'm not sure it's such a great idea to try to make ImplicitCursor more complicated considering how tricky this gets, I think it would be better to accept some limitations when types provide absolutely no guarantees to the interface.

Also, don't forget, even if we do store a sibling index, the cursors would still have to repeatedly iterate over the parent children because it's still not guaranteed that children is indexable.

@ExpandingMan
Copy link
Contributor Author

ExpandingMan commented Oct 1, 2022

@oscardssmith could we do prevsibling in a future PR? It is not trivial to implement and I believe this PR fixes a major issue that lots of packages throughout the ecosystem are likely dealing with. (ImplicitCursor also did not have prevsibling to begin with so nothing is being removed.)

@oscardssmith
Copy link
Member

yeah. that works.

@ExpandingMan
Copy link
Contributor Author

@oscardssmith any objections to merging this?

@oscardssmith
Copy link
Member

go for it.

@ExpandingMan
Copy link
Contributor Author

lol I don't have permissions, I wasn't saying that I was going to do it 😅

@ExpandingMan
Copy link
Contributor Author

Btw, if you have any lingering concerns about breakage now is the time to discuss them... I'm not worried because we did not even break our own unit tests, and it's not like our unit tests are that bad. Also back when I did the major PR I ran tests on the entire ecosystem and almost nothing broke and that was a much bigger change, so I think I'm willing to have someone come along and curse me out if something bad happens.

@oscardssmith
Copy link
Member

I have to change a few minor things, but the changes are pretty minor and I'm using more of the API than anyone else.

@oscardssmith
Copy link
Member

I'll merge this next week if no one objects.

@oscardssmith oscardssmith merged commit 781a01d into JuliaCollections:master Oct 19, 2022
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.

Tree traversal really slow due to ridiculous ammounts of compilation
4 participants