From a164e7f5299e8dc6d8a9424e16b7cceded2dcaa2 Mon Sep 17 00:00:00 2001 From: KristofferC Date: Tue, 6 Dec 2022 17:05:12 +0100 Subject: [PATCH] wip: simplify foreigncall handling --- src/optimize.jl | 138 ++++++++---------------------------------------- 1 file changed, 23 insertions(+), 115 deletions(-) diff --git a/src/optimize.jl b/src/optimize.jl index 0e87f0de..86d214bd 100644 --- a/src/optimize.jl +++ b/src/optimize.jl @@ -135,7 +135,7 @@ end function lookup_getproperties(a::Expr) if a.head === :call && length(a.args) == 3 && - a.args[1] isa QuoteNode && a.args[1].value === Base.getproperty && + a.args[1] isa QuoteNode && a.args[1].value === Base.getproperty && a.args[2] isa QuoteNode && a.args[2].value isa Module && a.args[3] isa QuoteNode && a.args[3].value isa Symbol return lookup_global_ref(Core.GlobalRef(a.args[2].value, a.args[3].value)) @@ -179,47 +179,22 @@ function optimize!(code::CodeInfo, scope) # Replace :llvmcall and :foreigncall with compiled variants. See # https://github.com/JuliaDebug/JuliaInterpreter.jl/issues/13#issuecomment-464880123 foreigncalls_idx = Int[] - delete_idxs = Int[] for (idx, stmt) in enumerate(code.code) # Foregincalls can be rhs of assignments if isexpr(stmt, :(=)) stmt = (stmt::Expr).args[2] end if isa(stmt, Expr) - if stmt.head === :call - # Check for :llvmcall - arg1 = stmt.args[1] - if (arg1 === :llvmcall || lookup_stmt(code.code, arg1) === Base.llvmcall) && isempty(sparams) && scope isa Method - nargs = length(stmt.args)-4 - # Call via `invokelatest` to avoid compiling it until we need it - delete_idx = Base.invokelatest(build_compiled_call!, stmt, Base.llvmcall, code, idx, nargs, sparams, evalmod) - delete_idx === nothing && error("llvmcall must be compiled, but exited early from build_compiled_call!") - push!(foreigncalls_idx, idx) - append!(delete_idxs, delete_idx) - end - elseif stmt.head === :foreigncall && scope isa Method - nargs = length(stmt.args[3]::SimpleVector) + is_llvmcall = stmt.head === :call && (stmt.args[1] === :llvmcall || lookup_stmt(code.code, stmt.args[1]) === Base.llvmcall) && isempty(sparams) && scope isa Method + is_ccall = stmt.head === :foreigncall && scope isa Method + if is_llvmcall || is_ccall # Call via `invokelatest` to avoid compiling it until we need it - delete_idx = Base.invokelatest(build_compiled_call!, stmt, :ccall, code, idx, nargs, sparams, evalmod) - if delete_idx !== nothing - push!(foreigncalls_idx, idx) - append!(delete_idxs, delete_idx) - end + Base.invokelatest(build_compiled_call!, stmt, code, sparams, evalmod) + push!(foreigncalls_idx, idx) end end end - if !isempty(delete_idxs) - ssalookup = compute_ssa_mapping_delete_statements!(code, delete_idxs) - let lkup = ssalookup - foreigncalls_idx = map(x -> lkup[x], foreigncalls_idx) - end - deleteat!(codelocs(code), delete_idxs) - deleteat!(code.code, delete_idxs) - code.ssavaluetypes = length(code.code) - renumber_ssa!(code.code, ssalookup) - end - ## Un-nest :call expressions (so that there will be only one :call per line) # This will allow us to re-use args-buffers rather than having to allocate new ones each time. old_code, old_codelocs = code.code, codelocs(code) @@ -274,64 +249,16 @@ function parametric_type_to_expr(@nospecialize(t::Type)) end # Handle :llvmcall & :foreigncall (issue #28) -function build_compiled_call!(stmt::Expr, fcall, code, idx, nargs::Int, sparams::Vector{Symbol}, evalmod) - TVal = evalmod == Core.Compiler ? Core.Compiler.Val : Val +function build_compiled_call!(stmt::Expr, code, sparams::Vector{Symbol}, evalmod) + nargs = length(stmt.args) - 5 delete_idx = Int[] - if fcall === :ccall - cfunc, RetType, ArgType = lookup_stmt(code.code, stmt.args[1]), stmt.args[2], stmt.args[3]::SimpleVector - # delete cconvert and unsafe_convert calls and forward the original values, since - # the same conversions will be applied within the generated compiled variant of this :foreigncall anyway - args = [] - for (atype, arg) in zip(ArgType, stmt.args[6:6+nargs-1]) - if atype === Any - push!(args, arg) - else - if arg isa SSAValue - unsafe_convert_expr = code.code[arg.id]::Expr - push!(delete_idx, arg.id) # delete the unsafe_convert - cconvert_val = unsafe_convert_expr.args[3] - if isa(cconvert_val, SSAValue) - push!(delete_idx, cconvert_val.id) # delete the cconvert - newarg = (code.code[cconvert_val.id]::Expr).args[3] - push!(args, newarg) - else - @assert isa(cconvert_val, SlotNumber) - push!(args, cconvert_val) - end - elseif arg isa SlotNumber - idx = findfirst(code.code) do expr - Meta.isexpr(expr, :(=)) || return false - lhs = expr.args[1] - return lhs isa SlotNumber && lhs.id === arg.id - end::Int - unsafe_convert_expr = code.code[idx]::Expr - push!(delete_idx, idx) # delete the unsafe_convert - push!(args, unsafe_convert_expr.args[2]) - else - error("unexpected foreigncall argument type encountered: $(typeof(arg))") - end - end - end - else - # Run a mini-interpreter to extract the types - framecode = FrameCode(CompiledCalls, code; optimize=false) - frame = Frame(framecode, prepare_framedata(framecode, [])) - idxstart = idx - for i = 2:4 - idxstart = smallest_ref(code.code, stmt.args[i], idxstart) - end - frame.pc = idxstart - if idxstart < idx - while true - pc = step_expr!(Compiled(), frame) - pc === idx && break - pc === nothing && error("this should never happen") - end - end - cfunc, RetType, ArgType = @lookup(frame, stmt.args[2]), @lookup(frame, stmt.args[3]), @lookup(frame, stmt.args[4])::DataType - args = stmt.args[5:end] - end + TVal = evalmod == Core.Compiler ? Core.Compiler.Val : Val + + cfunc, RetType, ArgType = lookup_stmt(code.code, stmt.args[1]), stmt.args[2], stmt.args[3]::SimpleVector + args = stmt.args[6:end] + dynamic_ccall = false + oldcfunc = nothing if isa(cfunc, Expr) # specification by tuple, e.g., (:clock, "libc") cfunc = something(static_eval(cfunc), cfunc) end @@ -344,18 +271,12 @@ function build_compiled_call!(stmt::Expr, fcall, code, idx, nargs::Int, sparams: oldcfunc = cfunc cfunc = gensym("ptr") end - if isa(RetType, SimpleVector) - @assert length(RetType) == 1 - RetType = RetType[1] - end # When the ccall is dynamic we pass the pointer as an argument so can reuse the function - cc_key = (dynamic_ccall ? :ptr : cfunc, RetType, ArgType, evalmod, length(sparams)) # compiled call key + cc_key = (cfunc, RetType, ArgType, evalmod, length(sparams)) # compiled call key f = get(compiled_calls, cc_key, nothing) argnames = Any[Symbol(:arg, i) for i = 1:nargs] if f === nothing - if fcall === :ccall - ArgType = Expr(:tuple, Any[parametric_type_to_expr(t) for t in ArgType::SimpleVector]...) - end + ArgType = Expr(:tuple, Any[parametric_type_to_expr(t) for t in ArgType::SimpleVector]...) RetType = parametric_type_to_expr(RetType) # #285: test whether we can evaluate an type constraints on parametric expressions # this essentially comes down to having the names be available in CompiledCalls, @@ -367,30 +288,17 @@ function build_compiled_call!(stmt::Expr, fcall, code, idx, nargs::Int, sparams: return nothing end wrapargs = copy(argnames) - if dynamic_ccall - pushfirst!(wrapargs, cfunc) - end for sparam in sparams push!(wrapargs, :(::$TVal{$sparam})) end - methname = gensym("compiledcall") - calling_convention = stmt.args[5] - if calling_convention === :(:llvmcall) - def = :( - function $methname($(wrapargs...)) where {$(sparams...)} - return $fcall($cfunc, llvmcall, $RetType, $ArgType, $(argnames...)) - end) - elseif calling_convention === :(:stdcall) - def = :( - function $methname($(wrapargs...)) where {$(sparams...)} - return $fcall($cfunc, stdcall, $RetType, $ArgType, $(argnames...)) - end) - else - def = :( - function $methname($(wrapargs...)) where {$(sparams...)} - return $fcall($cfunc, $RetType, $ArgType, $(argnames...)) - end) + if dynamic_ccall + pushfirst!(wrapargs, cfunc) end + methname = gensym("compiledcall") + def = :( + function $methname($(wrapargs...)) where {$(sparams...)} + return $(Expr(:foreigncall, cfunc, RetType, stmt.args[3:5]..., argnames...)) + end) f = Core.eval(evalmod, def) compiled_calls[cc_key] = f end