Skip to content

ssa: isolate index range panic path - #2344

Merged
xushiwei merged 2 commits into
xgo-dev:mainfrom
visualfc:codex/check-index-range-fast-path
Aug 18, 2026
Merged

ssa: isolate index range panic path#2344
xushiwei merged 2 commits into
xgo-dev:mainfrom
visualfc:codex/check-index-range-fast-path

Conversation

@visualfc

@visualfc visualfc commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

Summary

Move index bounds panics off the normal execution path and lower them through dedicated signed and unsigned runtime
helpers.

Previously, every checked index operation called:

CheckIndexRange(outOfRange, index, signed, length)

even when the index was valid. The helper carried a dynamic condition and a signedness flag into the runtime.

This change branches on the bounds-check result in generated code and calls a runtime panic helper only from the
failure path:

func PanicIndex(x int, y int)
func PanicIndexU(x uint, y int)

This matches Go's runtime.panicIndex / runtime.panicIndexU lowering model.

Motivation

The previous lowering kept a runtime call on the successful index path:

%out_of_range = ...
call void @runtime.CheckIndexRange(
i1 %out_of_range,
i64 %index,
i1 %signed,
i64 %length
)

The runtime call received information that was already known by the compiler:

  • The bounds-check condition
  • Whether the index was signed
  • A constant true once the failure path was isolated

This unnecessarily complicated the runtime interface and the generated fast path.

Implementation

Index lowering now:

  1. Computes the existing out-of-range condition.
  2. Branches to a dedicated failure block.
  3. Selects the panic helper at compile time based on index signedness.
  4. Calls PanicIndex or PanicIndexU only from the failure block.
  5. Continues directly to the index operation on the successful path.

Signed indexes are lowered to:

%out_of_range = ...
br i1 %out_of_range, label %panic, label %continue

panic:
call void @runtime.PanicIndex(i64 %index, i64 %length)
br label %continue

Unsigned indexes use:

call void @runtime.PanicIndexU(i64 %index, i64 %length)

The actual argument width follows the target architecture through int and uint.

Runtime Changes

Remove the obsolete runtime helper:

func CheckIndexRange(
outOfRange bool,
index int64,
signed bool,
length int,
)

Index bounds failures now use the existing interfaces:

func PanicIndex(x int, y int)
func PanicIndexU(x uint, y int)

This removes the runtime condition and signedness parameters while preserving signed and unsigned bounds-error
formatting.

Source Line Information

The panic block intentionally retains a continuation edge instead of ending immediately with unreachable.

Although PanicIndex and PanicIndexU do not return at runtime, placing unreachable directly after the call caused
LLVM's return-address line information to resolve to the following source line. This broke recovered bounds-panic
stack traces and TestRuntimeStatementLineInfo.

Keeping a branch after the panic call gives the return address an instruction with the correct debug location. Runtime
behavior is unchanged because the panic helpers never return.

Bounds-Check Modes

The existing -B behavior is preserved:

  • Normal builds emit the conditional panic path.
  • Builds with bounds checks disabled emit neither PanicIndex nor PanicIndexU.
  • Mandatory checks unrelated to index bounds, such as nil dereference and unsafe builtin checks, remain unchanged.

Test Coverage

