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

@nospecialize does not work with unnamed parameters #56688

Open
archermarx opened this issue Nov 26, 2024 · 0 comments
Open

@nospecialize does not work with unnamed parameters #56688

archermarx opened this issue Nov 26, 2024 · 0 comments

Comments

@archermarx
Copy link
Contributor

Summary

@nospecialize does not work when attached to unused parameters in a function signature (i.e. those named _).
More generally, unused parameters, unless both named and used with @no_specialize will generate specializations even if they are unable to affect the result of a method invocation.

Description

Consider the following three function definitions

f(a, b, _) = a + b / 2

f(1, 2, 3)
f(1, 2, 3.0)
f(1, 2, 3.0 + im)

g(a, b, @nospecialize _) = a + b / 2

g(1, 2, 3)
g(1, 2, 3.0)
g(1, 2, 3.0 + im)

h(a, b, @nospecialize _x) = a + b / 2

h(1, 2, 3)
h(1, 2, 3.0)
h(1, 2, 3.0 + im)

If we inspect the specializations for these methods, we find the following

julia> Base.specializations(methods(f)[1])
Base.MethodSpecializations(svec(MethodInstance for f(::Int64, ::Int64, ::Int64), MethodInstance for f(::Int64, ::Int64, ::Float64),
 MethodInstance for f(::Int64, ::Int64, ::ComplexF64), nothing, nothing, nothing, nothing))

julia> Base.specializations(methods(g)[1])
Base.MethodSpecializations(svec(MethodInstance for g(::Int64, ::Int64, ::Int64), MethodInstance for g(::Int64, ::Int64, ::Float64),
 MethodInstance for g(::Int64, ::Int64, ::ComplexF64), nothing, nothing, nothing, nothing))

julia> Base.specializations(methods(h)[1])
Base.MethodSpecializations(MethodInstance for h(::Int64, ::Int64, ::Any))

In short, despite the throwaway parameter being unused, the functions specialize on it regardless, even if @nospecialize is used, unless the throwaway parameter is given a name.

This leads to the generation of a lot of useless code, potentially significantly increasing compile times. As a more realistic example, closer to how I ran across this issue in my code while trying to reduce precompilation times, consider the following code which defines an interface:

# Interface definition
abstract type Model end

function do_something(::T, _, _) where {T <: Model}
    throw(ArgumentError("$(nameof(T)) has not implemented the `do_something` interface!"))
end

# Model1 uses both parameters
struct Model1 <: Model end

function do_something(::Model1, param1, param2)
    return param1 + param2
end

# Model2 uses only one param
struct Model2 <: Model
    val::Float64
end

function do_something(m::Model2, param1, _)
    return param1 * m.val
end

# Model3 does not implement the interface
struct Model3 <: Model end

try
    do_something(Model3(), 1.0, 2.0)
catch
end

try
    do_something(Model3(), 1.0, 2)
catch
end

do_something(Model1(), 1.0, 2.0)
do_something(Model1(), 1.0, 2)
do_something(Model2(5.0), 1.0, 2.0)
do_something(Model2(5.0), 1.0, 2)

@show length(Base.specializations(methods(do_something)[1])) # 2
@show length(Base.specializations(methods(do_something)[2])) # 2
@show length(Base.specializations(methods(do_something)[2])) # 2

Ideally, neither the fallback method of do_something nor the one taking a Model2 would specialize on their unused parameters. However, each generates two specializations. This can be fixed by using @no_specialize and a named parameter,
but is quite ugly and pretty advanced. It can also lead to superfluous IDE tooling warnings about unused parameters.

Versions

@mcabbott tested this and found that it occurs on both v1.10 and nightly. I have found it to also occur on v1.11.1

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

No branches or pull requests

1 participant