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

Type-stable codegen for specialized in(v, ::Tuple) #54026

Merged
merged 3 commits into from
May 9, 2024

Conversation

NHDaly
Copy link
Member

@NHDaly NHDaly commented Apr 10, 2024

Since we were already specializing in for Tuples anyway, we should be generating code that can take advantage of the tuple type, rather than doing a generic, type-unstable traversal.

Before:

julia> @btime Base.in($(5), (1, 2.0, 3, "hey", 5.2))
  53.541 ns (6 allocations: 224 bytes)
false

After:

julia> @btime Base.in($(5), (1, 2.0, 3, "hey", 5.2))
  1.666 ns (0 allocations: 0 bytes)
false

The new code statically unrolls the tuple comparisons, and thus also compiles away any of the comparisons that are statically known to be not equal, such as the ==(::Int, ::String) comparison for element 4 in the example above.

@NHDaly
Copy link
Member Author

NHDaly commented Apr 10, 2024

We were already fully specializing on the tuple type, as you can see here:

julia> length(MethodAnalysis.methodinstances(in, (Any,Tuple)))
66

so I'm hopeful that this shouldn't be making our compilation times any worse.

The one thing that I'm not sure about is what role the recursion plays here: are we going to be caching more compilation results for all the recursion intermediates, in which case this will be increasing the compiler's memory usage? Unfortunately, I don't know of any other way to generate per-element specialized code other than recursion like this. I would use foreach(), but we want to be able to terminate early if we find a match. (It could of course be done with @generated, but i was trying to avoid that.)

If we don't want to pursue this approach, we should instead mark the original in() function as @nospecailize on tuples, since there's really no reason for us to be generating code per-tuple if the body is type unstable anyway.

@test ∈(5, (5,))
@test ∉(0, (5,))
@test ∉(5, ())
end
Copy link
Member Author

Choose a reason for hiding this comment

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

As far as I can tell, there weren't any tests for in(x, ::Tuple) before this PR, so I added some.

base/operators.jl Outdated Show resolved Hide resolved
@NHDaly NHDaly marked this pull request as ready for review April 10, 2024 16:30
@NHDaly
Copy link
Member Author

NHDaly commented Apr 10, 2024

Tagged @aviatesk and @JeffBezanson since this touches compilation times, and I want to be sensitive to that.

There's no rush on this PR; I just happened to be looking at this code for something else, and while I was there I noticed that in() was type-unstable for tuples.

@NHDaly NHDaly changed the title Better codegen for specialized in(v, ::Tuple) Type-stable codegen for specialized in(v, ::Tuple) Apr 10, 2024
@quinnj
Copy link
Member

quinnj commented Apr 13, 2024

I would use foreach(), but we want to be able to terminate early if we find a match.

As more of a drive-by comment, I also recently wanted something like this and came up with my own applyeach function that supports early termination in a foreach-like function.

base/operators.jl Outdated Show resolved Hide resolved
@NHDaly NHDaly requested a review from martinholters May 8, 2024 16:28
@NHDaly
Copy link
Member Author

NHDaly commented May 8, 2024

Sorry for my delay. Please have another look! :)

NHDaly and others added 2 commits May 9, 2024 14:55
Since we were already specializing in for Tuples anyway, we should be
generating code that can take advantage of the tuple type, rather than
doing a generic, type-unstable traversal.

Before:
```julia
julia> @Btime Base.in($(5), (1, 2.0, 3, "hey", 5.2))
  53.541 ns (6 allocations: 224 bytes)
false
```

After:
```julia
julia> @Btime Base.in($(5), (1, 2.0, 3, "hey", 5.2))
  1.666 ns (0 allocations: 0 bytes)
false
```

The new code statically unrolls the tuple comparisons, and thus also
compiles away any of the comparisons that are statically known to be
not equal, such as the `==(::Int, ::String)` comparison for element 4 in
the example above.
Per review comment
@aviatesk
Copy link
Sponsor Member

aviatesk commented May 9, 2024

The recursive implementation seems to cause stack overflow during inference with large tuples. It looks like we need a technique similar to what’s used in map, where dispatches to a loop implementation based on a specific tuple length threshold.

@aviatesk aviatesk force-pushed the nhd-specialized-in-tuples branch from 8269883 to 703de63 Compare May 9, 2024 06:17
@nsajko
Copy link
Contributor

nsajko commented May 9, 2024

Competing PR: #54411

A problem (regression relative to master) with both PRs, although it's slightly worse with this PR than with mine, is that even in trivial cases the return type inference is pessimistic:

julia> Core.Compiler.return_type(_in_tuple, Tuple{Int,Tuple{},Bool})
Union{Missing, Bool}

EDIT: fixed in my PR

EDIT2: the above observation wasn't valid. Making the third argument a constant, instead, looks fine:

julia> f(x, t) = Base._in_tuple(x, t, false)
f (generic function with 1 method)

julia> Core.Compiler.return_type(f, Tuple{Int,Tuple{}})
Bool

julia> Core.Compiler.return_type(f, Tuple{Int,Tuple{(Int for _  1:100)...}})
Bool

@aviatesk
Copy link
Sponsor Member

aviatesk commented May 9, 2024

The inference for _in_tuple should always account for the possibility of Missing. For the inference of in itself, it's possible to exclude missing in such straightforward cases (even without these PRs though).

@aviatesk
Copy link
Sponsor Member

aviatesk commented May 9, 2024

Looks like we can go ahead with this. We can potentially incorporate #54411 in the future if it proves to be useful.

@aviatesk aviatesk merged commit 3920694 into master May 9, 2024
7 checks passed
@aviatesk aviatesk deleted the nhd-specialized-in-tuples branch May 9, 2024 10:16
Comment on lines +1353 to +1354
function _in_tuple(x, @nospecialize(itr::Tuple), anymissing::Bool)
@inline
Copy link
Member Author

Choose a reason for hiding this comment

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

Can you help me understand why you moved the inline annotation? Does it have any difference, or you just prefer this stylistically? Thanks!

Copy link
Contributor

Choose a reason for hiding this comment

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

  1. An @inline applied to a method carries less weight than an @inline applied to code block within the method body.

  2. The semantics are different, when used within the method body it suggests to inline callees.

Copy link
Contributor

Choose a reason for hiding this comment

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

BTW can you help review my linked PR, given that you're freshly acquainted with the code?

Copy link
Sponsor Member

Choose a reason for hiding this comment

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

They have the same effect, but @inline method def is defined in base/expr.jl and is not available at this point of loading base/operators.jl when building system image. the version which is used without any arguments on the other hand is defined at the very early stage of sysimg build, so we can use it even before loading base/expr.jl.

lazarusA pushed a commit to lazarusA/julia that referenced this pull request Jul 12, 2024
Since we were already specializing in for Tuples anyway, we should be
generating code that can take advantage of the tuple type, rather than
doing a generic, type-unstable traversal.

Before:
```julia
julia> @Btime Base.in($(5), (1, 2.0, 3, "hey", 5.2))
  53.541 ns (6 allocations: 224 bytes)
false
```

After:
```julia
julia> @Btime Base.in($(5), (1, 2.0, 3, "hey", 5.2))
  1.666 ns (0 allocations: 0 bytes)
false
```

The new code statically unrolls the tuple comparisons, and thus also
compiles away any of the comparisons that are statically known to be not
equal, such as the `==(::Int, ::String)` comparison for element 4 in the
example above.

---------

Co-authored-by: Shuhei Kadowaki <[email protected]>
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.

None yet

5 participants