LLVM IR fixtures were updated to verify:

  • The bounds predicate branches to a dedicated failure block.
  • Signed indexes call PanicIndex.
  • Unsigned indexes call PanicIndexU.
  • Both helpers use the two-argument runtime interface.
  • The failure block retains the expected continuation branch.
  • Constant and dynamic indexes preserve the correct signedness.
  • Array, slice, string, generic, reflect, runtime, libc, and libgo indexing paths use the new lowering.
  • Disabled bounds checks do not emit either panic helper.
  • Recovered panic stacks report the original bounds-expression line.
 核心测试形式:

  for i := 0; i < b.N; i++ {
      sum += values[indexes[i&(datasetSize-1)]]
  }

  同时覆盖动态切片读、动态切片写和动态字符串读。输入在计时前构造,结果写入全局 sink,防止访问被删除。

  测试环境:

  - Apple M5,darwin/arm64
  - 基线:upstream/main (7f954d618)
  - PR:d3fa29e51
  - -benchtime=500ms -count=10
  - LLGO_BUILD_CACHE=off
  - 表中取 10 轮中位数,数值越低越好

   Benchmark            upstream/main             PR      变化     加速
  ━━━━━━━━━━━━━━━━━━━  ━━━━━━━━━━━━━━━  ━━━━━━━━━━━━━  
   DynamicSliceRead       0.938 ns/op    0.337 ns/op    -64.1%    2.79x
  ───────────────────  ───────────────  ─────────────  
   DynamicSliceWrite      0.926 ns/op    0.309 ns/op    -66.6%    3.00x
  ───────────────────  ───────────────  ─────────────  
   DynamicStringRead      0.927 ns/op    0.327 ns/op    -64.8%    2.84x

  这个 microbenchmark 是动态索引密集场景,每轮包含索引表访问和目标对象访问,因此反映的是边界检查热路径优化效果,不应直接等同于整个应用的性能提升。

@visualfc
visualfc force-pushed the codex/check-index-range-fast-path branch from 49a9eab to d3fa29e Compare August 17, 2026 02:47
@github-actions

github-actions Bot commented Aug 17, 2026

Copy link
Copy Markdown

LLGo baseline benchmarks

cb5d70581ad5 | workflow run | long-term charts

Program measurements

Platform Workload File size vs base Build vs base Run vs base
Linux cprintf 19368 B -16 B / -0.1% (better) 330.009 ms +2.673 ms / +0.8% (worse) 1.255 ms -55.08 us / -4.2% (better)
Linux fmtprintf 1869816 B -10960 B / -0.6% (better) 2.618 s -37.13 ms / -1.4% (better) 3.226 ms -16.88 us / -0.5% (better)
Linux println 68776 B -104 B / -0.2% (better) 330.656 ms -4.167 ms / -1.2% (better) 1.577 ms +15.75 us / +1.0% (worse)
macOS cprintf 84672 B 0 B / +0.0% 606.562 ms +217.7 ms / +56.0% (worse) 4.939 ms +2.29 ms / +86.5% (worse)
macOS fmtprintf 1892448 B +240 B / +0.01268% (worse) 2.546 s +6.7 ms / +0.3% (worse) 13.049 ms -38.92 us / -0.3% (better)
macOS println 121360 B -16 B / -0.01318% (better) 610.538 ms +257.3 ms / +72.8% (worse) 5.873 ms +2.01 ms / +52.0% (worse)
Core language and compiler benchmarks
Platform Benchmark ns/op vs base
Linux BenchmarkLookupPCRandom 13.260 ns/op +0.05 ns/op / +0.4% (worse)
Linux BenchmarkMergeCompilerFlags 151.200 ns/op +0.8 ns/op / +0.5% (worse)
Linux BenchmarkMergeLinkerFlags 94.460 ns/op -0.09 ns/op / -0.1% (better)
Linux BenchmarkChannelBuffered 36.020 ns/op +2.37 ns/op / +7.0% (worse)
Linux BenchmarkChannelHandoff 27817 ns/op +1163 ns/op / +4.4% (worse)
Linux BenchmarkDefer 44.950 ns/op +1.51 ns/op / +3.5% (worse)
Linux BenchmarkDirectCall 1.562 ns/op +0.005 ns/op / +0.3% (worse)
Linux BenchmarkGlobalRead 1.869 ns/op +0.312 ns/op / +20.0% (worse)
Linux BenchmarkGlobalWrite 2.489 ns/op +0.009 ns/op / +0.4% (worse)
Linux BenchmarkGoroutine 44365 ns/op +679 ns/op / +1.6% (worse)
Linux BenchmarkInterfaceCall 8.241 ns/op +0.138 ns/op / +1.7% (worse)
Linux BenchmarkRuntimeGetG 1.869 ns/op 0 ns/op / +0.0%
macOS BenchmarkLookupPCRandom 12.620 ns/op -0.4 ns/op / -3.1% (better)
macOS BenchmarkMergeCompilerFlags 136 ns/op +22.7 ns/op / +20.0% (worse)
macOS BenchmarkMergeLinkerFlags 78.820 ns/op +4.71 ns/op / +6.4% (worse)
macOS BenchmarkChannelBuffered 27.020 ns/op +3.24 ns/op / +13.6% (worse)
macOS BenchmarkChannelHandoff 6883 ns/op -2934 ns/op / -29.9% (better)
macOS BenchmarkDefer 29.250 ns/op -2.35 ns/op / -7.4% (better)
macOS BenchmarkDirectCall 1.027 ns/op -0.101 ns/op / -9.0% (better)
macOS BenchmarkGlobalRead 1.117 ns/op +0.03 ns/op / +2.8% (worse)
macOS BenchmarkGlobalWrite 1.011 ns/op -0.166 ns/op / -14.1% (better)
macOS BenchmarkGoroutine 32664 ns/op +649 ns/op / +2.0% (worse)
macOS BenchmarkInterfaceCall 4.723 ns/op -1.464 ns/op / -23.7% (better)
macOS BenchmarkRuntimeGetG 2.021 ns/op -0.519 ns/op / -20.4% (better)

