Skip to content

Reduce the cost of hull and intersect_interval - #772

Merged
OlivierHnt merged 2 commits into
JuliaIntervals:masterfrom
timholy:teh/hull
Aug 4, 2026
Merged

Reduce the cost of hull and intersect_interval#772
OlivierHnt merged 2 commits into
JuliaIntervals:masterfrom
timholy:teh/hull

Conversation

@timholy

@timholy timholy commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

This follows the discussion in #585: can empty intervals be handled more cheaply, and can variadic set operations inspect their arguments only once? Both are possible after changing the internal representation of an empty interval, as @OlivierHnt suggested.

The prerequisite mince fix is already merged in #771.

Empty-interval representation

An empty BareInterval{<:AbstractFloat} was stored as (NaN, NaN). Every call to inf, sup, or bounds then had to translate those fields into the bounds required by IEEE 1788. Empty intervals are now stored as (typemax(T), typemin(T)), as they already were for Rational:

sup(x::BareInterval) = x.hi
bounds(x::BareInterval) = (x.lo, x.hi)
inf(x::BareInterval{<:AbstractFloat}) =
    ifelse(iszero(x.lo), copysign(x.lo, -1), x.lo)

The only remaining normalization restores the standard's -0 lower bound.

The implementation now relies on stored bounds never being NaN. is_valid_interval rejects NaN in checked constructors, and --check-bounds=yes checks the invariant in the internal constructor. The check compiles away otherwise because check_bounds is part of Julia's precompilation cache key.

The package's exported callables were swept over empty, entire, thin-zero, half-bounded, and ordinary intervals at the supported bound types. The only operation that produced a stored NaN was mince on empty or unbounded intervals, which #771 fixed.

Set operations

The new representation removes the special empty case from both operations. The empty bounds are neutral under min and max, while an empty operand of intersect_interval makes the computed lower bound exceed the upper bound:

hull(x::BareInterval{T}, y::BareInterval{T}) where {T<:NumTypes} =
    _unsafe_bareinterval(T, min(x.lo, y.lo), max(x.hi, y.hi))

For decorated intervals, the minimum decoration is ill exactly when an operand is NaI. The same minimum supplies the result for dec = :auto. After excluding NaI, dec = :default is always trv, so it needs no further decoration adjustment.

The variadic methods now reduce the bare intervals and compute the decoration minimum and isguaranteed conjunction once. Dedicated variadic methods for BareInterval also fix a MethodError: the previous generic fallback passed a dec keyword to a method that does not accept one.

Benchmarks

Measured on an i7-14700KF with Julia 1.12.6:

Operation master branch IntervalFastMath
hull(bare, bare) 2.47 ns 1.81 ns 1.82 ns
hull(Interval, Interval) 6.18 ns 2.18 ns 1.82 ns
hull(w, x, y, z) 16.97 ns 3.78 ns 2.82 ns
intersect_interval(Interval, Interval) 6.34 ns 2.01 ns

In loops over 1000-element vectors, consuming both bounds of every result:

Operation master branch speedup
reduce(hull, v) 9.76 ns 2.66 ns 3.7×
out[i] = hull(x[i], y[i]) 6.26 ns 1.75 ns 3.6×
reduce(intersect_interval, v) 6.09 ns 1.08 ns 5.7×
out[i] = intersect_interval(x[i], y[i]) 6.32 ns 2.01 ns 3.1×

Cheaper bound access also improves other operations: addition and subtraction by about 1.4×, multiplication by 1.24×, and mid by 1.3× on Interval and 2.3× on BareInterval. There is no allocation change for BigFloat or Rational.

Testing

New tests cover the representation invariants, empty and NaI operands, decoration semantics, variadic and pairwise agreement, isguaranteed propagation, and bound-type promotion.

@github-actions

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Benchmark Results (Julia v1)

