Skip to content

Commit 23633c2

Browse files
committed
make @allocated x = f(...) work as before
Also add `@constprop :none` and `@noinline` to prevent the measuring code from interfering with the subject code. Fixes #58780
1 parent 8581930 commit 23633c2

File tree

4 files changed

+94
-24
lines changed

4 files changed

+94
-24
lines changed

base/reduce.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ Like [`mapreduce`](@ref), but with guaranteed right associativity, as in [`foldr
218218
provided, the keyword argument `init` will be used exactly once. In general, it will be
219219
necessary to provide `init` to work with empty collections.
220220
"""
221-
mapfoldr(f, op, itr; init=_InitialValue()) = mapfoldr_impl(f, op, init, itr)
221+
mapfoldr(f::F, op::F2, itr; init=_InitialValue()) where {F,F2} = mapfoldr_impl(f, op, init, itr)
222222

223223

224224
"""
@@ -237,7 +237,7 @@ julia> foldr(=>, 1:4; init=0)
237237
1 => (2 => (3 => (4 => 0)))
238238
```
239239
"""
240-
foldr(op, itr; kw...) = mapfoldr(identity, op, itr; kw...)
240+
foldr(op::F, itr; kw...) where {F} = mapfoldr(identity, op, itr; kw...)
241241

242242
## reduce & mapreduce
243243

base/timing.jl

Lines changed: 61 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -472,41 +472,93 @@ 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}
486486
stats = Base.gc_num()
487-
f(args...)
487+
@noinline f(args...)
488488
diff = Base.GC_Diff(Base.gc_num(), stats)
489489
return Base.gc_alloc_count(diff)
490490
end
491491
only(methods(allocations)).called = 0xff
492492

493493
function is_simply_call(@nospecialize ex)
494+
is_simple_atom(a) = a isa QuoteNode || a isa Symbol || !isa_ast_node(a)
494495
Meta.isexpr(ex, :call) || return false
495496
for a in ex.args
496-
a isa QuoteNode && continue
497-
a isa Symbol && continue
498-
isa_ast_node(a) || continue
497+
is_simple_atom(a) && continue
498+
Meta.isexpr(a, :..., 1) && is_simple_atom(a.args[1]) && continue
499499
return false
500500
end
501501
return true
502502
end
503503

504+
function _gen_allocation_measurer(ex, fname::Symbol)
505+
if isexpr(ex, :call)
506+
if !is_simply_call(ex)
507+
ex = :((() -> $ex)())
508+
end
509+
pushfirst!(ex.args, GlobalRef(Base, fname))
510+
return quote
511+
Experimental.@force_compile
512+
$(esc(ex))
513+
end
514+
elseif fname === :allocated
515+
# v1.11-compatible implementation
516+
return quote
517+
Experimental.@force_compile
518+
local b0 = Ref{Int64}(0)
519+
local b1 = Ref{Int64}(0)
520+
gc_bytes(b0)
521+
$(esc(ex))
522+
gc_bytes(b1)
523+
b1[] - b0[]
524+
end
525+
else
526+
@assert fname === :allocations
527+
return quote
528+
Experimental.@force_compile
529+
# Note this value is unused, but without it `allocated` and `allocations`
530+
# are sufficiently different that the compiler can remove allocations here
531+
# that it cannot remove there, giving inconsistent numbers.
532+
local b1 = Ref{Int64}(0)
533+
local stats = Base.gc_num()
534+
$(esc(ex))
535+
local diff = Base.GC_Diff(Base.gc_num(), stats)
536+
gc_bytes(b1)
537+
Base.gc_alloc_count(diff)
538+
end
539+
end
540+
end
541+
504542
"""
505543
@allocated
506544
507545
A macro to evaluate an expression, discarding the resulting value, instead returning the
508546
total number of bytes allocated during evaluation of the expression.
509547
548+
If the expression is a function call, an effort is made to measure only allocations from
549+
the argument expressions and during the function, excluding any overhead from calling it
550+
and not performing constant propagation with the provided argument values. If you want to
551+
include those effects, i.e. measuring the call site as well, use the syntax
552+
`@allocated (()->f(1))()`.
553+
554+
It is recommended to measure function calls with only simple argument expressions, e.g.
555+
`x = []; @allocated f(x)` instead of `@allocated f([])` to clarify that only `f` is
556+
being measured.
557+
558+
For more complex expressions, the code is simply run in place and therefore may see
559+
allocations due to the surrounding context. For example it is possible for
560+
`@allocated f(1)` and `@allocated x = f(1)` to give different results.
561+
510562
See also [`@allocations`](@ref), [`@time`](@ref), [`@timev`](@ref), [`@timed`](@ref),
511563
and [`@elapsed`](@ref).
512564
@@ -516,11 +568,7 @@ julia> @allocated rand(10^6)
516568
```
517569
"""
518570
macro allocated(ex)
519-
if !is_simply_call(ex)
520-
ex = :((() -> $ex)())
521-
end
522-
pushfirst!(ex.args, GlobalRef(Base, :allocated))
523-
return esc(ex)
571+
_gen_allocation_measurer(ex, :allocated)
524572
end
525573

526574
"""
@@ -541,11 +589,7 @@ julia> @allocations rand(10^6)
541589
This macro was added in Julia 1.9.
542590
"""
543591
macro allocations(ex)
544-
if !is_simply_call(ex)
545-
ex = :((() -> $ex)())
546-
end
547-
pushfirst!(ex.args, GlobalRef(Base, :allocations))
548-
return esc(ex)
592+
_gen_allocation_measurer(ex, :allocations)
549593
end
550594

551595

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: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1540,15 +1540,40 @@ end
15401540
# issue #41656
15411541
run(`$(Base.julia_cmd()) -e 'isempty(x) = true'`)
15421542

1543+
function treshape59278(X::AbstractArray, n, m)
1544+
Y = reshape(X, n, m)
1545+
Y .= 1.0
1546+
return X
1547+
end
1548+
1549+
# a function that allocates iff no constprop
1550+
@inline maybealloc59278(n, _) = ntuple(i->rand(), n)
1551+
15431552
@testset "Base/timing.jl" begin
15441553
@test Base.jit_total_bytes() >= 0
15451554

15461555
# 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.
15491556
@test (() -> @allocations "a")() == 0
1550-
@test (() -> @allocations "a" * "b")() == 0 # constant propagation
1557+
"a" * Base.inferencebarrier("b")
15511558
@test (() -> @allocations "a" * Base.inferencebarrier("b"))() == 1
1559+
# test that you can grab the value from @allocated
1560+
@allocated _x = 1+2
1561+
@test _x === 3
1562+
1563+
n, m = 10, 20
1564+
X = rand(n, m)
1565+
treshape59278(X, n, m)
1566+
# test that @allocated and @allocations are consistent about whether anything was
1567+
# allocated in a case where the compiler can sometimes remove an allocation
1568+
# https://github.com/JuliaLang/julia/issues/58634#issuecomment-2940840651
1569+
@test ((@allocated treshape59278(X, n, m))==0) == ((@allocations treshape59278(X, n, m))==0)
1570+
# TODO: would be nice to have but not yet reliable
1571+
#@test ((@allocated begin treshape59278(X, n, m) end)==0) == ((@allocations begin treshape59278(X, n, m) end)==0)
1572+
1573+
# test that all wrapped allocations are counted and constprop is not done
1574+
@test (@allocated @noinline maybealloc59278(10, [])) > (@allocated maybealloc59278(10, 0)) > 0
1575+
# but if you wrap it in another function it can be constprop'd
1576+
@test (@allocated (()->maybealloc59278(10, []))()) == 0
15521577

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

0 commit comments

Comments
 (0)