Skip to content

Commit 3fe18cd

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 3fe18cd

File tree

4 files changed

+125
-17
lines changed

4 files changed

+125
-17
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: 115 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -472,20 +472,27 @@ 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 = @noinline 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 = @noinline 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
@@ -501,12 +508,56 @@ function is_simply_call(@nospecialize ex)
501508
return true
502509
end
503510

511+
function _gen_allocation_measurer(ex, fname::Symbol)
512+
if isexpr(ex, :call)
513+
if !is_simply_call(ex)
514+
ex = :((() -> $ex)())
515+
end
516+
pushfirst!(ex.args, GlobalRef(Base, fname))
517+
return esc(ex)
518+
elseif fname === :allocated
519+
# v1.11-compatible implementation
520+
return quote
521+
Experimental.@force_compile
522+
local b0 = Ref{Int64}(0)
523+
local b1 = Ref{Int64}(0)
524+
gc_bytes(b0)
525+
local val = $(esc(ex))
526+
gc_bytes(b1)
527+
donotdelete(val)
528+
b1[] - b0[]
529+
end
530+
else
531+
@assert fname === :allocations
532+
return quote
533+
Experimental.@force_compile
534+
local b1 = Ref{Int64}(0) # See note above in `allocations`
535+
local stats = Base.gc_num()
536+
local val = $(esc(ex))
537+
local diff = Base.GC_Diff(Base.gc_num(), stats)
538+
gc_bytes(b1)
539+
donotdelete(val)
540+
Base.gc_alloc_count(diff)
541+
end
542+
end
543+
end
544+
504545
"""
505546
@allocated
506547
507548
A macro to evaluate an expression, discarding the resulting value, instead returning the
508549
total number of bytes allocated during evaluation of the expression.
509550
551+
If the expression is a function call, an effort is made to measure only allocations
552+
during the function, excluding any overhead from calling it and not performing
553+
constant propagation with the provided argument values. This mode of use is recommended.
554+
If you want to include those effects, i.e. measuring the call site as well,
555+
use the syntax `@allocated (()->f(1))()`.
556+
557+
For more complex expressions, the code is simply run in place and therefore may see
558+
allocations due to the surrounding context. For example it is possible for
559+
`@allocated f(1)` and `@allocated x = f(1)` to give different results.
560+
510561
See also [`@allocations`](@ref), [`@time`](@ref), [`@timev`](@ref), [`@timed`](@ref),
511562
and [`@elapsed`](@ref).
512563
@@ -516,11 +567,37 @@ julia> @allocated rand(10^6)
516567
```
517568
"""
518569
macro allocated(ex)
519-
if !is_simply_call(ex)
520-
ex = :((() -> $ex)())
570+
_gen_allocation_measurer(ex, :allocated)
571+
end
572+
573+
"""
574+
@allocated inside f(...)
575+
576+
Return the number of bytes allocated during the execution of a given function,
577+
excluding any allocations in the argument expressions or due to the call itself.
578+
579+
```julia-repl
580+
julia> @allocated identity([])
581+
32
582+
583+
julia> @allocated inside identity([])
584+
0
585+
```
586+
587+
!!! compat "Julia 1.12"
588+
This macro was added in Julia 1.12.
589+
"""
590+
macro allocated(how, ex)
591+
if how === :inside
592+
if isexpr(ex, :call)
593+
pushfirst!(ex.args, GlobalRef(Base, :allocated))
594+
return esc(ex)
595+
else
596+
error("`@allocated inside` requires a call expression")
597+
end
598+
else
599+
error("Unrecognized symbol argument to `@allocated`; must be `inside`")
521600
end
522-
pushfirst!(ex.args, GlobalRef(Base, :allocated))
523-
return esc(ex)
524601
end
525602

526603
"""
@@ -541,11 +618,37 @@ julia> @allocations rand(10^6)
541618
This macro was added in Julia 1.9.
542619
"""
543620
macro allocations(ex)
544-
if !is_simply_call(ex)
545-
ex = :((() -> $ex)())
621+
_gen_allocation_measurer(ex, :allocations)
622+
end
623+
624+
"""
625+
@allocations inside f(...)
626+
627+
Return the number of allocations during the execution of a given function,
628+
excluding any allocations in the argument expressions or due to the call itself.
629+
630+
```julia-repl
631+
julia> @allocations identity([])
632+
1
633+
634+
julia> @allocations inside identity([])
635+
0
636+
```
637+
638+
!!! compat "Julia 1.12"
639+
This macro was added in Julia 1.12.
640+
"""
641+
macro allocations(how, ex)
642+
if how === :inside
643+
if isexpr(ex, :call)
644+
pushfirst!(ex.args, GlobalRef(Base, :allocations))
645+
return esc(ex)
646+
else
647+
error("`@allocations inside` requires a call expression")
648+
end
649+
else
650+
error("Unrecognized symbol argument to `@allocations`; must be `inside`")
546651
end
547-
pushfirst!(ex.args, GlobalRef(Base, :allocations))
548-
return esc(ex)
549652
end
550653

551654

test/boundscheck_exec.jl

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -349,8 +349,9 @@ if bc_opt == bc_default
349349
m2 = Memory{Int}(undef,n)
350350
m1 === m2
351351
end
352-
no_alias_prove(1)
353-
@test (@allocated no_alias_prove(5)) == 0
352+
no_alias_prove5() = no_alias_prove(5)
353+
no_alias_prove5()
354+
@test (@allocated no_alias_prove5()) == 0
354355
end
355356

356357
@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)