Skip to content

Commit c8fd1e4

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 c8fd1e4

File tree

4 files changed

+121
-17
lines changed

4 files changed

+121
-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: 111 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -472,20 +472,25 @@ 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+
@noinline f(args...)
480480
Base.gc_bytes(b1)
481481
return b1[] - b0[]
482482
end
483483
only(methods(allocated)).called = 0xff
484484

485-
function allocations(f, args::Vararg{Any,N}) where {N}
485+
@constprop :none function allocations(f, args::Vararg{Any,N}) where {N}
486+
# Note this value is unused, but without it `allocated` and `allocations`
487+
# are sufficiently different that the compiler can remove allocations here
488+
# that it cannot remove there, giving inconsistent numbers.
489+
b1 = Ref{Int64}(0)
486490
stats = Base.gc_num()
487-
f(args...)
491+
@noinline f(args...)
488492
diff = Base.GC_Diff(Base.gc_num(), stats)
493+
gc_bytes(b1)
489494
return Base.gc_alloc_count(diff)
490495
end
491496
only(methods(allocations)).called = 0xff
@@ -501,12 +506,54 @@ function is_simply_call(@nospecialize ex)
501506
return true
502507
end
503508

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

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

551650

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)