From 1f8df2a1e96ca9b68fe2e7bdeb3426d216059420 Mon Sep 17 00:00:00 2001 From: Cody Tapscott Date: Sun, 1 Oct 2023 15:23:58 -0400 Subject: [PATCH] Add preliminary `ignore_throw` support This allows us to filter allocations that occur only when throwing errors in called functions. Before this change: ```julia julia> check_allocs(*, (Matrix{Float64},Matrix{Float64})) 20-element Vector{AllocCheck.AllocInstance}: ... ``` After: ```julia julia> check_allocs(*, (Matrix{Float64},Matrix{Float64}); ignore_throw=true) 1-element Vector{AllocCheck.AllocInstance}: Allocation of Matrix{Float64} in ./boot.jl:477 | Array{T,2}(::UndefInitializer, m::Int, n::Int) where {T} = Stacktrace: [1] Array @ ./boot.jl:477 [inlined] [2] Array @ ./boot.jl:485 [inlined] [3] similar @ ./array.jl:418 [inlined] [4] *(A::Matrix{Float64}, B::Matrix{Float64}) @ LinearAlgebra ~/.julia/juliaup/julia-1.10.0-beta2+0.x64.linux.gnu/share/julia/stdlib/v1.10/LinearAlgebra/src/matmul.jl:113 ``` --- Project.toml | 3 +-- README.md | 23 ++++++++--------------- src/AllocCheck.jl | 42 ++++++++++++++++++++++++++++++++++++++++-- src/utils.jl | 27 +-------------------------- 4 files changed, 50 insertions(+), 45 deletions(-) diff --git a/Project.toml b/Project.toml index 00aadb1..f1c64c2 100644 --- a/Project.toml +++ b/Project.toml @@ -4,13 +4,12 @@ authors = ["JuliaHub Inc."] version = "1.0.0-DEV" [deps] -Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" GPUCompiler = "61eb1bfa-7361-4325-ad38-22787b887f55" LLVM = "929cbde3-209d-540e-8aea-75f648917ca0" [compat] GPUCompiler = "0.24" -LLVM = "6.2" +LLVM = "6.3" julia = "1.10" [extras] diff --git a/README.md b/README.md index 601b19b..6bf080f 100644 --- a/README.md +++ b/README.md @@ -16,23 +16,16 @@ julia> linsolve(a, b) = a \ b julia> length(check_allocs(linsolve, (Matrix{Float64}, Vector{Float64}))) 175 -``` - -#### Known Limitations - 1. Error paths +julia> length(check_allocs(sin, (Float64,))) +2 - Allocations in error-throwing paths are not distinguished from other allocations: +julia> length(check_allocs(sin, (Float64,); ignore_throw=true)) # ignore allocations that only happen when throwing errors +0 +``` -```julia -julia> check_allocs(sin, (Float64,))[1] +#### Limitations -Allocation of Float64 in ./special/trig.jl:28 - | @noinline sin_domain_error(x) = throw(DomainError(x, "sin(x) is only defined for finite x.")) + 1. Runtime dispatch -Stacktrace: - [1] sin_domain_error(x::Float64) - @ Base.Math ./special/trig.jl:28 - [2] sin(x::Float64) - @ Base.Math ./special/trig.jl:39 -``` + Any runtime dispatch is conservatively assumed to allocate. diff --git a/src/AllocCheck.jl b/src/AllocCheck.jl index 1172908..93e8904 100644 --- a/src/AllocCheck.jl +++ b/src/AllocCheck.jl @@ -148,6 +148,39 @@ function Base.show(io::IO, alloc::AllocInstance) end end +function rename_calls_and_throws!(f::LLVM.Function, job) + + # In order to detect whether an instruction executes only when + # throwing an error, we re-write all throws to pass through the + # same basic block and then we check whether the instruction + # is post-dominated by this "any_throw" basic block. + any_throw = BasicBlock(f, "any_throw") + + builder = IRBuilder() + position!(builder, any_throw) + throw_ret = ret!(builder) # Dummy instruction for dominance test + for block in blocks(f) + for inst in instructions(block) + if isa(inst, LLVM.CallInst) + rename_ir!(job, inst) + + decl = called_operand(inst) + # TODO: Identify catch blocks and filter any functions + # called only in throw-only contexts + if name(decl) == "ijl_throw" || name(decl) == "llvm.trap" + position!(builder, block) + brinst = br!(builder, any_throw) + end + end + end + end + dispose(builder) + + # Return the "any_throw" instruction so that it can be used + # for post-dominance tests. + return throw_ret +end + """ check_allocs(func, types; entry_abi=:specfunc, ret_mod=false) @@ -167,7 +200,7 @@ AllocCheck.AllocInstance[] ``` """ -function check_allocs(@nospecialize(func), @nospecialize(types); entry_abi=:specfunc, ret_mod=false) +function check_allocs(@nospecialize(func), @nospecialize(types); entry_abi=:specfunc, ret_mod=false, ignore_throw=false) job = create_job(func, types; entry_abi) allocs = AllocInstance[] mod = JuliaContext() do ctx @@ -177,12 +210,16 @@ function check_allocs(@nospecialize(func), @nospecialize(types); entry_abi=:spec # display(mod) (; compiled) = ir[2] for f in functions(mod) + throw = rename_calls_and_throws!(f, job) + postdom = LLVM.PostDomTree(f) for block in blocks(f) for inst in instructions(block) if isa(inst, LLVM.CallInst) - rename_ir!(job, inst) decl = called_operand(inst) if is_alloc_function(name(decl)) + throw_only = dominates(postdom, throw, inst) + ignore_throw && throw_only && continue + bt = backtrace_(inst; compiled) alloc = AllocInstance(inst, bt) push!(allocs, alloc) @@ -190,6 +227,7 @@ function check_allocs(@nospecialize(func), @nospecialize(types); entry_abi=:spec end end end + dispose(postdom) end mod end diff --git a/src/utils.jl b/src/utils.jl index ddb17e2..75ae1b8 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -1,28 +1,3 @@ -using Base64 - -file_exists_at(x) = try isfile(x); catch; false end -const BUILDBOT_STDLIB_PATH = dirname(abspath(String(pathof(Base64)), "..", "..", "..")) -replace_buildbot_stdlibpath(str::String) = replace(str, BUILDBOT_STDLIB_PATH => Sys.STDLIB) - -""" - path = fixup_stdlib_source_path(path::String) - -Return `path` corrected for julia issue [#26314](https://github.com/JuliaLang/julia/issues/26314) if applicable. -Otherwise, return the input `path` unchanged. - -Due to the issue mentioned above, location info for methods defined one of Julia's standard libraries -are, for non source Julia builds, given as absolute paths on the worker that built the `julia` executable. -This function corrects such a path to instead refer to the local path on the users drive. -""" -function fixup_stdlib_source_path(path) - if !file_exists_at(path) - maybe_stdlib_path = replace_buildbot_stdlibpath(path) - file_exists_at(maybe_stdlib_path) && return maybe_stdlib_path - end - return path -end - - """ path = fixup_source_path(path) @@ -37,5 +12,5 @@ function fixup_source_path(file) file = normpath(newfile) end end - return fixup_stdlib_source_path(file) + return Base.fixup_stdlib_path(file) end