Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions base/compiler/optimize.jl
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,26 @@ function ipo_escape_cache(mi_cache::MICache) where MICache
end
null_escape_cache(linfo::Union{InferenceResult,MethodInstance}) = nothing

"""
run_minimal_passes(
ci::CodeInfo,
sv::OptimizationState,
caller::InferenceResult,
) -> ir::IRCode

Transform a `CodeInfo` to an `IRCode`. Like [`run_passes`](@ref) but it does not run the
majority of optimization passes.
"""
function run_minimal_passes(ci::CodeInfo, sv::OptimizationState, caller::InferenceResult)
@timeit "convert" ir = convert_to_ircode(ci, sv)
@timeit "slot2reg" ir = slot2reg(ir, ci, sv)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would not consider this to be a minimal pass, since it is quite a significant optimization and change to the IR

Suggested change
@timeit "slot2reg" ir = slot2reg(ir, ci, sv)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be very rare to deal with IRCode without slog2reg (unless you are changing slog2reg itself). For one thing, verify_ir considers IRCode with slots invalid:

elseif isa(op, Union{SlotNumber, TypedSlot})
@verify_error "Left over slot detected in converted IR"

So, I think running slot2reg with optimize = false makes sense, if we treat it as "least optimized typical IRCode that compiler passes deal with." But running slot2reg manually is not super hard. So I don't have super strong opinion about it.

@aviatesk @ianatol Any thoughts?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we can also pass numeric optimize_level

function run_passes(ci::CodeInfo, sv::OptimizationState, caller::InferenceResult, optimize_level = typeinf(Int))
    stage = 1
    isdone() = optimize_level < (stage += 1)
    @timeit "convert"   ir = convert_to_ircode(ci, sv)
    isdone() && return ir
    @timeit "slot2reg"  ir = slot2reg(ir, ci, sv)
    isdone() && return ir
    # TODO: Domsorting can produce an updated domtree - no need to recompute here
    @timeit "compact 1" ir = compact!(ir)
    isdone() && return ir
    @timeit "Inlining"  ir = ssa_inlining_pass!(ir, ir.linetable, sv.inlining, ci.propagate_inbounds)
    isdone() && return ir
    ...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer running slot2reg as we often work on post-slot2reg IRCode when developing Julia-level optimizer. I also think the other "pre-inlining optimizations (mostly DCE)" happening within convert_to_ircode and type_annotate! is also significant anyway.

And the optimize_level idea sounds very useful!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added optimize_level 04797d5

julia> function demo(f)
           y = f()
           y isa Any ? nothing : f()
       end;

julia> Base.code_ircode(demo; optimize_level = 1) |> only |> first
2 1 ─      (_3 = (_2)())::#UNDEF                                                                   │
3%2 = (_3 isa Main.Any)::#UNDEF                                                               │
  └──      goto #5 if not %2::#UNDEF                                                               │
  2return Main.nothing::#UNDEF                                                             │
  3 ─      Core.Const(:((_2)()))::#UNDEF                                                           │
  │        Core.Const(:(return %5))::#UNDEF                                                        │
  └──      unreachable::#UNDEF                                                                     │


julia> Base.code_ircode(demo; optimize_level = 2) |> only |> first
2 1%1 = (_2)()::Any3%2 = (%1 isa Main.Any)::Core.Const(true)                                                     │
  └──      goto #3 if not %2                                                                       │
  2return Main.nothing3 ─      Core.Const(:((_2)()))::Union{}                                                          │
  │        Core.Const(:(return %5))::Union{}                                                       │
  └──      unreachable                                                                             │


julia> Base.code_ircode(demo) |> only |> first
2 1 ─     (_2)()::Any3 └──     goto #3 if not true                                                                      │
  2return Main.nothing3 ─     unreachable                                                                              │

@timeit "compact" ir = compact!(ir)
if JLOptions().debug_level == 2
@timeit "verify" (verify_ir(ir); verify_linetable(ir.linetable))
end
return ir
end

