What is the runtime cost of array bounds checks, and how does the JIT compiler optimize them away?
answer
- compare+branch per access; one unsigned compare
- JIT = HotSpot C2 on hot methods
- BCE via range analysis on i < a.length
- loop predication hoists check; unroll enables SIMD
- speculate + uncommon trap -> deopt, never unsafe
basics
~20 sEach array access includes a tiny check that the index is valid, which costs a compare-and-branch. The JIT compiler often proves the index is always safe (for example in a normal for loop) and removes the check, so well-written loops pay almost nothing.
solid answer
~50 sConceptually every array access compiles to: compare the index against length, branch to throw if out of range, then access. That is a couple of extra instructions and a branch per access. In hot code this would hurt, so the HotSpot JIT performs bounds-check elimination (BCE). Using range analysis it proves the index can never go out of range — the canonical case is for (i = 0; i < a.length; i++), where the loop bound is the length itself — and drops the check entirely. It also does loop unrolling, hoisting the check out of the loop (range-check elimination), and uses loop-predication / uncommon traps: it speculatively removes the check and, if a value ever violates the assumption, deoptimizes back to the safe version. The practical advice is to write straightforward, length-bounded loops so the optimizer recognizes the pattern; convoluted index math or aliasing can defeat BCE and leave the checks in.
go deeper
Aware that bounds checks add a small per-access cost but generally shouldn't be worried about for everyday code.
Knows the JIT can remove redundant checks in normal loops and that you shouldn't micro-optimize them manually.
Explains BCE via range analysis on length-bounded loops, loop predication/hoisting, and that idiomatic loops let the optimizer succeed.
Discusses speculative elimination backed by deoptimization, the interaction with vectorization/unrolling, patterns that defeat BCE, and weighs the safety guarantee against the (usually negligible) steady-state cost versus Unsafe/foreign-memory bypasses.
## The cost model A bounds check is logically inserted before **every** array access. In pseudo-machine terms `x = a[i]` becomes: ``` if (i < 0 || i >= a.length) throw AIOOBE; x = load a[base + i*scale]; ``` (The two comparisons are usually folded into **one unsigned compare**: treating `i` as unsigned, any negative value becomes a huge number, so `((unsigned)i >= length)` catches both `i < 0` and `i >= length` in a single instruction.) The runtime cost is therefore roughly **one compare + one branch per access**, plus the load of `a.length`. On modern CPUs a well-predicted branch is nearly free, but in a tight numeric loop these checks still add up and, more importantly, they can **block vectorization** and other optimizations. ## Why a JIT, and what 'hot' means The JVM first **interprets** bytecode, then the **JIT (Just-In-Time) compiler** — HotSpot's C1/C2 — compiles **hot** methods (frequently executed ones identified by profiling) to native code. It is at this stage that bounds checks can be optimized. ## Bounds-Check Elimination (BCE) **BCE** is the optimization that proves a check is unnecessary and deletes it. The compiler does **range/value analysis**: it tracks the possible range of the index variable. The textbook case is a counted loop bounded by the array's own length: ```java for (int i = 0; i < a.length; i++) { sum += a[i]; // i is provably in [0, a.length) -> check removed } ``` Here the loop condition `i < a.length` *guarantees* `i` is in range every iteration, so C2 removes the per-element check. Related techniques: - **Range-check elimination / loop predication:** instead of checking inside the loop, the JIT hoists a single check *before* the loop (a 'predicate'): if the whole range is known safe, run a fast checkless loop body; otherwise fall back. This converts N checks into ~1. - **Loop unrolling + main/post loops:** the compiler splits a loop into a checkless 'main' loop over the safe range and a small guarded remainder, enabling vectorization (SIMD) on the main part. - **Speculation + uncommon traps (deoptimization):** the JIT may *assume* an index stays in range based on profiling and emit code with no check. If that assumption is ever violated, it hits an **uncommon trap**, **deoptimizes** back to the interpreted/safe version, and re-checks. So correctness is never sacrificed — only the common-case cost. ## When BCE fails BCE depends on the compiler being able to *prove* safety. It is defeated by: - indices computed from opaque sources (method returns it can't analyze, complex arithmetic, `% `/bit tricks it can't bound), - accessing a *different* array than the one the loop is bounded by (the bound proves nothing about a second array), - writes that could change `length`-related state, or escaping references that confuse aliasing analysis. In those cases the check stays in, and you pay the per-access cost. ## Practical guidance - Write **simple, length-bounded `for` loops** (or enhanced `for`/`for-each`, which the JIT recognizes well) so BCE fires. - Avoid hand-rolled index arithmetic when a plain counted loop will do. - Don't try to 'beat' the checks with unsafe tricks; the JIT usually removes them, and `sun.misc.Unsafe`/foreign-memory bypasses sacrifice the safety guarantee. - Bounds checks are a **safety feature whose cost is largely a compile-time concern** — in steady-state hot code they typically cost close to zero. ## Key takeaways - A check is a single (unsigned) compare + branch + a `length` load per access. - HotSpot's C2 eliminates or hoists most of them via range analysis, loop predication, and unrolling. - Speculative removal is backed by deoptimization, so it never breaks correctness. - Idiomatic length-bounded loops are what let the optimizer succeed.
- How can a single CPU instruction check both the lower and upper bound?By treating the index as an unsigned integer: a negative signed value becomes a very large unsigned value, so a single 'unsigned i >= length' comparison catches both i < 0 and i >= length at once.
- If the JIT speculatively removes a check and the assumption later fails, is memory safety lost?No. The optimized code includes an uncommon trap; on a violated assumption the JVM deoptimizes back to the safe, checked version, so an actual out-of-bounds access never occurs.
- Name a code pattern that prevents bounds-check elimination.Indexing a second array by a counter bounded by a different array's length, or using an index from opaque arithmetic the compiler can't bound — the JIT can't prove safety, so it keeps the check.
saying these in an interview costs you the question
- Saying you should disable bounds checks for performance (you can't, and the JIT handles it)
- Claiming bounds checks make Java hopelessly slow for numeric code
- Thinking BCE works on any loop regardless of how the index is computed
- Believing speculative removal can cause an out-of-bounds read (deopt prevents that)