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

Inlining/elision of length(::Const(::SimpleVector)) #37314

Closed
martinholters opened this issue Sep 1, 2020 · 1 comment
Closed

Inlining/elision of length(::Const(::SimpleVector)) #37314

martinholters opened this issue Sep 1, 2020 · 1 comment

Comments

@martinholters
Copy link
Member

Coming from discussion at #37230; related to #37303.

julia> foo() = length(Tuple{Any,Any}.parameters)
foo (generic function with 1 method)

julia> @code_typed foo()
CodeInfo(
1%1 = Base.getfield(Tuple{Any, Any}, :parameters)::Core.SimpleVector%2 = Main.length(%1)::Core.Const(2, false)
└──      return %2
) => Int64

Here, the result of the length call is known at compile time, but the call to length is not elided. Even worse, it uses dynamic dispatch, resulting in relatively bad performance.

I've poked at his a bit, but I'm not 100% happy with what I've got so far. First attempt is

--- a/base/compiler/abstractinterpretation.jl
+++ b/base/compiler/abstractinterpretation.jl
@@ -982,7 +982,7 @@ function abstract_call_known(interp::AbstractInterpreter, @nospecialize(f),
         return CallMeta(abstract_call_known(interp, <:, fargs, argtypes, sv).rt, false)
     elseif la == 2 && isa(argtypes[2], Const) && isa(argtypes[2].val, SimpleVector) && istopfunction(f, :length)
         # mark length(::SimpleVector) as @pure
-        return CallMeta(Const(length(argtypes[2].val)), false)
+        return CallMeta(Const(length(argtypes[2].val)), nothing)
     elseif la == 3 && isa(argtypes[2], Const) && isa(argtypes[3], Const) &&
             isa(argtypes[2].val, SimpleVector) && isa(argtypes[3].val, Int) && istopfunction(f, :getindex)
         # mark getindex(::SimpleVector, i::Int) as @pure

The false there tells the optimizer not to worry about this call. But apparently, it should. That gives:

julia> @code_typed foo()
CodeInfo(
1%1 = Base.getfield(Tuple{Any, Any}, :parameters)::Core.SimpleVector%2 = $(Expr(:gc_preserve_begin, :(%1)))
│   %3 = $(Expr(:foreigncall, :(:jl_value_ptr), Ptr{Nothing}, svec(Any), 0, :(:ccall), :(%1)))::Ptr{Nothing}%4 = Base.bitcast(Ptr{Int64}, %3)::Ptr{Int64}%5 = Base.pointerref(%4, 1, 1)::Int64$(Expr(:gc_preserve_end, :(%2)))
└──      return %5
) => Int64

So length is inlined and the dynamic dispatch is gone, but there is still unnecessary code left. Note, however, that the constant 2, although apparently lost, is still there ... somewhere:

julia> bar() = length(Tuple{Any,Any}.parameters) + 1
bar (generic function with 1 method)

julia> @code_typed bar()
CodeInfo(
1%1 = Base.getfield(Tuple{Any, Any}, :parameters)::Core.SimpleVector%2 = $(Expr(:gc_preserve_begin, :(%1)))
│        $(Expr(:foreigncall, :(:jl_value_ptr), Ptr{Nothing}, svec(Any), 0, :(:ccall), :(%1)))::Ptr{Nothing}$(Expr(:gc_preserve_end, :(%2)))
└──      return 3
) => Int64

My second attempt was to completely elide the length call by declaring it effect-free with

--- a/base/compiler/ssair/queries.jl
+++ b/base/compiler/ssair/queries.jl
@@ -31,6 +31,9 @@ function stmt_effect_free(@nospecialize(stmt), @nospecialize(rt), src, sptypes::
                         Any[argextype(ea[i], src, sptypes) for i = 2:length(ea)])
             end
             contains_is(_PURE_BUILTINS, f) && return true
+            if length(ea) == 2 && widenconst(argextype(ea[2], src, sptypes)) == SimpleVector && isa(rt, Const) && istopfunction(f, :length)
+                return true
+            end
             contains_is(_PURE_OR_ERROR_BUILTINS, f) || return false
             rt === Bottom && return false
             return _builtin_nothrow(f, Any[argextype(ea[i], src, sptypes) for i = 2:length(ea)], rt)

(And this is instead of, not in addition to, the diff above.)
Then I get:

julia> @code_typed bar()
CodeInfo(
1return 3
) => Int64

Yay! But still:

julia> @code_typed foo()
CodeInfo(
1%1 = Base.getfield(Tuple{Any, Any}, :parameters)::Core.SimpleVector%2 = Main.length(%1)::Core.Const(2, false)
└──      return %2
) => Int64

So for bar(), further constant-propagting the result of length through the + 1 gets rid of its use by eliding the addition, allowing length to be elided as well. But for foo(), the return is a use of length's result, preventing its elision. But why can't it be replaced with its constant return value? Apparently I have not found the correct spot to patch here. @Keno to the rescue!

@martinholters
Copy link
Member Author

Seems fixed, most likely by the effects machinery.

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