From 773c64e1662ee9f93a580b90225865352bf28e32 Mon Sep 17 00:00:00 2001 From: Cody Tapscott Date: Fri, 17 Nov 2023 11:39:01 -0500 Subject: [PATCH] Update docs for new `@check_allocs` macro --- README.md | 4 +- docs/src/api.md | 6 ++- docs/src/index.md | 47 ++++++++++++------- .../optional_debugging_and_logging.md | 36 ++++++++++---- src/AllocCheck.jl | 2 +- src/macro.jl | 33 +++++++++++++ 6 files changed, 97 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index c5587d6..50943b1 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,6 @@ julia> length(check_allocs(sin, (Float64,); ignore_throw=true)) # ignore allocat #### Limitations - 1. Runtime dispatch + Every call into a `@check_allocs` function behaves like a dynamic dispatch. This means that it can trigger compilation dynamically (involving lots of allocation), and even when the function has already been compiled, a small amount of allocation is still expected on function entry. - Any runtime dispatch is conservatively assumed to allocate. + For most applications, the solution is to use `@check_allocs` to wrap your top-level entry point or your main application loop, in which case those applications are only incurred once. `@check_allocs` will guarantee that no dynamic compilation or allocation occurs once your function has started running. diff --git a/docs/src/api.md b/docs/src/api.md index f6882c9..f262571 100644 --- a/docs/src/api.md +++ b/docs/src/api.md @@ -8,4 +8,8 @@ ```@docs AllocCheck.check_allocs -``` \ No newline at end of file +``` + +```@docs +AllocCheck.@check_allocs +``` diff --git a/docs/src/index.md b/docs/src/index.md index 13b268d..1d35aeb 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -6,47 +6,58 @@ AllocCheck operates on _functions_, trying to statically determine wether or not ## Getting started -The main entry point to check allocations is the function [`check_allocs`](@ref), which takes the function to check as the first argument, and a tuple of argument types as the second argument: -```@example README +The primary entry point to check allocations is the macro [`@check_allocs`](@ref) which is used to annotate a function definition that you'd like to enforce allocation checks for: +```@repl README using AllocCheck -mymod(x) = mod(x, 2.5) +using Test # hide +@check_allocs mymod(x) = mod(x, 2.5) -check_allocs(mymod, (Float64,)) +mymod(1.5) # call automatically checked for allocations ``` -This returned an empty array, indicating that the function was proven to not allocate any memory 🎉 +This call happened without error, indicating that the function was proven to not allocate any memory after it starts 🎉 When used on a function that may allocate memory -```@example README -linsolve(a, b) = a \ b +```@repl README +@check_allocs linsolve(a, b) = a \ b -allocs = check_allocs(linsolve, (Matrix{Float64}, Vector{Float64})); -length(allocs) +linsolve(rand(10,10), rand(10)) ``` -we get a non-empty array of allocation instances. Each allocation instance contains some useful information, for example +the function call raises an `AllocCheckFailure`. + +The `errors` field allows us to inspect the individual errors to get some useful information. For example: ```@example README -allocs[1] +try + linsolve(rand(10,10), rand(10)) +catch err + err.allocs[1] +end ``` we see what type of object was allocated, and where in the code the allocation appeared. ### Functions that throw exceptions + Some functions that we do not expect may allocate memory, like `sin`, actually may: ```@example README -length(check_allocs(sin, (Float64,))) +@allocated try sin(Inf) catch end ``` -The reason for this is that `sin` may **throw an error**, and the exception path requires some allocations. We can ignore allocations that only happen when throwing errors by passing `ignore_throw=true`: +The reason for this is that `sin` needs to allocate if it **throws an error**. + +By default, `@check_allocs` ignores all such allocations and assumes that no exceptions are thrown. If you care about detecting these allocations anyway, you can use `ignore_throw=false`: ```@example README -allocs = check_allocs(sin, (Float64,); ignore_throw=true) # ignore allocations that only happen when throwing errors +@check_allocs mysin1(x) = sin(x) +@check_allocs ignore_throw=false mysin2(x) = sin(x) -using Test -@test isempty(allocs) +@test mysin1(1.5) == sin(1.5) +@test_throws AllocCheckFailure mysin2(1.5) ``` ## Limitations - 1. Runtime dispatch - Any runtime dispatch is conservatively assumed to allocate. \ No newline at end of file + Every call into a `@check_allocs` function behaves like a dynamic dispatch. This means that it can trigger compilation dynamically (involving lots of allocation), and even when the function has already been compiled, a small amount of allocation is still expected on function entry. + + For most applications, the solution is to use `@check_allocs` to wrap your top-level entry point or your main application loop, in which case those applications are only incurred once. `@check_allocs` will guarantee that no dynamic compilation or allocation occurs once your function has started running. diff --git a/docs/src/tutorials/optional_debugging_and_logging.md b/docs/src/tutorials/optional_debugging_and_logging.md index 91624dd..e069b34 100644 --- a/docs/src/tutorials/optional_debugging_and_logging.md +++ b/docs/src/tutorials/optional_debugging_and_logging.md @@ -2,7 +2,8 @@ For debugging purposes, it may sometimes be beneficial to include logging statements in a function, for example ```@example DEBUGGING -function myfun(verbose::Bool) +using AllocCheck # hide +@check_allocs function myfun(verbose::Bool) a = 0.0 for i = 1:3 a = a + i @@ -12,9 +13,8 @@ end nothing # hide ``` Here, the printing of some relevant information is only performed if `verbose = true`. While the printing is optional, and not performed if `verbose = false`, [`check_allocs`](@ref) operates on _types rather than values_, i.e., `check_allocs` only knows that the argument is of type `Bool`, not that it may have the value `false`: -```@example DEBUGGING -using AllocCheck -check_allocs(myfun, (Bool,)) |> length +```@repl DEBUGGING +myfun(false) ``` Indeed, this function was determined to potentially allocate memory. @@ -28,7 +28,7 @@ function typed_myfun(::Val{verbose}) where verbose end end -check_allocs(typed_myfun, (Val{false},)) |> length +length(check_allocs(typed_myfun, (Val{false},))) ``` The compiler, and thus also AllocCheck, now knows that the value of `verbose` is `false`, since this is encoded in the _type_ `Val{false}`. The compiler can use this knowledge to figure out that the `@info` statement won't be executed, and thus prove that the function will not allocate memory. @@ -40,9 +40,27 @@ typed_myfun(Val{true}()) ## Advanced: Constant propagation -Sometimes, the compiler is able to use _constant propagation_ to determine what path through a program will be taken based on the _value of constants_. We demonstrate this effect below, where the value `verbose = false` is hard-coded + +Sometimes, code written without this trick will still work just fine with AllocCheck. + +That's because in some limited scenarios, the compiler is able to use _constant propagation_ to determine what path through a program will be taken based on the _value of constants_. + +We demonstrate this effect below, where the value `verbose = false` is hard-coded into the function: ```@example DEBUGGING -my_outer_function() = myfun(false) # Hard coded value false -check_allocs(my_outer_function, ()) |> length +@check_allocs function constant_myfun() + verbose = false + a = 0.0 + for i = 1:3 + a = a + i + verbose && @info "a = $a" + end + return a +end + +constant_myfun() ``` -When looking at `my_outer_function`, the compiler knows that `verbose = false` since this constant is hard coded into the program, and the compiler thus has the same amount of information here as when the value was lifted into the type domain. Constant propagation is considered a performance optimization that the compiler may or may not perform, and it is thus recommended to use the `Val` type to lift values into the type domain to guarantee that the compiler will use this information. \ No newline at end of file + +When looking at `constant_myfun`, the compiler knows that `verbose = false` since this constant is hard coded into the program. Sometimes, the compiler can even propagate constant values all the way into called functions. + +This is useful, but it's not guaranteed to happen in general. The `Val{T}` trick described here ensures that the variable is propagated as a constant everywhere it is required. + diff --git a/src/AllocCheck.jl b/src/AllocCheck.jl index d316d6d..d755448 100644 --- a/src/AllocCheck.jl +++ b/src/AllocCheck.jl @@ -295,7 +295,7 @@ function find_allocs!(mod::LLVM.Module, meta; ignore_throw=true) end """ - check_allocs(func, types; entry_abi=:specfunc, ret_mod=false) + check_allocs(func, types; ignore_throw=true) Compiles the given function and types to LLVM IR and checks for allocations. Returns a vector of `AllocationSite` structs, each containing a `CallInst` and a backtrace. diff --git a/src/macro.jl b/src/macro.jl index 1eb29d2..bf46a31 100644 --- a/src/macro.jl +++ b/src/macro.jl @@ -23,6 +23,39 @@ function extract_keywords(ex0) return kws, arg end +""" + @check_allocs ignore_throw=true (function def) + +Wraps the provided function definition so that all calls to it will be automatically +checked for allocations. + +If the check fails, an `AllocCheckFailure` exception is thrown containing the detailed +failures, including the backtrace for each defect. + +Note: All calls to the wrapped function are effectively a dynamic dispatch, which +means they are type-unstable and may allocate memory at function _entry_. `@check_allocs` +only guarantees the absence of allocations after the function has started running. + +# Example +```jldoctest +julia> @check_allocs multiply(x,y) = x*y +multiply (generic function with 1 method) + +julia> multiply(1.5, 3.5) # no allocations for Float64 +5.25 + +julia> multiply(rand(3,3), rand(3,3)) # matmul needs to allocate the result +ERROR: @check_alloc function contains 1 allocations. + +Stacktrace: + [1] macro expansion + @ ~/repos/AllocCheck/src/macro.jl:134 [inlined] + [2] multiply(x::Matrix{Float64}, y::Matrix{Float64}) + @ Main ./REPL[2]:133 + [3] top-level scope + @ REPL[5]:1 +``` +""" macro check_allocs(ex...) kws, body = extract_keywords(ex) if _is_func_def(body)