Compared with 95b3ff5d135e measured in the same runner job.

@fennoai fennoai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: isolate CheckIndexRange in an unreachable failing branch

The core change in ssa/datastruct.go swaps the always-emitted InlineCall(CheckIndexRange, check, ...) for a hoisted IfThen(check, { Call(CheckIndexRange, true, ...); Unreachable() }). This is correct and a genuine fast-path win:

  • Semantics preserved — the panic predicate (check = signed idx < 0 OR unsigned idx >= max) is unchanged. The branch is taken on exactly the same out-of-bounds set as before, and inside it CheckIndexRange(true, ...) always panics, so no out-of-bounds index can slip through.
  • Unreachable is soundpanicBounds never returns, and IfThen (ssa/stmt_builder.go:230) detects the unreachable terminator and correctly suppresses the jump to the merge block, so no dead fallthrough is emitted.
  • PerformanceInlineCall does not actually inline (it's b.Call with a TODO), so the old code emitted an unconditional runtime call on every indexed access. The new form keeps the call off the in-bounds path and gives LLVM a clean cold/no-return branch to sink.
  • The updated test assertion accurately pins the intended br/call/unreachable IR shape.

All findings below are minor/advisory; none are blocking.

Comment thread ssa/datastruct.go Outdated
Comment thread ssa/datastruct.go Outdated
Comment thread ssa/bounds_checks_test.go Outdated
@visualfc
visualfc force-pushed the codex/check-index-range-fast-path branch from d3fa29e to 7d6fc56 Compare August 17, 2026 03:47
@codecov

codecov Bot commented Aug 17, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@visualfc
visualfc force-pushed the codex/check-index-range-fast-path branch from 7d6fc56 to 57aabe2 Compare August 17, 2026 04:41
@visualfc
visualfc force-pushed the codex/check-index-range-fast-path branch 6 times, most recently from 14c0712 to 5a389a9 Compare August 18, 2026 06:58
@visualfc
visualfc force-pushed the codex/check-index-range-fast-path branch 3 times, most recently from c2f26a7 to 95149d1 Compare August 18, 2026 08:43
@visualfc
visualfc force-pushed the codex/check-index-range-fast-path branch from 95149d1 to cb5d705 Compare August 18, 2026 08:51
@xushiwei
xushiwei merged commit 45c5e91 into xgo-dev:main Aug 18, 2026
42 checks passed
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.

3 participants