Julia: Function redefinition can improve performance/inlining/inference

Created on 11 Jul 2019  ยท  4Comments  ยท  Source: JuliaLang/julia

I don't think this is a particularly new observation and is likely related to #28683, #28940, enduring broadcasting struggles for StaticArrays (https://github.com/JuliaArrays/StaticArrays.jl/issues/482, https://github.com/JuliaArrays/StaticArrays.jl/issues/609), and associated lore about "recursion limiting heuristics" (#29816, #29294), but most of those linked issues are closed now so I figured I'd resurface this old test case that still seems to work in 1.2.0-rc2.0:

  | | |_| | | | (_| |  |  Version 1.2.0-rc2.0 (2019-07-08)
 _/ |\__'_|_|_|\__'_|  |  Official https://julialang.org/ release
|__/                   |

(v1.2) pkg> st
    Status `~/.julia/environments/v1.2/Project.toml`
  [6e4b80f9] BenchmarkTools v0.4.2
  [90137ffa] StaticArrays v0.11.0

julia> using LinearAlgebra, Test, BenchmarkTools, StaticArrays

julia> perp(v) = SVector(v[2], -v[1])
perp (generic function with 1 method)

julia> f(v) = perp.(v) ./ norm.(v)
f (generic function with 1 method)

julia> x = rand(SVector{1,SVector{2,Float64}})
1-element SArray{Tuple{1},SArray{Tuple{2},Float64,1,2},1,1} with indices SOneTo(1):
 [0.5991125473138443, 0.13915128285280431]

julia> @inferred f(x)
ERROR: return type SArray{Tuple{1},SArray{Tuple{2},Float64,1,2},1,1} does not match inferred return type SArray{Tuple{1},_A,1,1} where _A
Stacktrace:
 [1] error(::String) at ./error.jl:33
 [2] top-level scope at REPL[5]:1

julia> @benchmark f($x)
BenchmarkTools.Trial: 
  memory estimate:  96 bytes
  allocs estimate:  3
  --------------
  minimum time:     25.924 ns (0.00% GC)
  median time:      27.211 ns (0.00% GC)
  mean time:        36.222 ns (19.56% GC)
  maximum time:     36.263 ฮผs (99.83% GC)
  --------------
  samples:          10000
  evals/sample:     993

julia> @code_warntype optimize=true f(x)
Variables
  #self#::Core.Compiler.Const(f, false)
  v::SArray{Tuple{1},SArray{Tuple{2},Float64,1,2},1,1}

Body::SArray{Tuple{1},_A,1,1} where _A
1 โ”€ %1 = Core.tuple(v)::Tuple{SArray{Tuple{1},SArray{Tuple{2},Float64,1,2},1,1}}
โ”‚   %2 = %new(Base.Broadcast.Broadcasted{StaticArrays.StaticArrayStyle{1},Nothing,typeof(perp),Tuple{SArray{Tuple{1},SArray{Tuple{2},Float64,1,2},1,1}}}, perp, %1, nothing)::Base.Broadcast.Broadcasted{StaticArrays.StaticArrayStyle{1},Nothing,typeof(perp),Tuple{SArray{Tuple{1},SArray{Tuple{2},Float64,1,2},1,1}}}
โ”‚   %3 = Core.tuple(v)::Tuple{SArray{Tuple{1},SArray{Tuple{2},Float64,1,2},1,1}}
โ”‚   %4 = %new(Base.Broadcast.Broadcasted{StaticArrays.StaticArrayStyle{1},Nothing,typeof(norm),Tuple{SArray{Tuple{1},SArray{Tuple{2},Float64,1,2},1,1}}}, LinearAlgebra.norm, %3, nothing)::Base.Broadcast.Broadcasted{StaticArrays.StaticArrayStyle{1},Nothing,typeof(norm),Tuple{SArray{Tuple{1},SArray{Tuple{2},Float64,1,2},1,1}}}
โ”‚   %5 = Core.tuple(%2, %4)::Tuple{Base.Broadcast.Broadcasted{StaticArrays.StaticArrayStyle{1},Nothing,typeof(perp),Tuple{SArray{Tuple{1},SArray{Tuple{2},Float64,1,2},1,1}}},Base.Broadcast.Broadcasted{StaticArrays.StaticArrayStyle{1},Nothing,typeof(norm),Tuple{SArray{Tuple{1},SArray{Tuple{2},Float64,1,2},1,1}}}}
โ”‚   %6 = %new(Base.Broadcast.Broadcasted{StaticArrays.StaticArrayStyle{1},Tuple{SOneTo{1}},typeof(/),Tuple{Base.Broadcast.Broadcasted{StaticArrays.StaticArrayStyle{1},Nothing,typeof(perp),Tuple{SArray{Tuple{1},SArray{Tuple{2},Float64,1,2},1,1}}},Base.Broadcast.Broadcasted{StaticArrays.StaticArrayStyle{1},Nothing,typeof(norm),Tuple{SArray{Tuple{1},SArray{Tuple{2},Float64,1,2},1,1}}}}}, /, %5, (SOneTo(1),))::Base.Broadcast.Broadcasted{StaticArrays.StaticArrayStyle{1},Tuple{SOneTo{1}},typeof(/),Tuple{Base.Broadcast.Broadcasted{StaticArrays.StaticArrayStyle{1},Nothing,typeof(perp),Tuple{SArray{Tuple{1},SArray{Tuple{2},Float64,1,2},1,1}}},Base.Broadcast.Broadcasted{StaticArrays.StaticArrayStyle{1},Nothing,typeof(norm),Tuple{SArray{Tuple{1},SArray{Tuple{2},Float64,1,2},1,1}}}}}
โ”‚   %7 = invoke Base.Broadcast.copy(%6::Base.Broadcast.Broadcasted{StaticArrays.StaticArrayStyle{1},Tuple{SOneTo{1}},typeof(/),Tuple{Base.Broadcast.Broadcasted{StaticArrays.StaticArrayStyle{1},Nothing,typeof(perp),Tuple{SArray{Tuple{1},SArray{Tuple{2},Float64,1,2},1,1}}},Base.Broadcast.Broadcasted{StaticArrays.StaticArrayStyle{1},Nothing,typeof(norm),Tuple{SArray{Tuple{1},SArray{Tuple{2},Float64,1,2},1,1}}}}})::SArray{Tuple{1},_A,1,1} where _A
โ””โ”€โ”€      return %7

julia> perp(v) = SVector(v[2], -v[1])
perp (generic function with 1 method)

julia> @inferred f(x)
1-element SArray{Tuple{1},SArray{Tuple{2},Float64,1,2},1,1} with indices SOneTo(1):
 [0.22624014036577766, -0.9740715573751618]

julia> @benchmark f($x)
BenchmarkTools.Trial: 
  memory estimate:  0 bytes
  allocs estimate:  0
  --------------
  minimum time:     2.461 ns (0.00% GC)
  median time:      2.954 ns (0.00% GC)
  mean time:        3.125 ns (0.00% GC)
  maximum time:     17.793 ns (0.00% GC)
  --------------
  samples:          10000
  evals/sample:     1000

julia> @code_warntype optimize=true f(x)
Variables
  #self#::Core.Compiler.Const(f, false)
  v::SArray{Tuple{1},SArray{Tuple{2},Float64,1,2},1,1}

Body::SArray{Tuple{1},SArray{Tuple{2},Float64,1,2},1,1}
1 โ”€โ”€ %1  = Base.getfield(v, :data)::Tuple{SArray{Tuple{2},Float64,1,2}}
โ”‚    %2  = Base.getfield(%1, 1, false)::SArray{Tuple{2},Float64,1,2}
โ”‚    %3  = Base.getfield(v, :data)::Tuple{SArray{Tuple{2},Float64,1,2}}
โ”‚    %4  = Base.getfield(%3, 1, false)::SArray{Tuple{2},Float64,1,2}
โ”‚    %5  = Base.getfield(%4, :data)::Tuple{Float64,Float64}
โ”‚    %6  = Base.getfield(%5, 1, false)::Float64
โ”‚    %7  = Base.mul_float(%6, %6)::Float64
โ”‚    %8  = Base.getfield(%4, :data)::Tuple{Float64,Float64}
โ”‚    %9  = Base.getfield(%8, 2, false)::Float64
โ”‚    %10 = Base.mul_float(%9, %9)::Float64
โ”‚    %11 = Base.add_float(%7, %10)::Float64
โ”‚    %12 = Base.lt_float(%11, 0.0)::Bool
โ””โ”€โ”€โ”€       goto #3 if not %12
2 โ”€โ”€       invoke Base.Math.throw_complex_domainerror(:sqrt::Symbol, %11::Float64)
โ””โ”€โ”€โ”€       $(Expr(:unreachable))
3 โ”„โ”€ %16 = Base.Math.sqrt_llvm(%11)::Float64
โ””โ”€โ”€โ”€       goto #4
4 โ”€โ”€       goto #5
5 โ”€โ”€       goto #6
6 โ”€โ”€       goto #7
7 โ”€โ”€ %21 = Base.getfield(%2, :data)::Tuple{Float64,Float64}
โ”‚    %22 = Base.getfield(%21, 2, true)::Float64
โ”‚    %23 = Base.getfield(%2, :data)::Tuple{Float64,Float64}
โ”‚    %24 = Base.getfield(%23, 1, true)::Float64
โ”‚    %25 = Base.neg_float(%24)::Float64
โ””โ”€โ”€โ”€       goto #8
8 โ”€โ”€ %27 = Base.div_float(%22, %16)::Float64
โ”‚    %28 = Base.div_float(%25, %16)::Float64
โ”‚    %29 = StaticArrays.tuple(%27, %28)::Tuple{Float64,Float64}
โ”‚    %30 = %new(SArray{Tuple{2},Float64,1,2}, %29)::SArray{Tuple{2},Float64,1,2}
โ””โ”€โ”€โ”€       goto #9
9 โ”€โ”€ %32 = StaticArrays.tuple(%30)::Tuple{SArray{Tuple{2},Float64,1,2}}
โ”‚    %33 = %new(SArray{Tuple{1},SArray{Tuple{2},Float64,1,2},1,1}, %32)::SArray{Tuple{1},SArray{Tuple{2},Float64,1,2},1,1}
โ””โ”€โ”€โ”€       goto #10
10 โ”€       goto #11
11 โ”€       goto #12
12 โ”€       return %33

Most helpful comment

For my case, I managed to turn it into a smaller example:

g(p) = p[1] => identity.(p[2])
f() = g.((1 => (Val(2), Val(3)), 4 => (Val(5), Val(6))))

Example session:

   _       _ _(_)_     |  Documentation: https://docs.julialang.org
  (_)     | (_) (_)    |
   _ _   _| |_  __ _   |  Type "?" for help, "]?" for Pkg help.
  | | | | | | |/ _` |  |
  | | |_| | | | (_| |  |  Version 1.3.0-DEV.533 (2019-07-10)
 _/ |\__'_|_|_|\__'_|  |  Commit 073cbbb491 (2 days old master)
|__/                   |

julia> using Test

julia> g(p) = p[1] => identity.(p[2])
g (generic function with 1 method)

julia> f() = g.((1 => (Val(2), Val(3)), 4 => (Val(5), Val(6))))
f (generic function with 1 method)

julia> @inferred f()
ERROR: return type Tuple{Pair{Int64,Tuple{Val{2},Val{3}}},Pair{Int64,Tuple{Val{5},Val{6}}}} does not match inferred return type Tuple{Pair{Int64,_A} where _A,Pair{Int64,_A} where _A}
Stacktrace:
 [1] error(::String) at ./error.jl:33
 [2] top-level scope at REPL[4]:1

julia> f() = g.((1 => (Val(2), Val(3)), 4 => (Val(5), Val(6))))
f (generic function with 1 method)

julia> @inferred f()
(1 => (Val{2}(), Val{3}()), 4 => (Val{5}(), Val{6}()))

All 4 comments

FWIW here is a code snippet to trigger a similar issue about inference. ~Like broadcasting, it's heavily relying on recursion.~ (edit: it was broadcasting as well) The inference succeeds in the second try with Julia 1.2.0-rc1.2, 1.2.0-rc2.0, and 1.3.0-DEV.533. In 1.0.3 and 1.1.1, it fails in the second try as well. Here is the record in Travis: https://travis-ci.com/tkf/NDReducibles.jl/builds/118867594

using Pkg
pkg"add https://github.com/tkf/NDReducibles.jl#inference-quirk"
pkgid = Base.PkgId(Base.UUID(0xe7e9fd8f232e412695ea53110d05d1fe), "NDReducibles")
path = Base.locate_package(pkgid)
testdir = joinpath(dirname(dirname(path)), "test")
Pkg.activate(testdir)
Pkg.instantiate()

using NDReducibles: AccessPattern, Index, plan

f1() = plan(AccessPattern.((
    ones(0) => (Index(:i),),
    ones(0, 0) => (Index(:i), Index(:j)),
))...)

using Test
try
    @inferred f1()
catch exception
    @info "Failed as expected" exception
end

@info "Redefining it..."
f2() = plan(AccessPattern.((
    ones(0) => (Index(:i),),
    ones(0, 0) => (Index(:i), Index(:j)),
))...)

@inferred f2()
@info "It worked!"

For my case, I managed to turn it into a smaller example:

g(p) = p[1] => identity.(p[2])
f() = g.((1 => (Val(2), Val(3)), 4 => (Val(5), Val(6))))

Example session:

   _       _ _(_)_     |  Documentation: https://docs.julialang.org
  (_)     | (_) (_)    |
   _ _   _| |_  __ _   |  Type "?" for help, "]?" for Pkg help.
  | | | | | | |/ _` |  |
  | | |_| | | | (_| |  |  Version 1.3.0-DEV.533 (2019-07-10)
 _/ |\__'_|_|_|\__'_|  |  Commit 073cbbb491 (2 days old master)
|__/                   |

julia> using Test

julia> g(p) = p[1] => identity.(p[2])
g (generic function with 1 method)

julia> f() = g.((1 => (Val(2), Val(3)), 4 => (Val(5), Val(6))))
f (generic function with 1 method)

julia> @inferred f()
ERROR: return type Tuple{Pair{Int64,Tuple{Val{2},Val{3}}},Pair{Int64,Tuple{Val{5},Val{6}}}} does not match inferred return type Tuple{Pair{Int64,_A} where _A,Pair{Int64,_A} where _A}
Stacktrace:
 [1] error(::String) at ./error.jl:33
 [2] top-level scope at REPL[4]:1

julia> f() = g.((1 => (Val(2), Val(3)), 4 => (Val(5), Val(6))))
f (generic function with 1 method)

julia> @inferred f()
(1 => (Val{2}(), Val{3}()), 4 => (Val{5}(), Val{6}()))

In the last example, if I run g(0 => (Val(5), Val(6))); g(0 => (Val(2), Val(3))) before defining f then inference succeeds. Conversely, defining f twice without running it in between does not help inference.

Another example of this issue was discussed on the discourse recently.

Are there any plans for resolving this issue?

Was this page helpful?
0 / 5 - 0 ratings

Related issues

i-apellaniz picture i-apellaniz  ยท  3Comments

felixrehren picture felixrehren  ยท  3Comments

iamed2 picture iamed2  ยท  3Comments

tkoolen picture tkoolen  ยท  3Comments

Keno picture Keno  ยท  3Comments