function run_passes(ci::CodeInfo, sv::OptimizationState, caller::InferenceResult)
@timeit "convert" ir = convert_to_ircode(ci, sv)
@timeit "slot2reg" ir = slot2reg(ir, ci, sv)
Expand Down
31 changes: 31 additions & 0 deletions base/compiler/typeinfer.jl
Original file line number Diff line number Diff line change
Expand Up @@ -918,6 +918,37 @@ function typeinf_code(interp::AbstractInterpreter, method::Method, @nospecialize
return code, rt
end

"""
typeinf_ircode(
interp::AbstractInterpreter,
method::Method,
atype,
sparams::SimpleVector,
run_optimizer::Bool,
) -> (ir::Union{IRCode,Nothing}, returntype::Type)

Infer a `method` and return an `IRCode` with inferred `returntype` on success.
"""
function typeinf_ircode(interp::AbstractInterpreter, method::Method, @nospecialize(atype), sparams::SimpleVector, run_optimizer::Bool)
ccall(:jl_typeinf_begin, Cvoid, ())
frame = typeinf_frame(interp, method, atype, sparams, false)
if frame === nothing
ccall(:jl_typeinf_end, Cvoid, ())
return nothing, Any
end
(; result) = frame
opt_params = OptimizationParams(interp)
opt = OptimizationState(frame, opt_params, interp)
if run_optimizer
ir = run_passes(opt.src, opt, result)
else
ir = run_minimal_passes(opt.src, opt, result)
end
rt = widenconst(ignorelimited(result.result))
ccall(:jl_typeinf_end, Cvoid, ())
return ir, rt
end

# compute an inferred frame
function typeinf_frame(interp::AbstractInterpreter, method::Method, @nospecialize(atype), sparams::SimpleVector, run_optimizer::Bool)
mi = specialize_method(method, atype, sparams)::MethodInstance
Expand Down
86 changes: 86 additions & 0 deletions base/reflection.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1291,6 +1291,92 @@ function code_typed_opaque_closure(@nospecialize(oc::Core.OpaqueClosure);
end
end

"""
code_ircode(f, [types])

Return an array of pairs of `IRCode` and inferred return type if type inference succeeds.
The `Method` is included instead of `IRCode` otherwise.

See also: [`code_typed`](@ref)

# Internal Keyword Arguments

This section should be considered internal, and is only for who understands Julia compiler
internals.

- `world=Base.get_world_counter()`: optional, controls the world age to use when looking up
methods, use current world age if not specified.
- `interp=Core.Compiler.NativeInterpreter(world)`: optional, controls the interpreter to
use, use the native interpreter Julia uses if not specified.

# Example

One can put the argument types in a tuple to get the corresponding `code_ircode`.

```julia
julia> Base.code_ircode(+, (Float64, Float64))
1-element Vector{Any}:
383 1 ─ %1 = Base.add_float(_2, _3)::Float64 │
└── return %1 │
=> Float64
```
"""
function code_ircode(
@nospecialize(f),
@nospecialize(types = default_tt(f));
world = get_world_counter(),
interp = Core.Compiler.NativeInterpreter(world),
optimize::Bool = true,
)
if isa(f, Core.OpaqueClosure)
error("OpaqueClosure not supported")
end
ft = Core.Typeof(f)
if isa(types, Type)
u = unwrap_unionall(types)
tt = rewrap_unionall(Tuple{ft,u.parameters...}, types)
else
tt = Tuple{ft,types...}
end
return code_ircode_by_type(tt; world, interp, optimize)
end

"""
code_ircode_by_type(types::Type{<:Tuple}; ...)

Similar to [`code_ircode`](@ref), except the argument is a tuple type describing
a full signature to query.
"""
function code_ircode_by_type(
@nospecialize(tt::Type);
world = get_world_counter(),
interp = Core.Compiler.NativeInterpreter(world),
optimize::Bool = true,
)
ccall(:jl_is_in_pure_context, Bool, ()) &&
error("code reflection cannot be used from generated functions")
tt = to_tuple_type(tt)
matches = _methods_by_ftype(tt, -1, world)::Vector
asts = []
for match in matches
match = match::Core.MethodMatch
meth = func_for_method_checked(match.method, tt, match.sparams)
(code, ty) = Core.Compiler.typeinf_ircode(
interp,
meth,
match.spec_types,
match.sparams,
optimize,
)
if code === nothing
push!(asts, meth => Any)
else
push!(asts, code => ty)
end
end
return asts
end

function return_types(@nospecialize(f), @nospecialize(types=default_tt(f));
world = get_world_counter(),
interp = Core.Compiler.NativeInterpreter(world))
Expand Down
14 changes: 14 additions & 0 deletions test/compiler/ssair.jl
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,20 @@ f_if_typecheck() = (if nothing; end; unsafe_load(Ptr{Int}(0)))
success(pipeline(Cmd(cmd); stdout=stdout, stderr=stderr)) && isempty(String(take!(stderr)))
end

@testset "code_ircode" begin
@test first(only(Base.code_ircode(+, (Float64, Float64)))) isa Compiler.IRCode
@test first(only(Base.code_ircode(+, (Float64, Float64); optimize = false))) isa
Compiler.IRCode

function demo(f)
f()
f()
f()
end
@test first(only(Base.code_ircode(demo))) isa Compiler.IRCode
@test first(only(Base.code_ircode(demo; optimize = false))) isa Compiler.IRCode
end

let
function test_useref(stmt, v, op)
if isa(stmt, Expr)
Expand Down