Repository navigation
codegen: Propagate ipo_purity_bits to LLVM function attributes - #61394
Conversation
|
I'm pretty sure @gbaraldi had a previous version of this PR. The really hard part is iirc, From LLVM's point of view, some of the things that we consider effect free are effect-full. |
|
Ah ok! I didn't find anything that looked similar. Note that macOS tests passed. |
|
I think it's #50188 |
How did you compile the Rust code? By default rustc doesn't do an optimised build, which makes performance comparisons very misleading. |
|
The memory read ones are potentially illegal because they ignore the safe point |
@giordano it was with |
The issue is that we can't reorder writes across function calls due to safepoints. if LLVM moves a store to a pointer past a function call we potentially miss a rooting event. |
|
I think it might be a good idea to experiment with LLVM's |
|
Ok. Just to summarize
So the current PR, even if it disabled those two, doesn't seem to have much merit. |
f63e243 to
6f6b447
Compare
|
@nanosoldier |
|
Is this enough to LICM calls. Like |
|
yeah LLVM can't licm functions that may throw. I really think |
|
Your benchmark job has completed - possible performance regressions were detected. A full report can be found here. |
|
I guess this will subsume #51120 as well |
|
We probably need a few llvm pass tests to make sure the optimisations don't disappear with an llvm bump? |
|
@gbaraldi I just force pushed because my email was wrong and picking up another author. also drop an unrelated whitespace change in Test.jl |
|
Disabling emission of
Note: I explored adding a |
|
@gbaraldi confirmed with the LLVM folk that they never infer speculatable, so I disabled it |
|
@nanosoldier |
|
The benchmark job you requested has completed - possible performance regressions were detected. 4818 benchmarks were executed, 120 showed regressions, and 127 showed improvements. |
|
Using the benchmark plot summary code from JuliaCI/Nanosoldier.jl#227 on #61394 (comment)
|
|
I couldn't reproduce those biggest regressions locally on mac. @gbaraldi good to merge? |
While it doesn't do LICM on julia> code_llvm((Float64,); debuginfo=:none) do x sin(x) * sin(x) + cos(x) * cos(x) end; Function Signature: var"#92"(Float64)
define double @"julia_#92_0"(double %"x::Float64") local_unnamed_addr #0 {
top:
%0 = call double @j_sin_0(double %"x::Float64") #10
%1 = fmul double %0, %0
%2 = call double @j_cos_0(double %"x::Float64") #10
%3 = fmul double %2, %2
%4 = fadd double %1, %3
ret double %4
}This is going to make code involving rotation matrices a lot simpler to write: you can repeat those julia> code_llvm((Float64,); debuginfo=:none) do x sin(x) * sin(x) + cos(x) * cos(x) end; Function Signature: var"#50"(Float64)
define double @"julia_#50_0"(double %"x::Float64") local_unnamed_addr #0 {
top:
%0 = call double @j_sin_0(double %"x::Float64")
%1 = call double @j_sin_0(double %"x::Float64")
%2 = fmul double %0, %1
%3 = call double @j_cos_0(double %"x::Float64")
%4 = call double @j_cos_0(double %"x::Float64")
%5 = fmul double %3, %4
%6 = fadd double %2, %5
ret double %6
}Also fastmath code can be better optimised with this PR: julia> code_llvm((Float64,); debuginfo=:none) do x; @fastmath sin(x) - sin(x) end; Function Signature: var"#101"(Float64)
define double @"julia_#101_0"(double %"x::Float64") local_unnamed_addr #0 {
top:
%0 = call double @j_sin_0(double %"x::Float64") #9
ret double 0.000000e+00
}When no-NaNs can be assumed, the compiler can simplify the code more effectively (just sad it doesn't remove the dead code, not even with julia> code_llvm((Float64,); debuginfo=:none) do x; @fastmath sin(x) - sin(x) end; Function Signature: var"#47"(Float64)
define double @"julia_#47_0"(double %"x::Float64") local_unnamed_addr #0 {
top:
%0 = call double @j_sin_0(double %"x::Float64")
%1 = call double @j_sin_0(double %"x::Float64")
%2 = fsub fast double %0, %1
ret double %2
} |
|
Outside of that dangling comment it seems good |
This lack of specialization is problematic, because the result of the map is used in the function body. Without specialization, this causes excessive allocations. This was discovered by @aplavin with this benchmark: ```julia using BenchmarkTools a = (x1=[1.0], x2=[2.0], x3=[3.0], x4=[4.0], x5=[5.0], x6=[6.0], x7=[7.0], x8=[8.0], x9=[9.0], x10=[10.0], x11=[11.0]) b = map(copy, a) function bench(a, b, N) s = 0 for _ in 1:N; s += length(map(copyto!, a, b).x1); end; s end bench(a, b, 1) @showtime bench(a, b, 1_000_000) ``` Results: * `map` on master: `bench(a, b, 1000000): 0.495330 seconds (4.00 M allocations: 366.211 MiB, 34.93% gc time)` * `map` on PR: `bench(a, b, 1000000): 0.050094 seconds (1 allocation: 16 bytes)`
…chmarking (JuliaLang#61438) Adds `Base.blackbox(x)` and the corresponding `:blackbox` setting for `compilerbarrier`, motivated by the LICM-hoisting issue seen in JuliaLang#61394. `Base.blackbox(x)` returns `x` but treats the output as opaque to the optimizer: it cannot be constant-folded, CSE'd, or treated as loop-invariant. This is Julia's equivalent of Google Benchmark's `DoNotOptimize`. https://google.github.io/benchmark/user_guide.html#preventing-optimization `blackbox` is implemented as `compilerbarrier(:blackbox, x)`, keeping all compiler-barrier functionality consolidated in one builtin rather than introducing a new one. ## Implementation **Julia-level (inference):** `:blackbox` has `consistent=ALWAYS_FALSE` and returns `widenconst(val)`, preventing constant propagation and CSE at the abstract interpretation level. **Codegen level**, three cases based on the value's LLVM type: - **Non-pointer scalars** (integers, floats — anything that fits in a register): emits `asm "" "=r,0"` directly, tying the output register to the input. - **Boxed/GC-tracked pointers**: cannot use register-tied asm on GC-tracked address spaces. Instead, emits a `julia.blackbox` intrinsic call that is lowered to `asm "" "=r,0"` on the raw untracked pointer after GC frame expansion. - **Unboxed aggregates / unboxed pointers** (e.g., structs, `Ptr{T}`): uses `asm sideeffect "" "~{memory}"` to clobber memory, making the value appear non-invariant without needing a register constraint. Ghost types (`Nothing`, etc.) pass through unchanged with no code emitted. --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Valentin Churavy <v.churavy@gmail.com> Co-authored-by: Keno Fischer <1291671+Keno@users.noreply.github.com>
…iaLang#61394) Translate Julia's inferred effects (consistent, effect_free, nothrow, terminates, notaskstate) into LLVM function attributes so that middle-end passes like GVN, LICM, and DSE can exploit them. The key design insight is that GC interactions don't need to be visible before GC lowering. Call-site declarations get optimistic memory attributes (e.g. memory(argmem: read)) that enable pre-GC optimizations, then LateLowerGCFrame widens them to memory(readwrite) before safepoint analysis so post-GC passes see correct semantics. Attributes added: - nounwind: for nothrow functions (with uwtable(async) on definitions to preserve .eh_frame for stack scanning) - mustprogress: for terminating functions - willreturn: for nothrow+terminating functions - memory(argmem: read): for consistent+effect_free functions with no user-facing pointer arguments (call-site declarations only) - readnone on gcstack param: for notaskstate functions, so LICM can hoist pure calls past heap stores - "julia.safepoint" marker: on all call-site declarations, used by LateLowerGCFrame to identify and widen optimistic attrs LateLowerGCFrame strips all optimistic attributes (memory effects, readnone on gcstack) from both call instructions and function declarations before safepoint analysis runs. Previously explored in JuliaLang#47844 Developed with Claude --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Gabriel Baraldi <28694980+gbaraldi@users.noreply.github.com>
…chmarking (JuliaLang#61438) Adds `Base.blackbox(x)` and the corresponding `:blackbox` setting for `compilerbarrier`, motivated by the LICM-hoisting issue seen in JuliaLang#61394. `Base.blackbox(x)` returns `x` but treats the output as opaque to the optimizer: it cannot be constant-folded, CSE'd, or treated as loop-invariant. This is Julia's equivalent of Google Benchmark's `DoNotOptimize`. https://google.github.io/benchmark/user_guide.html#preventing-optimization `blackbox` is implemented as `compilerbarrier(:blackbox, x)`, keeping all compiler-barrier functionality consolidated in one builtin rather than introducing a new one. ## Implementation **Julia-level (inference):** `:blackbox` has `consistent=ALWAYS_FALSE` and returns `widenconst(val)`, preventing constant propagation and CSE at the abstract interpretation level. **Codegen level**, three cases based on the value's LLVM type: - **Non-pointer scalars** (integers, floats — anything that fits in a register): emits `asm "" "=r,0"` directly, tying the output register to the input. - **Boxed/GC-tracked pointers**: cannot use register-tied asm on GC-tracked address spaces. Instead, emits a `julia.blackbox` intrinsic call that is lowered to `asm "" "=r,0"` on the raw untracked pointer after GC frame expansion. - **Unboxed aggregates / unboxed pointers** (e.g., structs, `Ptr{T}`): uses `asm sideeffect "" "~{memory}"` to clobber memory, making the value appear non-invariant without needing a register constraint. Ghost types (`Nothing`, etc.) pass through unchanged with no code emitted. --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Valentin Churavy <v.churavy@gmail.com> Co-authored-by: Keno Fischer <1291671+Keno@users.noreply.github.com>
…iaLang#61394) Translate Julia's inferred effects (consistent, effect_free, nothrow, terminates, notaskstate) into LLVM function attributes so that middle-end passes like GVN, LICM, and DSE can exploit them. The key design insight is that GC interactions don't need to be visible before GC lowering. Call-site declarations get optimistic memory attributes (e.g. memory(argmem: read)) that enable pre-GC optimizations, then LateLowerGCFrame widens them to memory(readwrite) before safepoint analysis so post-GC passes see correct semantics. Attributes added: - nounwind: for nothrow functions (with uwtable(async) on definitions to preserve .eh_frame for stack scanning) - mustprogress: for terminating functions - willreturn: for nothrow+terminating functions - memory(argmem: read): for consistent+effect_free functions with no user-facing pointer arguments (call-site declarations only) - readnone on gcstack param: for notaskstate functions, so LICM can hoist pure calls past heap stores - "julia.safepoint" marker: on all call-site declarations, used by LateLowerGCFrame to identify and widen optimistic attrs LateLowerGCFrame strips all optimistic attributes (memory effects, readnone on gcstack) from both call instructions and function declarations before safepoint analysis runs. Previously explored in JuliaLang#47844 Developed with Claude --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Gabriel Baraldi <28694980+gbaraldi@users.noreply.github.com>
Calls tagged "julia.safepoint" are Julia calls whose memory effects codegen narrowed from inferred effects. They can still reach a GC safepoint, so LocalScan must treat them as safepoints regardless of what the memory effects claim, and the effects must be stripped before any post-lowering pass could use them against the now-explicit GC frame stores. This is the rooting half of re-landing JuliaLang#61394 (reverted in JuliaLang#62697, see JuliaLang#62613): rooting no longer depends on pass ordering having stripped the attributes first. Assisted-by: Claude Code (Fable 5.1)
Calls tagged "julia.safepoint" are Julia calls whose memory effects codegen narrowed from inferred effects. They can still reach a GC safepoint, so LocalScan must treat them as safepoints regardless of what the memory effects claim, and the effects must be stripped before any post-lowering pass could use them against the now-explicit GC frame stores. This is the rooting half of re-landing #61394 (reverted in #62697, see #62613): rooting no longer depends on pass ordering having stripped the attributes first. Assisted-by: Claude Code (Fable 5.1)




Updated after @gbaraldi (who previously explored this in #47844) helped out:
Translate Julia's inferred effects (consistent, effect_free, nothrow, terminates, notaskstate) into LLVM function attributes so that middle-end passes like GVN, LICM, and DSE can exploit them.
The key design insight is that GC interactions don't need to be visible before GC lowering. Call-site declarations get optimistic memory attributes (e.g. memory(argmem: read)) that enable pre-GC optimizations, then LateLowerGCFrame widens them to memory(readwrite) before safepoint analysis so post-GC passes see correct semantics.
Attributes added:
to preserve .eh_frame for stack scanning)
user-facing pointer arguments (call-site declarations only)
speculatable: for the above when also nothrow+terminatingNot usedhoist pure calls past heap stores
LateLowerGCFrame to identify and widen optimistic attrs
LateLowerGCFrame strips all optimistic attributes (memory effects,
readnone on gcstack) from both call instructions and function
declarations before safepoint analysis runs.
Developed with Claude