Skip to content

It is too hard to reason about when Julia specializes arguments #51423

@jakobnissen

Description

@jakobnissen

Background

Julia users quickly learn that Julia specializes code for the types of every argument, and that every function is its own type.
However, invariably, users will try to write a function like this:

function foo(f, v)
    v2 = map(f, v)
    y = 0
    for i in v2
        y += i
    end
    y
end

And find that it allocates like crazy:

julia x = collect(1:1_000_000);

julia> @time foo(isodd, x);
  0.084467 seconds (2.00 M allocations: 31.448 MiB)

This is extraordinarily unintuitive, but of course this is a well known issue.

Strangely, there are various ways you can regain specialization, for example here - where I use f in the function:

function foo(f, v)
    v2 = map(f, v)
    y = f(first(v)) - f(first(v)) # zero, but uses f
    for i in v2
        y += i
    end
    y
end

You also can enable specialization by annotating f::F.
And you can disable specialization using @nospecialize - but look at what happens if you do these toggether:

function foo(@nospecialize(f::F), v) where F
    v2 = map(f, v)
    y = 0
    for i in v2
        y += i
    end
    y
end

julia> @time foo(isodd, x);
  0.001473 seconds (3 allocations: 976.625 KiB)

@nospecialize is not respected. Even when you apply @nospecialize to the whole function:

julia> @nospecialize function foo(f::F, v) where F
           v2 = map(f, v)
           y = 0
           for i in v2
               y += i
           end
           y
       end

julia> @time foo(isodd, x);
ERROR: UndefVarError: `foo` not defined

Oops - @nospecialize silently deleted the function there.

It seems very hard to understand when Julia specializes and when it does not.
Luckily, we have code introspection tooling like @code_warntype - but of course, this doesn't work in this case and actively misleads the investigator as to what is wrong (a known issue: #32834).

Furthermore, it's quite unclear what kind of specialization exactly is blocked by @nospecialize - as a user, the mental model of @nospecialize is presumably: "Julia just treats this @nospecialize(x::Integer) as an Integer, nothing else". But this isn't the case, and the actual semantics of @nospecialize is kind of complex, see #41931.

In short:

This issue

While each of the individual issues linked to are already known, I think the overall issue really is this: What can we expect a Julia user to intuit or learn about specialization?

If specialization is critical for fast code, then users must be able to have their code specialize, which means they must be able have a working understanding of when it specializes. I argue that it is currently so hard to understand what is happening that you need to be a Julia compiler developer, or have years of Julian experience, to really understand what is happening.

If specialization is considered a compiler implementation detail, then it is unacceptable that Julia chooses to despecialize code seemingly arbitrarily

Possible solutions

Perhaps the simplest solution would be: Julia always specializes, and always respects @nospecialize.

Perhaps effects could be used to achieve this: In the original function map(f, v) in this post, this could be made to specialize because Julia knew that the result of the map was used in the rest of the function. This way, lack of specialization would never affect type stability and therefore could be more of a compiler implementation detail, with the semantics being that Julia always specializes. This would also make #32834 less critical to fix, and would enable us to remove the section from the Julia performance tips.

Metadata

Metadata

Assignees

No one assigned

    Labels

    designDesign of APIs or of the language itselfperformanceMust go fastertypes and dispatchTypes, subtyping and method dispatch

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions