Skip to content

Commit 377ad2d

Browse files
committed
RFC: A path forward on --check-bounds
In 1.9, `--check-bounds=no` has started causing significant performance regressions (e.g. #50110). This is because we switched a number of functions that used to be `@pure` to new effects-based infrastructure, which very closely tracks the the legality conditions for concrete evaluation. Unfortunately, disabling bounds checking completely invalidates all prior legality analysis, so the only realistic path we have is to completely disable it. In general, we are learning that these kinds of global make-things-faster-but-unsafe flags are highly problematic for a language for several reasons: - Code is written with the assumption of a particular mode being chosen, so it is in general not possible or unsafe to compose libraries (which in a language like julia is a huge problem). - Unsafe semantics are often harder for the compiler to reason about, causing unexpected performance issues (although the 1.9 --check-bounds=no issues are worse than just disabling concrete eval for things that use bounds checking) In general, I'd like to remove the `--check-bounds=` option entirely (#48245), but that proposal has encountered major opposition. This PR implements an alternative proposal: We introduce a new function `Core.should_check_bounds(boundscheck::Bool) = boundscheck`. This function is passed the result of `Expr(:boundscheck)` (which is now purely determined by the inliner based on `@inbounds`, without regard for the command line flag). In this proposal, what the command line flag does is simply redefine this function to either `true` or `false` (unconditionally) depending on the value of the flag. Of course, this causes massive amounts of recompilation, but I think this can be addressed by adding logic to loading that loads a pkgimage with appropriate definitions to cure the invalidations. The special logic we have now now to take account of the --check-bounds flag in .ji selection, would be replaced by automatically injecting the special pkgimage as a dependency to every loaded image. This part isn't implemented in this PR, but I think it's reasonable to do. I think with that, the `--check-bounds` flag remains functional, while having much more well defined behavior, as it relies on the standard world age mechanisms. A major benefit of this approach is that it can be scoped appropriately using overlay tables. For exmaple: ``` julia> using CassetteOverlay julia> @MethodTable AssumeInboundsTable; julia> @overlay AssumeInboundsTable Core.should_check_bounds(b::Bool) = false; julia> assume_inbounds = @overlaypass AssumeInboundsTable julia> assume_inbounds(f, args...) # f(args...) with bounds checking disabled dynamically ``` Similar logic applies to GPUCompiler, which already supports overlay tables.
1 parent 0da46e2 commit 377ad2d

File tree

11 files changed

+28
-53
lines changed

11 files changed

+28
-53
lines changed

base/array.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1017,9 +1017,9 @@ Dict{String, Int64} with 2 entries:
10171017
function setindex! end
10181018

10191019
@eval setindex!(A::Array{T}, x, i1::Int) where {T} =
1020-
arrayset($(Expr(:boundscheck)), A, x isa T ? x : convert(T,x)::T, i1)
1020+
arrayset(Core.should_check_bounds($(Expr(:boundscheck))), A, x isa T ? x : convert(T,x)::T, i1)
10211021
@eval setindex!(A::Array{T}, x, i1::Int, i2::Int, I::Int...) where {T} =
1022-
(@inline; arrayset($(Expr(:boundscheck)), A, x isa T ? x : convert(T,x)::T, i1, i2, I...))
1022+
(@inline; arrayset(Core.should_check_bounds($(Expr(:boundscheck))), A, x isa T ? x : convert(T,x)::T, i1, i2, I...))
10231023

10241024
__inbounds_setindex!(A::Array{T}, x, i1::Int) where {T} =
10251025
arrayset(false, A, convert(T,x)::T, i1)

base/boot.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -852,4 +852,6 @@ function _hasmethod(@nospecialize(tt)) # this function has a special tfunc
852852
return Intrinsics.not_int(ccall(:jl_gf_invoke_lookup, Any, (Any, Any, UInt), tt, nothing, world) === nothing)
853853
end
854854

855+
should_check_bounds(boundscheck::Bool) = boundscheck
856+
855857
ccall(:jl_set_istopmod, Cvoid, (Any, Bool), Core, true)

base/client.jl

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,15 @@ function exec_options(opts)
272272
invokelatest(Main.Distributed.process_opts, opts)
273273
end
274274

275+
# Maybe redefine bounds checking if requested
276+
if JLOptions().check_bounds != 0
277+
if JLOptions().check_bounds == 1
278+
Core.eval(Main, :(Core.should_check_bounds(boundscheck::Bool) = true))
279+
else
280+
Core.eval(Main, :(Core.should_check_bounds(boundscheck::Bool) = false))
281+
end
282+
end
283+
275284
interactiveinput = (repl || is_interactive::Bool) && isa(stdin, TTY)
276285
is_interactive::Bool |= interactiveinput
277286

base/compiler/abstractinterpretation.jl

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -835,13 +835,6 @@ end
835835
function concrete_eval_eligible(interp::AbstractInterpreter,
836836
@nospecialize(f), result::MethodCallResult, arginfo::ArgInfo, sv::AbsIntState)
837837
(;effects) = result
838-
if inbounds_option() === :off
839-
if !is_nothrow(effects)
840-
# Disable concrete evaluation in `--check-bounds=no` mode,
841-
# unless it is known to not throw.
842-
return :none
843-
end
844-
end
845838
if !effects.noinbounds && stmt_taints_inbounds_consistency(sv)
846839
# If the current statement is @inbounds or we propagate inbounds, the call's consistency
847840
# is tainted and not consteval eligible.

base/compiler/ssair/inlining.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -664,8 +664,8 @@ function batch_inline!(ir::IRCode, todo::Vector{Pair{Int,Any}}, propagate_inboun
664664
end
665665
finish_cfg_inline!(state)
666666

667-
boundscheck = inbounds_option()
668-
if boundscheck === :default && propagate_inbounds
667+
boundscheck = :default
668+
if propagate_inbounds
669669
boundscheck = :propagate
670670
end
671671

base/compiler/utilities.jl

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -513,9 +513,3 @@ function coverage_enabled(m::Module)
513513
end
514514
return false
515515
end
516-
function inbounds_option()
517-
opt_check_bounds = JLOptions().check_bounds
518-
opt_check_bounds == 0 && return :default
519-
opt_check_bounds == 1 && return :on
520-
return :off
521-
end

base/essentials.jl

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# This file is a part of Julia. License is MIT: https://julialang.org/license
22

3-
import Core: CodeInfo, SimpleVector, donotdelete, compilerbarrier, arrayref
3+
import Core: CodeInfo, SimpleVector, donotdelete, compilerbarrier, arrayref, should_check_bounds
44

55
const Callable = Union{Function,Type}
66

@@ -10,8 +10,8 @@ const Bottom = Union{}
1010
length(a::Array) = arraylen(a)
1111

1212
# This is more complicated than it needs to be in order to get Win64 through bootstrap
13-
eval(:(getindex(A::Array, i1::Int) = arrayref($(Expr(:boundscheck)), A, i1)))
14-
eval(:(getindex(A::Array, i1::Int, i2::Int, I::Int...) = (@inline; arrayref($(Expr(:boundscheck)), A, i1, i2, I...))))
13+
eval(:(getindex(A::Array, i1::Int) = arrayref(should_check_bounds($(Expr(:boundscheck))), A, i1)))
14+
eval(:(getindex(A::Array, i1::Int, i2::Int, I::Int...) = (@inline; arrayref(should_check_bounds($(Expr(:boundscheck))), A, i1, i2, I...))))
1515

1616
==(a::GlobalRef, b::GlobalRef) = a.mod === b.mod && a.name === b.name
1717

@@ -678,7 +678,7 @@ julia> f2()
678678
implementation after you are certain its behavior is correct.
679679
"""
680680
macro boundscheck(blk)
681-
return Expr(:if, Expr(:boundscheck), esc(blk))
681+
return Expr(:if, Expr(:call, GlobalRef(Core, :should_check_bounds), Expr(:boundscheck)), esc(blk))
682682
end
683683

684684
"""
@@ -741,7 +741,7 @@ end
741741

742742
# SimpleVector
743743

744-
@eval getindex(v::SimpleVector, i::Int) = (@_foldable_meta; Core._svec_ref($(Expr(:boundscheck)), v, i))
744+
@eval getindex(v::SimpleVector, i::Int) = (@_foldable_meta; Core._svec_ref(should_check_bounds($(Expr(:boundscheck))), v, i))
745745
function length(v::SimpleVector)
746746
@_total_meta
747747
t = @_gc_preserve_begin v

base/experimental.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,9 @@ Base.IndexStyle(::Type{<:Const}) = IndexLinear()
2929
Base.size(C::Const) = size(C.a)
3030
Base.axes(C::Const) = axes(C.a)
3131
@eval Base.getindex(A::Const, i1::Int) =
32-
(Base.@inline; Core.const_arrayref($(Expr(:boundscheck)), A.a, i1))
32+
(Base.@inline; Core.const_arrayref(Core.should_check_bounds($(Expr(:boundscheck))), A.a, i1))
3333
@eval Base.getindex(A::Const, i1::Int, i2::Int, I::Int...) =
34-
(Base.@inline; Core.const_arrayref($(Expr(:boundscheck)), A.a, i1, i2, I...))
34+
(Base.@inline; Core.const_arrayref(Core.should_check_bounds($(Expr(:boundscheck))), A.a, i1, i2, I...))
3535

3636
"""
3737
@aliasscope expr

base/tuple.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ firstindex(@nospecialize t::Tuple) = 1
2828
lastindex(@nospecialize t::Tuple) = length(t)
2929
size(@nospecialize(t::Tuple), d::Integer) = (d == 1) ? length(t) : throw(ArgumentError("invalid tuple dimension $d"))
3030
axes(@nospecialize t::Tuple) = (OneTo(length(t)),)
31-
@eval getindex(@nospecialize(t::Tuple), i::Int) = getfield(t, i, $(Expr(:boundscheck)))
32-
@eval getindex(@nospecialize(t::Tuple), i::Integer) = getfield(t, convert(Int, i), $(Expr(:boundscheck)))
31+
@eval getindex(@nospecialize(t::Tuple), i::Int) = getfield(t, i, should_check_bounds($(Expr(:boundscheck))))
32+
@eval getindex(@nospecialize(t::Tuple), i::Integer) = getfield(t, convert(Int, i), should_check_bounds($(Expr(:boundscheck))))
3333
__inbounds_getindex(@nospecialize(t::Tuple), i::Int) = getfield(t, i, false)
3434
__inbounds_getindex(@nospecialize(t::Tuple), i::Integer) = getfield(t, convert(Int, i), false)
3535
getindex(t::Tuple, r::AbstractArray{<:Any,1}) = (eltype(t)[t[ri] for ri in r]...,)

src/cgutils.cpp

Lines changed: 2 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1736,26 +1736,10 @@ static void emit_concretecheck(jl_codectx_t &ctx, Value *typ, const std::string
17361736
error_unless(ctx, emit_isconcrete(ctx, typ), msg);
17371737
}
17381738

1739-
#define CHECK_BOUNDS 1
1740-
static bool bounds_check_enabled(jl_codectx_t &ctx, jl_value_t *inbounds) {
1741-
#if CHECK_BOUNDS==1
1742-
if (jl_options.check_bounds == JL_OPTIONS_CHECK_BOUNDS_ON)
1743-
return 1;
1744-
if (jl_options.check_bounds == JL_OPTIONS_CHECK_BOUNDS_OFF)
1745-
return 0;
1746-
if (inbounds == jl_false)
1747-
return 0;
1748-
return 1;
1749-
#else
1750-
return 0;
1751-
#endif
1752-
}
1753-
17541739
static Value *emit_bounds_check(jl_codectx_t &ctx, const jl_cgval_t &ainfo, jl_value_t *ty, Value *i, Value *len, jl_value_t *boundscheck)
17551740
{
17561741
Value *im1 = ctx.builder.CreateSub(i, ConstantInt::get(ctx.types().T_size, 1));
1757-
#if CHECK_BOUNDS==1
1758-
if (bounds_check_enabled(ctx, boundscheck)) {
1742+
if (boundscheck != jl_false) {
17591743
++EmittedBoundschecks;
17601744
Value *ok = ctx.builder.CreateICmpULT(im1, len);
17611745
setName(ctx.emission_context, ok, "boundscheck");
@@ -1790,7 +1774,6 @@ static Value *emit_bounds_check(jl_codectx_t &ctx, const jl_cgval_t &ainfo, jl_v
17901774
ctx.f->getBasicBlockList().push_back(passBB);
17911775
ctx.builder.SetInsertPoint(passBB);
17921776
}
1793-
#endif
17941777
return im1;
17951778
}
17961779

@@ -2960,14 +2943,12 @@ static Value *emit_array_nd_index(
29602943
Value *a = boxed(ctx, ainfo);
29612944
Value *i = Constant::getNullValue(ctx.types().T_size);
29622945
Value *stride = ConstantInt::get(ctx.types().T_size, 1);
2963-
#if CHECK_BOUNDS==1
2964-
bool bc = bounds_check_enabled(ctx, inbounds);
2946+
bool bc = inbounds != jl_false;
29652947
BasicBlock *failBB = NULL, *endBB = NULL;
29662948
if (bc) {
29672949
failBB = BasicBlock::Create(ctx.builder.getContext(), "oob");
29682950
endBB = BasicBlock::Create(ctx.builder.getContext(), "idxend");
29692951
}
2970-
#endif
29712952
SmallVector<Value *> idxs(nidxs);
29722953
for (size_t k = 0; k < nidxs; k++) {
29732954
idxs[k] = emit_unbox(ctx, ctx.types().T_size, argv[k], (jl_value_t*)jl_long_type); // type asserted by caller
@@ -2979,7 +2960,6 @@ static Value *emit_array_nd_index(
29792960
if (k < nidxs - 1) {
29802961
assert(nd >= 0);
29812962
Value *d = emit_arraysize_for_unsafe_dim(ctx, ainfo, ex, k + 1, nd);
2982-
#if CHECK_BOUNDS==1
29832963
if (bc) {
29842964
BasicBlock *okBB = BasicBlock::Create(ctx.builder.getContext(), "ib");
29852965
// if !(i < d) goto error
@@ -2989,12 +2969,10 @@ static Value *emit_array_nd_index(
29892969
ctx.f->getBasicBlockList().push_back(okBB);
29902970
ctx.builder.SetInsertPoint(okBB);
29912971
}
2992-
#endif
29932972
stride = ctx.builder.CreateMul(stride, d);
29942973
setName(ctx.emission_context, stride, "stride");
29952974
}
29962975
}
2997-
#if CHECK_BOUNDS==1
29982976
if (bc) {
29992977
// We have already emitted a bounds check for each index except for
30002978
// the last one which we therefore have to do here.
@@ -3055,7 +3033,6 @@ static Value *emit_array_nd_index(
30553033
ctx.f->getBasicBlockList().push_back(endBB);
30563034
ctx.builder.SetInsertPoint(endBB);
30573035
}
3058-
#endif
30593036

30603037
return i;
30613038
}

0 commit comments

Comments
 (0)