Skip to content

Commit

Permalink
Add preliminary ignore_throw support
Browse files Browse the repository at this point in the history
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
```
  • Loading branch information
topolarity committed Oct 1, 2023
1 parent 44fda20 commit 1f8df2a
Show file tree
Hide file tree
Showing 4 changed files with 50 additions and 45 deletions.
3 changes: 1 addition & 2 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
23 changes: 8 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
42 changes: 40 additions & 2 deletions src/AllocCheck.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -177,19 +210,24 @@ 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)
end
end
end
end
dispose(postdom)
end
mod
end
Expand Down
27 changes: 1 addition & 26 deletions src/utils.jl
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -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

0 comments on commit 1f8df2a

Please sign in to comment.