Skip to content

Commit 1b6b8c5

Browse files
committed
make @allocated x = f(...) work as before
Also add `donotdelete` to ensure the code actually runs, and `@constprop :none`. Add `@allocated inside` and `@allocations inside` Fixes #58780
1 parent b24014a commit 1b6b8c5

File tree

4 files changed

+107
-27
lines changed

4 files changed

+107
-27
lines changed

NEWS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ New library features
6060
* `sort(keys(::Dict))` and `sort(values(::Dict))` now automatically collect, they previously threw ([#56978]).
6161
* `Base.AbstractOneTo` is added as a supertype of one-based axes, with `Base.OneTo` as its subtype ([#56902]).
6262
* `takestring!(::IOBuffer)` removes the content from the buffer, returning the content as a `String`.
63+
* New forms `@allocated inside f(...)` and `@allocations inside f(...)` to measure allocations inside
64+
a function, excluding arguments and calling context ([#59278]).
6365

6466
Standard library changes
6567
------------------------

base/timing.jl

Lines changed: 99 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -472,41 +472,47 @@ function gc_bytes()
472472
b[]
473473
end
474474

475-
function allocated(f, args::Vararg{Any,N}) where {N}
475+
@constprop :none function allocated(f, args::Vararg{Any,N}) where {N}
476476
b0 = Ref{Int64}(0)
477477
b1 = Ref{Int64}(0)
478478
Base.gc_bytes(b0)
479-
f(args...)
479+
val = f(args...)
480480
Base.gc_bytes(b1)
481+
donotdelete(val)
481482
return b1[] - b0[]
482483
end
483484
only(methods(allocated)).called = 0xff
484485

485-
function allocations(f, args::Vararg{Any,N}) where {N}
486+
@constprop :none function allocations(f, args::Vararg{Any,N}) where {N}
487+
# Note this value is unused, but without it `allocated` and `allocations`
488+
# are sufficiently different that the compiler can remove allocations here
489+
# that it cannot remove there, giving inconsistent numbers.
490+
b1 = Ref{Int64}(0)
486491
stats = Base.gc_num()
487-
f(args...)
492+
val = f(args...)
488493
diff = Base.GC_Diff(Base.gc_num(), stats)
494+
gc_bytes(b1)
495+
donotdelete(val)
489496
return Base.gc_alloc_count(diff)
490497
end
491498
only(methods(allocations)).called = 0xff
492499

493-
function is_simply_call(@nospecialize ex)
494-
Meta.isexpr(ex, :call) || return false
495-
for a in ex.args
496-
a isa QuoteNode && continue
497-
a isa Symbol && continue
498-
isa_ast_node(a) || continue
499-
return false
500-
end
501-
return true
502-
end
503-
504500
"""
505501
@allocated
506502
507503
A macro to evaluate an expression, discarding the resulting value, instead returning the
508504
total number of bytes allocated during evaluation of the expression.
509505
506+
If the expression is a function call, an effort is made to measure only allocations
507+
during the function, excluding any overhead from calling it and not performing
508+
constant propagation with the provided argument values. This mode of use is recommended.
509+
If you want to include those effects, i.e. measuring the call site as well,
510+
use the syntax `@allocated (()->f(1))()`.
511+
512+
For more complex expressions, the code is simply run in place and therefore may see
513+
allocations due to the surrounding context. For example it is possible for
514+
`@allocated f(1)` and `@allocated x = f(1)` to give different results.
515+
510516
See also [`@allocations`](@ref), [`@time`](@ref), [`@timev`](@ref), [`@timed`](@ref),
511517
and [`@elapsed`](@ref).
512518
@@ -516,11 +522,46 @@ julia> @allocated rand(10^6)
516522
```
517523
"""
518524
macro allocated(ex)
519-
if !is_simply_call(ex)
520-
ex = :((() -> $ex)())
525+
quote
526+
Experimental.@force_compile
527+
local b0 = Ref{Int64}(0)
528+
local b1 = Ref{Int64}(0)
529+
gc_bytes(b0)
530+
local val = $(esc(ex))
531+
gc_bytes(b1)
532+
donotdelete(val)
533+
b1[] - b0[]
534+
end
535+
end
536+
537+
"""
538+
@allocated inside f(...)
539+
540+
Return the number of bytes allocated during the execution of a given function,
541+
excluding any allocations in the argument expressions or due to the call itself.
542+
543+
```julia-repl
544+
julia> @allocated identity([])
545+
32
546+
547+
julia> @allocated inside identity([])
548+
0
549+
```
550+
551+
!!! compat "Julia 1.12"
552+
This macro was added in Julia 1.12.
553+
"""
554+
macro allocated(how, ex)
555+
if how === :inside
556+
if isexpr(ex, :call)
557+
pushfirst!(ex.args, GlobalRef(Base, :allocated))
558+
return esc(ex)
559+
else
560+
error("`@allocated inside` requires a call expression")
561+
end
562+
else
563+
error("Unrecognized symbol argument to `@allocated`; must be `inside`")
521564
end
522-
pushfirst!(ex.args, GlobalRef(Base, :allocated))
523-
return esc(ex)
524565
end
525566

526567
"""
@@ -541,11 +582,46 @@ julia> @allocations rand(10^6)
541582
This macro was added in Julia 1.9.
542583
"""
543584
macro allocations(ex)
544-
if !is_simply_call(ex)
545-
ex = :((() -> $ex)())
585+
quote
586+
Experimental.@force_compile
587+
local b1 = Ref{Int64}(0) # See note above in `allocations`
588+
local stats = Base.gc_num()
589+
local val = $(esc(ex))
590+
local diff = Base.GC_Diff(Base.gc_num(), stats)
591+
gc_bytes(b1)
592+
donotdelete(val)
593+
Base.gc_alloc_count(diff)
594+
end
595+
end
596+
597+
"""
598+
@allocations inside f(...)
599+
600+
Return the number of allocations during the execution of a given function,
601+
excluding any allocations in the argument expressions or due to the call itself.
602+
603+
```julia-repl
604+
julia> @allocations identity([])
605+
1
606+
607+
julia> @allocations inside identity([])
608+
0
609+
```
610+
611+
!!! compat "Julia 1.12"
612+
This macro was added in Julia 1.12.
613+
"""
614+
macro allocations(how, ex)
615+
if how === :inside
616+
if isexpr(ex, :call)
617+
pushfirst!(ex.args, GlobalRef(Base, :allocations))
618+
return esc(ex)
619+
else
620+
error("`@allocations inside` requires a call expression")
621+
end
622+
else
623+
error("Unrecognized symbol argument to `@allocations`; must be `inside`")
546624
end
547-
pushfirst!(ex.args, GlobalRef(Base, :allocations))
548-
return esc(ex)
549625
end
550626

551627

test/boundscheck_exec.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -350,7 +350,7 @@ if bc_opt == bc_default
350350
m1 === m2
351351
end
352352
no_alias_prove(1)
353-
@test (@allocated no_alias_prove(5)) == 0
353+
@test (@allocated (()->no_alias_prove(5))()) == 0
354354
end
355355

356356
@testset "automatic boundscheck elision for iteration on some important types" begin

test/misc.jl

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1544,11 +1544,13 @@ run(`$(Base.julia_cmd()) -e 'isempty(x) = true'`)
15441544
@test Base.jit_total_bytes() >= 0
15451545

15461546
# sanity check `@allocations` returns what we expect in some very simple cases.
1547-
# These are inside functions because `@allocations` uses `Experimental.@force_compile`
1548-
# so can be affected by other code in the same scope.
15491547
@test (() -> @allocations "a")() == 0
1550-
@test (() -> @allocations "a" * "b")() == 0 # constant propagation
15511548
@test (() -> @allocations "a" * Base.inferencebarrier("b"))() == 1
1549+
# test that you can grab the value from @allocated
1550+
@allocated _x = 1+2
1551+
@test _x === 3
1552+
@test (@allocated inside identity([])) == 0
1553+
@test (@allocations inside identity([])) == 0
15521554

15531555
_lock_conflicts, _nthreads = eval(Meta.parse(read(`$(Base.julia_cmd()) -tauto -E '
15541556
_lock_conflicts = @lock_conflicts begin

0 commit comments

Comments
 (0)