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

Add a fast overload for fieldcount(Tuple{...}) #32495

Closed
wants to merge 1 commit into from

Conversation

NHDaly
Copy link
Member

@NHDaly NHDaly commented Jul 3, 2019

Add an overload for Base.fieldcount for Tuples, since we can determine it much more cheaply than for other types:

fieldcount(@nospecialize t::Type{T}) where T<:Tuple{Vararg{Any,N}} where N = N

Currently fieldcount(Tuple{Int, Float32}), for example, cannot const fold, and thus requires a runtime length check of the types array.

Before this change, on Julia 1.1 this took around 6ns, and on master, it's currently 13ns:

julia> @btime fieldcount(Tuple{Int,Int})
  13.043 ns (0 allocations: 0 bytes)
2

After this change, this is instantaneous:

julia> @btime fieldcount(Tuple{Int,Int})
  0.025 ns (0 allocations: 0 bytes)
2

Of course, the tradeoff is that the dispatch becomes more expensive if we have to do a dynamic dispatch:

julia> randtup() = tuple(1:rand(1:10)...)
randtup (generic function with 1 method)

julia> f() = fieldcount(typeof(randtup()))+1
f (generic function with 1 method)

julia> @btime f()
  680.552 ns (8 allocations: 438 bytes)
5

julia> Base.fieldcount(@nospecialize t::Type{T}) where T<:Tuple{Vararg{Any,N}} where N = N

julia> @btime f()
  703.386 ns (8 allocations: 428 bytes)
7

(@JeffBezanson as I was reviewing my notes from a meeting we had last year, it occurred to me that this overload might be useful. I remembered that you said fieldcount is still more expensive than it needs to be, and we came up with this Vararg workaround together. Seems like that might be useful to put in base?)

Add an overload for `Base.fieldcount` for Tuples, since we can determine it much more cheaply than for other types.

Currently `fieldcount(Tuple{Int, Float32})`, for example, cannot const fold, and thus requires a runtime length check of the types array.

Before this change, on Julia 1.1 this took around 6ns, and on master, it's currently 13ns:
```julia
julia> @Btime fieldcount(Tuple{Int,Int})
  13.043 ns (0 allocations: 0 bytes)
2
```
After this change, this is instantaneous:
```julia
julia> @Btime fieldcount(Tuple{Int,Int})
  0.025 ns (0 allocations: 0 bytes)
2
```

Of course, the tradeoff is that the dispatch becomes more expensive if we have to do a dynamic dispatch:
```julia
julia> randtup() = tuple(1:rand(1:10)...)
randtup (generic function with 1 method)

julia> f() = fieldcount(typeof(randtup()))+1
f (generic function with 1 method)

julia> @Btime f()
  680.552 ns (8 allocations: 438 bytes)
5

julia> Base.fieldcount(@nospecialize t::Type{T}) where T<:Tuple{Vararg{Any,N}} where N = N

julia> @Btime f()
  703.386 ns (8 allocations: 428 bytes)
7
```
@vtjnash
Copy link
Member

vtjnash commented Jul 3, 2019

Indeed, this would now make the function specialized on (and slow for) all types, since we can't create a single guard signature and pre-resolve the dispatch. FWIW, I'm proposing something similar at https://github.com/JuliaLang/julia/pull/32427/files#diff-fd99c9a15dadc23d9200069dadafb8bbR19, for similar reasons.

@NHDaly
Copy link
Member Author

NHDaly commented Jul 3, 2019

Thanks for the quick comment, @vtjnash.

Indeed, this would now make the function specialized on (and slow for) all types, since we can't create a single guard signature and pre-resolve the dispatch

Gotcha, thanks for the explanation. that makes sense!

TBH it's a bit awkward to use fieldcount() on tuples anyway, because they aren't really structs, so maybe we could just have a different function for this in base? Like what you're proposing in the other PR, but use a Real Name, instead of a _private one (and export it).

I think the applicable concept is arity, but that feels a bit too esoteric for a name for this..

@quinnj
Copy link
Member

quinnj commented Jul 4, 2019

Just commenting that whatever the solution, we should include NamedTuple in it as well (I've been meaning to bring this up as I keep seeing the same kind of poor codegen for fieldcount)

@NHDaly
Copy link
Member Author

NHDaly commented Dec 31, 2019

@vtjnash I'm going back through some old notifications and came back here. :)

I know that you referenced https://github.com/JuliaLang/julia/pull/32427/files#diff-fd99c9a15dadc23d9200069dadafb8bbR19, where the stated goal was explicitly to reduce usage of @pure, BUT if you're (reasonably) concerned about specializations blowup, could we do this with a @pure method just for tuples?:

julia> Base.@pure fieldcount_pure_reflection(@nospecialize t::Type{<:Tuple}) = length(t.parameters)
fieldcount_pure_reflection (generic function with 1 method)

julia> @btime fieldcount_pure_reflection(Tuple{Int,Int,Int})
  0.035 ns (0 allocations: 0 bytes)
3

That will not specialize new methods for new types.

(And so then I think we could be flexible on whether to make this a separate function only for tuples, or to add a method to fieldcount, right?)

EDIT: (and for NamedTuples, of course)

@vtjnash
Copy link
Member

vtjnash commented Dec 31, 2019

That @nospecialize is likely overruled by the higher priority where T.

Yeah, that's roughly what I did in #32427. Other parts of that triggered various type system bugs, so I got side-tracked.

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.

3 participants