Time benchmarks
master cccf4e8... master / cccf4e8...
BigFloat MPFI/basics/* 24.8 ± 3.5 μs 25.3 ± 3.5 μs 0.981 ± 0.2
BigFloat MPFI/basics/+ 14 ± 3.4 μs 12.4 ± 3.5 μs 1.13 ± 0.42
BigFloat MPFI/basics/- 17.4 ± 3.6 μs 17.3 ± 3.5 μs 1.01 ± 0.29
BigFloat MPFI/basics// 19.8 ± 3.6 μs 19.2 ± 3.4 μs 1.03 ± 0.26
BigFloat MPFI/basics/abs 10.7 ± 3.4 μs 11.2 ± 3.3 μs 0.961 ± 0.41
BigFloat MPFI/basics/acos 0.678 ± 0.0086 ms 0.679 ± 0.0083 ms 0.998 ± 0.018
BigFloat MPFI/basics/asin 0.657 ± 0.0098 ms 0.657 ± 0.0093 ms 1 ± 0.021
BigFloat MPFI/basics/atan 2.1 ± 0.0077 ms 2.1 ± 0.0082 ms 0.999 ± 0.0053
BigFloat MPFI/basics/cos 0.362 ± 0.01 ms 0.361 ± 0.0097 ms 1 ± 0.039
BigFloat MPFI/basics/cosh 0.432 ± 0.011 ms 0.43 ± 0.012 ms 1.01 ± 0.039
BigFloat MPFI/basics/exp 0.503 ± 0.01 ms 0.501 ± 0.011 ms 1 ± 0.03
BigFloat MPFI/basics/inv 18.9 ± 3.4 μs 19.1 ± 3.5 μs 0.987 ± 0.25
BigFloat MPFI/basics/log 0.47 ± 0.0094 ms 0.47 ± 0.0099 ms 1 ± 0.029
BigFloat MPFI/basics/sin 0.464 ± 0.011 ms 0.466 ± 0.011 ms 0.996 ± 0.033
BigFloat MPFI/basics/sinh 0.574 ± 0.01 ms 0.572 ± 0.01 ms 1 ± 0.025
BigFloat MPFI/basics/sqrt 27.7 ± 6.7 μs 27.4 ± 7 μs 1.01 ± 0.35
BigFloat MPFI/basics/tan 0.386 ± 0.01 ms 0.385 ± 0.01 ms 1 ± 0.038
BigFloat MPFI/basics/tanh 0.584 ± 0.0097 ms 0.583 ± 0.0097 ms 1 ± 0.024
BigFloat bareinterval/basics/* 0.0411 ± 0.0027 ms 0.0393 ± 0.0025 ms 1.05 ± 0.095
BigFloat bareinterval/basics/+ 21.9 ± 3.3 μs 21.4 ± 3.4 μs 1.02 ± 0.22
BigFloat bareinterval/basics/- 22.9 ± 3.3 μs 22.3 ± 3.3 μs 1.02 ± 0.21
BigFloat bareinterval/basics// 20.4 ± 5.1 μs 19.6 ± 5.2 μs 1.04 ± 0.38
BigFloat bareinterval/basics/abs 15.7 ± 3.2 μs 15.3 ± 3.1 μs 1.03 ± 0.3
BigFloat bareinterval/basics/acos 0.715 ± 0.026 ms 0.709 ± 0.024 ms 1.01 ± 0.05
BigFloat bareinterval/basics/asin 0.698 ± 0.025 ms 0.692 ± 0.022 ms 1.01 ± 0.048
BigFloat bareinterval/basics/atan 2.12 ± 0.0069 ms 2.11 ± 0.0071 ms 1 ± 0.0047
BigFloat bareinterval/basics/cos 0.344 ± 0.087 ms 0.341 ± 0.086 ms 1.01 ± 0.36
BigFloat bareinterval/basics/cosh 0.473 ± 0.02 ms 0.474 ± 0.023 ms 0.998 ± 0.065
BigFloat bareinterval/basics/exp 0.511 ± 0.01 ms 0.511 ± 0.01 ms 1 ± 0.028
BigFloat bareinterval/basics/inv 27.2 ± 3.5 μs 26.7 ± 3.4 μs 1.02 ± 0.18
BigFloat bareinterval/basics/log 0.519 ± 0.014 ms 0.503 ± 0.012 ms 1.03 ± 0.037
BigFloat bareinterval/basics/sin 0.5 ± 0.097 ms 0.5 ± 0.093 ms 0.999 ± 0.27
BigFloat bareinterval/basics/sinh 0.583 ± 0.011 ms 0.584 ± 0.011 ms 1 ± 0.027
BigFloat bareinterval/basics/sqrt 0.0358 ± 0.0029 ms 31.6 ± 2.4 μs 1.13 ± 0.13
BigFloat bareinterval/basics/tan 0.294 ± 0.056 ms 0.303 ± 0.056 ms 0.968 ± 0.26
BigFloat bareinterval/basics/tanh 0.598 ± 0.011 ms 0.595 ± 0.011 ms 1.01 ± 0.026
BigFloat interval/basics/* 0.0442 ± 0.002 ms 0.043 ± 0.002 ms 1.03 ± 0.066
BigFloat interval/basics/+ 25 ± 8.3 μs 23.6 ± 8.1 μs 1.06 ± 0.51
BigFloat interval/basics/- 25.1 ± 8 μs 24.6 ± 8.4 μs 1.02 ± 0.48
BigFloat interval/basics// 25.8 ± 4.9 μs 24.2 ± 4.9 μs 1.07 ± 0.3
BigFloat interval/basics/abs 14.6 ± 7.7 μs 14.2 ± 7.6 μs 1.03 ± 0.76
BigFloat interval/basics/acos 0.71 ± 0.044 ms 0.709 ± 0.04 ms 1 ± 0.084
BigFloat interval/basics/asin 0.691 ± 0.036 ms 0.687 ± 0.034 ms 1 ± 0.072
BigFloat interval/basics/atan 2.12 ± 0.0069 ms 2.12 ± 0.0086 ms 0.998 ± 0.0052
BigFloat interval/basics/cos 0.34 ± 0.014 ms 0.336 ± 0.013 ms 1.01 ± 0.057
BigFloat interval/basics/cosh 0.468 ± 0.019 ms 0.472 ± 0.023 ms 0.99 ± 0.062
BigFloat interval/basics/exp 0.518 ± 0.0097 ms 0.52 ± 0.0098 ms 0.997 ± 0.026
BigFloat interval/basics/inv 31.1 ± 7.6 μs 30.5 ± 7 μs 1.02 ± 0.34
BigFloat interval/basics/log 0.511 ± 0.044 ms 0.504 ± 0.035 ms 1.02 ± 0.11
BigFloat interval/basics/sin 0.489 ± 0.019 ms 0.487 ± 0.018 ms 1 ± 0.054
BigFloat interval/basics/sinh 0.591 ± 0.0098 ms 0.59 ± 0.01 ms 1 ± 0.024
BigFloat interval/basics/sqrt 0.0432 ± 0.031 ms 0.0405 ± 0.029 ms 1.07 ± 1.1
BigFloat interval/basics/tan 0.282 ± 0.014 ms 0.28 ± 0.013 ms 1.01 ± 0.07
BigFloat interval/basics/tanh 0.602 ± 0.01 ms 0.601 ± 0.01 ms 1 ± 0.024
bareinterval/basics/* 1.94 ± 0.04 μs 1.73 ± 0.04 μs 1.12 ± 0.035
bareinterval/basics/+ 0.531 ± 0.01 μs 0.521 ± 0.01 μs 1.02 ± 0.027
bareinterval/basics/- 0.541 ± 0 μs 0.541 ± 0.01 μs 1 ± 0.018
bareinterval/basics// 1.18 ± 0.02 μs 1.22 ± 0.03 μs 0.967 ± 0.029
bareinterval/basics/abs 0.22 ± 0.01 μs 0.191 ± 0.011 μs 1.15 ± 0.085
bareinterval/basics/acos 2.9 ± 0.039 μs 2.73 ± 0.03 μs 1.06 ± 0.018
bareinterval/basics/asin 3.14 ± 0.03 μs 2.98 ± 0.03 μs 1.05 ± 0.015
bareinterval/basics/atan 13 ± 0.22 μs 13.1 ± 0.17 μs 0.992 ± 0.021
bareinterval/basics/cos 0.0506 ± 0.00069 ms 0.0495 ± 0.00084 ms 1.02 ± 0.022
bareinterval/basics/cosh 7.25 ± 0.03 μs 7.24 ± 0.033 μs 1 ± 0.0062
bareinterval/basics/exp 4.29 ± 0.05 μs 4.26 ± 0.05 μs 1.01 ± 0.017
bareinterval/basics/inv 1.05 ± 0.02 μs 1.02 ± 0.02 μs 1.03 ± 0.028
bareinterval/basics/log 2.11 ± 0.02 μs 1.94 ± 0.021 μs 1.09 ± 0.016
bareinterval/basics/sin 0.0495 ± 0.018 ms 0.0488 ± 0.018 ms 1.01 ± 0.52
bareinterval/basics/sinh 9.98 ± 0.05 μs 10 ± 0.11 μs 0.995 ± 0.012
bareinterval/basics/sqrt 1.17 ± 0.02 μs 1.12 ± 0.01 μs 1.05 ± 0.02
bareinterval/basics/tan 29 ± 0.25 μs 28.4 ± 0.24 μs 1.02 ± 0.012
bareinterval/basics/tanh 0.296 ± 0.009 ms 0.299 ± 0.0089 ms 0.991 ± 0.042
interval/basics/* 2.83 ± 0.061 μs 3.09 ± 0.06 μs 0.915 ± 0.027
interval/basics/+ 1.37 ± 0.04 μs 1.58 ± 0.05 μs 0.867 ± 0.037
interval/basics/- 1.37 ± 0.04 μs 1.64 ± 0.041 μs 0.836 ± 0.032
interval/basics// 1.94 ± 0.05 μs 2.03 ± 0.07 μs 0.955 ± 0.041
interval/basics/abs 0.381 ± 0.03 μs 0.361 ± 0.04 μs 1.06 ± 0.14
interval/basics/acos 4.26 ± 0.04 μs 3.31 ± 0.05 μs 1.29 ± 0.023
interval/basics/asin 4.47 ± 0.04 μs 3.57 ± 0.05 μs 1.25 ± 0.021
interval/basics/atan 13.8 ± 0.23 μs 13.1 ± 0.23 μs 1.06 ± 0.026
interval/basics/cos 0.0684 ± 0.0034 ms 0.0676 ± 0.0034 ms 1.01 ± 0.071
interval/basics/cosh 9.01 ± 0.07 μs 9.03 ± 0.07 μs 0.998 ± 0.011
interval/basics/exp 5.9 ± 0.05 μs 4.93 ± 0.07 μs 1.2 ± 0.02
interval/basics/inv 1.61 ± 0.059 μs 1.51 ± 0.061 μs 1.07 ± 0.058
interval/basics/log 3.75 ± 0.05 μs 2.65 ± 0.06 μs 1.42 ± 0.037
interval/basics/sin 0.0673 ± 0.0033 ms 0.0668 ± 0.0034 ms 1.01 ± 0.072
interval/basics/sinh 11.5 ± 0.061 μs 10.6 ± 0.1 μs 1.09 ± 0.012
interval/basics/sqrt 1.67 ± 0.03 μs 1.58 ± 0.04 μs 1.06 ± 0.033
interval/basics/tan 0.0391 ± 0.0018 ms 0.0387 ± 0.0018 ms 1.01 ± 0.066
interval/basics/tanh 0.309 ± 0.008 ms 0.309 ± 0.0083 ms 0.998 ± 0.037
time_to_load 0.111 ± 0.0014 s 0.111 ± 0.002 s 1 ± 0.022
Memory benchmarks
master cccf4e8... master / cccf4e8...
BigFloat MPFI/basics/* 0.424 k allocs: 21.3 kB 0.424 k allocs: 21.3 kB 1
BigFloat MPFI/basics/+ 0.302 k allocs: 16.5 kB 0.302 k allocs: 16.5 kB 1
BigFloat MPFI/basics/- 0.402 k allocs: 20.4 kB 0.402 k allocs: 20.4 kB 1
BigFloat MPFI/basics// 0.345 k allocs: 18.2 kB 0.345 k allocs: 18.2 kB 1
BigFloat MPFI/basics/abs 0.324 k allocs: 17.4 kB 0.324 k allocs: 17.4 kB 1
BigFloat MPFI/basics/acos 1.24 k allocs: 0.0445 MB 1.24 k allocs: 0.0445 MB 1
BigFloat MPFI/basics/asin 1.08 k allocs: 0.0355 MB 1.08 k allocs: 0.0355 MB 1
BigFloat MPFI/basics/atan 2.14 k allocs: 0.0543 MB 2.14 k allocs: 0.0543 MB 1
BigFloat MPFI/basics/cos 2.9 k allocs: 0.0975 MB 2.9 k allocs: 0.0975 MB 1
BigFloat MPFI/basics/cosh 0.781 k allocs: 0.0322 MB 0.781 k allocs: 0.0322 MB 1
BigFloat MPFI/basics/exp 0.902 k allocs: 0.036 MB 0.902 k allocs: 0.036 MB 1
BigFloat MPFI/basics/inv 0.351 k allocs: 18.4 kB 0.351 k allocs: 18.4 kB 1
BigFloat MPFI/basics/log 0.407 k allocs: 20.6 kB 0.407 k allocs: 20.6 kB 1
BigFloat MPFI/basics/sin 3.18 k allocs: 0.106 MB 3.18 k allocs: 0.106 MB 1
BigFloat MPFI/basics/sinh 0.915 k allocs: 0.0369 MB 0.915 k allocs: 0.0369 MB 1
BigFloat MPFI/basics/sqrt 0.302 k allocs: 16.5 kB 0.302 k allocs: 16.5 kB 1
BigFloat MPFI/basics/tan 2.8 k allocs: 0.098 MB 2.8 k allocs: 0.098 MB 1
BigFloat MPFI/basics/tanh 0.908 k allocs: 0.0362 MB 0.908 k allocs: 0.0362 MB 1
BigFloat bareinterval/basics/* 0.262 k allocs: 26 kB 0.262 k allocs: 26 kB 1
BigFloat bareinterval/basics/+ 0.202 k allocs: 20.4 kB 0.202 k allocs: 20.4 kB 1
BigFloat bareinterval/basics/- 0.202 k allocs: 20.4 kB 0.202 k allocs: 20.4 kB 1
BigFloat bareinterval/basics// 0.1 k allocs: 10.8 kB 0.1 k allocs: 10.8 kB 1
BigFloat bareinterval/basics/abs 0.199 k allocs: 20.1 kB 0.199 k allocs: 20.1 kB 1
BigFloat bareinterval/basics/acos 1.33 k allocs: 0.0741 MB 1.27 k allocs: 0.0692 MB 1.07
BigFloat bareinterval/basics/asin 1.23 k allocs: 0.0647 MB 1.17 k allocs: 0.0597 MB 1.08
BigFloat bareinterval/basics/atan 2.04 k allocs: 0.08 MB 2.04 k allocs: 0.08 MB 1
BigFloat bareinterval/basics/cos 2.45 k allocs: 0.185 MB 2.45 k allocs: 0.185 MB 1
BigFloat bareinterval/basics/cosh 1.01 k allocs: 0.0671 MB 1.01 k allocs: 0.0671 MB 1
BigFloat bareinterval/basics/exp 0.802 k allocs: 0.0397 MB 0.802 k allocs: 0.0397 MB 1
BigFloat bareinterval/basics/inv 0.198 k allocs: 20 kB 0.198 k allocs: 20 kB 1
BigFloat bareinterval/basics/log 0.804 k allocs: 0.0694 MB 0.616 k allocs: 0.0522 MB 1.33
BigFloat bareinterval/basics/sin 2.9 k allocs: 0.197 MB 2.9 k allocs: 0.197 MB 1
BigFloat bareinterval/basics/sinh 0.815 k allocs: 0.0407 MB 0.815 k allocs: 0.0407 MB 1
BigFloat bareinterval/basics/sqrt 0.304 k allocs: 29.9 kB 0.26 k allocs: 25.8 kB 1.16
BigFloat bareinterval/basics/tan 1.67 k allocs: 0.124 MB 1.67 k allocs: 0.124 MB 1
BigFloat bareinterval/basics/tanh 0.808 k allocs: 0.0399 MB 0.808 k allocs: 0.0399 MB 1
BigFloat interval/basics/* 0.263 k allocs: 26.8 kB 0.263 k allocs: 26.8 kB 1
BigFloat interval/basics/+ 0.203 k allocs: 21.2 kB 0.203 k allocs: 21.2 kB 1
BigFloat interval/basics/- 0.203 k allocs: 21.2 kB 0.203 k allocs: 21.2 kB 1
BigFloat interval/basics// 0.101 k allocs: 11.6 kB 0.101 k allocs: 11.6 kB 1
BigFloat interval/basics/abs 0.2 k allocs: 20.9 kB 0.2 k allocs: 20.9 kB 1
BigFloat interval/basics/acos 1.77 k allocs: 0.115 MB 1.71 k allocs: 0.11 MB 1.04
BigFloat interval/basics/asin 1.53 k allocs: 0.0931 MB 1.47 k allocs: 0.088 MB 1.06
BigFloat interval/basics/atan 2.04 k allocs: 0.0814 MB 2.04 k allocs: 0.0812 MB 1
BigFloat interval/basics/cos 2.45 k allocs: 0.186 MB 2.45 k allocs: 0.186 MB 1
BigFloat interval/basics/cosh 1.01 k allocs: 0.0681 MB 1.01 k allocs: 0.068 MB 1
BigFloat interval/basics/exp 0.803 k allocs: 0.0408 MB 0.803 k allocs: 0.0408 MB 0.999
BigFloat interval/basics/inv 0.199 k allocs: 20.8 kB 0.199 k allocs: 20.8 kB 1
BigFloat interval/basics/log 1.1 k allocs: 0.0977 MB 0.917 k allocs: 0.0805 MB 1.21
BigFloat interval/basics/sin 2.9 k allocs: 0.198 MB 2.9 k allocs: 0.198 MB 1
BigFloat interval/basics/sinh 0.816 k allocs: 0.0417 MB 0.816 k allocs: 0.0418 MB 0.998
BigFloat interval/basics/sqrt 0.758 k allocs: 0.0715 MB 0.714 k allocs: 0.0675 MB 1.06
BigFloat interval/basics/tan 1.67 k allocs: 0.125 MB 1.67 k allocs: 0.125 MB 1
BigFloat interval/basics/tanh 0.809 k allocs: 0.041 MB 0.809 k allocs: 0.041 MB 1
bareinterval/basics/* 2 allocs: 1.62 kB 2 allocs: 1.62 kB 1
bareinterval/basics/+ 2 allocs: 1.62 kB 2 allocs: 1.62 kB 1
bareinterval/basics/- 2 allocs: 1.62 kB 2 allocs: 1.62 kB 1
bareinterval/basics// 2 allocs: 1.62 kB 2 allocs: 1.62 kB 1
bareinterval/basics/abs 2 allocs: 1.62 kB 2 allocs: 1.62 kB 1
bareinterval/basics/acos 2 allocs: 1.62 kB 2 allocs: 1.62 kB 1
bareinterval/basics/asin 2 allocs: 1.62 kB 2 allocs: 1.62 kB 1
bareinterval/basics/atan 2 allocs: 1.62 kB 2 allocs: 1.62 kB 1
bareinterval/basics/cos 0.56 k allocs: 0.0423 MB 0.56 k allocs: 0.0423 MB 1
bareinterval/basics/cosh 2 allocs: 1.62 kB 2 allocs: 1.62 kB 1
bareinterval/basics/exp 2 allocs: 1.62 kB 2 allocs: 1.62 kB 1
bareinterval/basics/inv 2 allocs: 1.62 kB 2 allocs: 1.62 kB 1
bareinterval/basics/log 2 allocs: 1.62 kB 2 allocs: 1.62 kB 1
bareinterval/basics/sin 0.56 k allocs: 0.0423 MB 0.56 k allocs: 0.0423 MB 1
bareinterval/basics/sinh 2 allocs: 1.62 kB 2 allocs: 1.62 kB 1
bareinterval/basics/sqrt 2 allocs: 1.62 kB 2 allocs: 1.62 kB 1
bareinterval/basics/tan 0.29 k allocs: 23 kB 0.29 k allocs: 23 kB 1
bareinterval/basics/tanh 1.03 k allocs: 0.0359 MB 1.03 k allocs: 0.0359 MB 1
interval/basics/* 3 allocs: 2.45 kB 3 allocs: 2.45 kB 1
interval/basics/+ 3 allocs: 2.45 kB 3 allocs: 2.45 kB 1
interval/basics/- 3 allocs: 2.45 kB 3 allocs: 2.45 kB 1
interval/basics// 3 allocs: 2.45 kB 3 allocs: 2.45 kB 1
interval/basics/abs 3 allocs: 2.45 kB 3 allocs: 2.45 kB 1
interval/basics/acos 3 allocs: 2.45 kB 3 allocs: 2.45 kB 1
interval/basics/asin 3 allocs: 2.45 kB 3 allocs: 2.45 kB 1
interval/basics/atan 3 allocs: 2.45 kB 3 allocs: 2.45 kB 1
interval/basics/cos 0.561 k allocs: 0.0431 MB 0.561 k allocs: 0.0431 MB 1
interval/basics/cosh 3 allocs: 2.45 kB 3 allocs: 2.45 kB 1
interval/basics/exp 3 allocs: 2.45 kB 3 allocs: 2.45 kB 1
interval/basics/inv 3 allocs: 2.45 kB 3 allocs: 2.45 kB 1
interval/basics/log 3 allocs: 2.45 kB 3 allocs: 2.45 kB 1
interval/basics/sin 0.561 k allocs: 0.0431 MB 0.561 k allocs: 0.0431 MB 1
interval/basics/sinh 3 allocs: 2.45 kB 3 allocs: 2.45 kB 1
interval/basics/sqrt 3 allocs: 2.45 kB 3 allocs: 2.45 kB 1
interval/basics/tan 0.291 k allocs: 24.4 kB 0.291 k allocs: 24.4 kB 1
interval/basics/tanh 1.03 k allocs: 0.0367 MB 1.03 k allocs: 0.0367 MB 1
time_to_load 0.149 k allocs: 11.2 kB 0.149 k allocs: 11.2 kB 1

Comment thread src/intervals/interval_operations/set_operations.jl Outdated
Comment thread src/intervals/interval_operations/set_operations.jl Outdated
@OlivierHnt

OlivierHnt commented Jul 31, 2026

Copy link
Copy Markdown
Member
  1. The use of --check-bounds is clever, but I am not sure it's good to conflate its meaning with the NaN check of interval bounds.
    One hope I had was that the NaN check could occur before the call to _unsafe_bareinterval, say somewhere in the (bare)interval constructor pipeline. Then maybe it's always safe (or anyways for hull and some others) to skip the NaN check.
    Perhaps this is also supported by this sentence:

The only operation that produced a stored NaN was mince on empty or unbounded intervals, which #771 fixed.

  1. There is still a little performance gap between IFM and this PR. Is this due to the normalization of 0.0? Then maybe, as 1. above, the normaliszero call could be handled outside of _unsafe_bareinterval.

EDIT: Oh nevermind, the performance gap is for Interval input, so that's just coming from the handling of Decoration. So checking NaN has a noticeable impact on the perf, whereas the normalization of 0.0 is completely innocuous.
Interesting 🤔

@timholy

timholy commented Jul 31, 2026

Copy link
Copy Markdown
Contributor Author

Yes, the main idea here is to never produce NaNs so you never have to check for them (this is why #771 had to come first). That check is expensive because it introduces branching, and branches break things like vectorization (https://en.wikipedia.org/wiki/Automatic_vectorization, see sections on control flow). Opting into the check gives you a way to easily check the validity of your code; but to be fast, the decision about whether to omit the safety check must be made at compile time.

The trick: if it's a package-wide setting rather than a type parameter, it has to depend on a setting that makes it into Julia's "is this precompiled cache valid given the settings for this session?" query. Otherwise Julia might end up picking, e.g., an "unsafe" precompiled cache in a project where you've deliberately enabled the check. --check-bounds is one of only a few options; the others correspond to --pkgimages, -g (aka --debug-info), --inline, -O (optimization level). Arguably -g or -O might make more sense.

There is one alternative to Julia's own settings: Preferences.jl, using a compile-time constant.

If you agree with the overall idea of setting this choice at compile time, which of these options seems best to you? Any of these could be activated as a special build step on CI, if you're prepared to wait for that one job (it will be a little slower, but not slower than it is now).

@OlivierHnt

OlivierHnt commented Jul 31, 2026

Copy link
Copy Markdown
Member

[...] to never produce NaNs so you never have to check for them [...]

Then why not modifying _unsafe_bareinterval by removing the check for NaN entirely, and putting this check upstream in the interval and bareinterval constructor pipeline?

EDIT: to be clear; the idea would be to make _unsafe_bareinterval as close to a no-op as possible. The cost of checking for an input NaN when constructing an interval should be more than marginal.
Either the entire call _normalisebound(a) can be put upstream in the constructor and also perhaps where needed at specific location across the source code when the operation might "de-normalise". Or, if that's too much, and since you already check that NaN is already out of scope, then only the _assert_not_nan(a) stays a mere isnan call.

@OlivierHnt

Copy link
Copy Markdown
Member

That check is expensive because it introduces branching [...]

Really? I mean it's just isnan(x::AbstractFloat) = (x != x)::Bool. Perhaps that's due to ::Bool; that would be unfortunate.

@OlivierHnt

Copy link
Copy Markdown
Member

I did not try it, but I now convinced myself that we can just drop the _assert_nan entirely.. If is_valid_interval already produces an empty interval for BareInterval (and an NaI for Interval which is stored as an empty interval internally) then everything should be safe. (unless someone hacks into the private functions, but that is always strongly discouraged)

What am I missing?

@timholy

timholy commented Jul 31, 2026

Copy link
Copy Markdown
Contributor Author

It's completely optional. I added it as an escape hatch to catch regressions; if any future change in the package does start producing NaNs, turning that check on will catch them directly at the source rather than letting them cascade through a bigger computation. That's the only advantage it provides, and that may not be much of one since you can also just try to trace back once you observe NaNs. To use this to isolate the source of the NaNs would require starting a fresh Julia session with the option turned on.

Really? I mean it's just isnan(x::AbstractFloat) = (x != x)::Bool. Perhaps that's due to ::Bool; that would be unfortunate.

It's not the evaluation of the Bool that's expensive; it's the change it causes in the compiled code. If you've never seen the "train switch station analogy," you might want to read the first answer to this question; it's entertaining and where I first learned about these issues. (Claude helped me find it again, and informed me that it's still the most upvoted stackoverflow question of all time!)

I'm happy to ditch the check altogether if that's what you prefer.

@OlivierHnt

OlivierHnt commented Jul 31, 2026

Copy link
Copy Markdown
Member

Thx for the link. I thought that our NaN machinery (on master) was not branching (eg

sup(x::BareInterval{T}) where {T<:AbstractFloat} = ifelse(isnan(x.hi), typemin(T), x.hi)
), we use ifelse as much as possible.
That's also the case for normalisezero, which here seems "free".

My vote is to remove the mechanism with --check-bounds=true (it feels too hacky for me)... let me mull over this for a bit 😅

For now there is just my two minor comments on the PR.

@timholy

timholy commented Aug 2, 2026

Copy link
Copy Markdown
Contributor Author

The branching is in hull:

isempty_interval(x) & isempty_interval(y) && return x

Here's a tally of the LLVM:

br select fcmp IR lines
sup(::BareInterval) before 0 1 1 9
sup(::BareInterval) after 0 0 0 7
inf(::BareInterval) before 0 2 2 12
inf(::BareInterval) after 0 1 1 10
hull(bare, bare) before 3 3 4 38
hull(bare, bare) after 0 1 1 18
intersect_interval(bare, bare) before 3 7 8 42
intersect_interval(bare, bare) after 3 1 2 26

@OlivierHnt

Copy link
Copy Markdown
Member

Ah of course, for some reason I got fixated on the normalizations involved in inf and sup... thanks!

@timholy

timholy commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

Glad it helps. Let me know what you decide about

remove the mechanism with --check-bounds=true (it feels too hacky for me)... let me mull over this for a bit

and then I'll make any further changes. (I addressed your two review points in the most recent force-push.)

@OlivierHnt

Copy link
Copy Markdown
Member

@Kolaru is going to run some checks for IntervalRootFinding.jl just to be sure.

I think the mechanism --check-bounds=true will be removed before merging the PR.

@Kolaru

Kolaru commented Aug 4, 2026

Copy link
Copy Markdown
Member

I ran the tests of IntervalRootFinding and TaylorModels with this PR and everything seems fine. So it looks like this PR doesn't break much downstream.

Regarding --check-bounds, I agree with Olivier that it is a bit strange: I would assume that this options refers exclusively to array bounds, mostly in the context of accessing part of the memory. Despite sharing the name for bounds, I think that we are doing something fundamentally different.

However, the idea could be added to the mechanism proposed in #637, together with other checks at construction (e.g. loss of guarantee).

@OlivierHnt

Copy link
Copy Markdown
Member

Fantastic thanks for running these checks.

@timholy once you removed the --check-bounds safeguard; we are good to go to merge the PR. 🎉

timholy added 2 commits August 4, 2026 06:08
Empty floating-point bare intervals were stored as `(NaN, NaN)`. Every
call to `inf`, `sup`, or `bounds` then translated those fields into the
bounds required by IEEE 1788.

Store `(typemax(T), typemin(T))` directly, as was already done for
rational bounds. This makes `sup` and `bounds` plain field accesses;
`inf` only needs to restore a negative zero lower bound.

Bounds that are not `NaN` become a requirement of the internal
constructor `_unsafe_bareinterval`; the checked constructors already
reject `NaN`.

This speeds up addition and subtraction by about 1.4x, multiplication by
1.2x, and `mid` by 2.3x.

Refs JuliaIntervals#585.

Assisted-by: Claude Opus 5 <noreply@anthropic.com>
The bounds of an empty interval are neutral under `min` and `max`; for
intersection, an empty operand makes the computed lower bound exceed the
upper bound. The bare operations can therefore read stored bounds
directly without a separate emptiness check.

For decorated intervals, the minimum decoration is `ill` exactly when an
operand is NaI, and the same value supplies `dec = :auto`. After
excluding NaI, `dec = :default` is always `trv` and needs no further
adjustment.

The variadic methods now compute the decoration minimum and
`isguaranteed` conjunction once, then reduce only the bare intervals.
Dedicated variadic `BareInterval` methods also fix the previous
`MethodError` caused by forwarding an unsupported `dec` keyword.

On Julia 1.12.6, pairwise `hull` for `Interval{Float64}` improves from
6.2 ns to 2.2 ns, four-argument `hull` from 17.0 ns to 3.8 ns, and
pairwise `intersect_interval` from 6.3 ns to 2.0 ns.

Refs JuliaIntervals#585.

Assisted-by: Claude Opus 5 <noreply@anthropic.com>
@timholy

timholy commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

Done! I'm happy with it once it passes CI, merge at will. I also added some notes to #637 and linked back here.

@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 74.27%. Comparing base (eaee4e6) to head (cccf4e8).
⚠️ Report is 6 commits behind head on master.
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #772      +/-   ##
==========================================
+ Coverage   73.77%   74.27%   +0.49%     
==========================================
  Files          32       32              
  Lines        3047     3090      +43     
==========================================
+ Hits         2248     2295      +47     
+ Misses        799      795       -4     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@OlivierHnt
OlivierHnt merged commit 943ee67 into JuliaIntervals:master Aug 4, 2026
22 checks passed
@timholy
timholy deleted the teh/hull branch August 4, 2026 22:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants