Are Kotlin range/for loops zero-overhead compared to handwritten index loops, and what allocations or boxing might occur? Discuss IntRange vs an IntProgression and the .indices iteration pattern.
answer
- literal range in for header = zero overhead
- boxing via Iterable<Int>/Iterator<Int> generics
- forEach/map on range can allocate+box
- IntRange is an IntProgression (first/last/step)
- indices = 0 until size, optimized to counter
basics
~20 sPlain integer for-loops like for (i in 0 until n) compile down to fast counter loops with no boxing. Costs can appear if you turn a range into an object, use very large Long ranges, or box values via generics.
solid answer
~50 sFor primitive ranges, the Kotlin compiler **optimizes the common case**: `for (i in 0 until n)` and `for (i in a..b step k)` are lowered to a plain JVM counter loop using `int` locals — no `IntRange` object is allocated and no `Integer` boxing occurs, matching a handwritten Java loop. The same applies to iterating an array or `list.indices`. Overhead appears when you (1) **materialize** a range as a value (`val r: IntRange = ...; for (i in r)`) where the compiler may iterate via the iterator object, or pass it through generic `Iterable<Int>` APIs that box each element; (2) iterate a `LongRange` (Long arithmetic); or (3) call `.iterator()` explicitly. `IntRange` is a closed range; `step`/`downTo` yield an `IntProgression` (with `first`/`last`/`step`). Using `forEach`/functional ops on a range can allocate a lambda/iterator and box, whereas a `for` over a literal range usually doesn't. Rule of thumb: keep ranges as loop literals on the hot path; avoid storing them in `Iterable<Int>`-typed variables when performance matters.
code
kotlin · 10 lines// Optimized: compiles to a primitive int counter loop, no allocation
var s = 0
for (i in 0 until n) s += i
// Slower: typed as Iterable<Int> -> boxing per element
val r: Iterable<Int> = 0 until n
for (i in r) s += i
// step/downTo produce an IntProgression
val p = 10 downTo 1 step 2 // first=10, last=2, step=-2go deeper
Believes idiomatic for loops are fine performance-wise (true) without deeper detail.
Knows literal integer ranges are efficient and that functional ops add some cost.
Explains compiler lowering to primitive counter loops, where boxing appears (generic Iterable<Int>), and IntRange vs IntProgression.
Reasons about hot-path allocation/boxing, when to keep ranges as literals, and the LinkedList random-access trap, balancing against readability.
## The optimized happy path When the range appears **directly** in the loop header with a known integer progression, the Kotlin compiler lowers it to a primitive counter loop: ```kotlin for (i in 0 until n) sum += i ``` becomes essentially: ```java for (int i = 0; i < n; i++) sum += i; // no IntRange object, no boxing ``` This holds for `..`, `until`, `downTo`, and constant `step`, and for `array.indices` / `list.indices`. So idiomatic integer loops are **zero-overhead** versus Java. ## Where overhead creeps in 1. **Materializing the range as a typed value.** Storing it widens the static type and can force iterator-based iteration: ```kotlin val r: Iterable<Int> = 0 until n // typed as Iterable<Int> for (i in r) { ... } // boxes each Int via the generic iterator ``` Keeping `val r = 0 until n` (inferred `IntRange`) is better, but a literal in the header is best of all on hot paths. 2. **Generics box.** Any path that goes through `Iterator<Int>` / `Iterable<Int>` boxes to `java.lang.Integer`, because generics on the JVM are erased and use objects. `IntRange`'s own iterator exposes a specialized `nextInt()` that the compiler can use, but generic call sites can't. 3. **Functional ops.** `(0 until n).forEach { }`, `.map { }`, `.sumOf { }` allocate lambdas/iterators and may box; a `for` loop over the literal range doesn't. 4. **Long ranges.** `LongRange`/`LongProgression` use 64-bit arithmetic — correct, slightly heavier than `Int`. ## IntRange vs IntProgression - `1..10` -> `IntRange` (a `ClosedRange<Int>` you can also use with `in`). - `1..10 step 2`, `10 downTo 1` -> `IntProgression` with `first`, `last`, `step`. `IntRange` is itself an `IntProgression`. Both are tiny value-holder objects, but on the optimized loop path none is actually allocated. ## .indices ```kotlin for (i in list.indices) { use(list[i]) } ``` `indices` returns `0 until size` and is optimized to a counter loop. For random-access lists/arrays this is fast; for a `LinkedList`, `list[i]` itself is O(n) — prefer `withIndex()`/direct iteration there. ## Practical guidance - Hot path: use range/index **literals** directly in the `for` header. - Avoid storing ranges in `Iterable<Int>`-typed variables. - Be aware functional combinators on ranges can box; a plain `for` won't. - Don't micro-optimize cold paths — readability wins. ## Summary Idiomatic integer for/range loops are zero-overhead (no alloc, no boxing). Boxing/allocation appears via generic `Iterable<Int>` typing, functional combinators, or explicit iterators. `IntRange` ⊂ `IntProgression`; both vanish on the optimized loop path.
- Does for (i in 0 until n) allocate an IntRange object?No — when the range is a literal in the loop header, the compiler lowers it to a plain int counter loop with no allocation and no boxing.
- Why might (0 until n).forEach { } be slower than the equivalent for loop?It goes through an iterator and a lambda, which can allocate and box Ints via generics, whereas the for-over-literal is specialized to primitives.
saying these in an interview costs you the question
- Claiming every range loop allocates an object
- Assuming forEach on a range is identical in cost to a for loop
- Not knowing generic Iterable<Int> boxes
- Treating list.indices iteration as O(1) per access on a LinkedList