| name | go-perf-compiler-intrinsics |
| description | Guides Go compiler intrinsics and microarchitecture tuning — the math/bits functions (OnesCount/popcount, LeadingZeros, TrailingZeros, RotateLeft, Mul64) the compiler lowers to single CPU instructions like POPCNT, the GOAMD64 v1-v4 and GOARM64 levels, the Go 1.17+ register calling convention, atomic intrinsics, and interface devirtualization. Fires on "popcount in Go", "use hardware instructions", "GOAMD64 levels", "reduce call overhead". Routes SIMD to go-perf-simd, PGO devirtualization to go-perf-pgo. |
| allowed-tools | Read, Glob, Grep |
| compatibility | Claude Code, Codex CLI, Gemini CLI |
Go Compiler Intrinsics & Microarchitecture Tuning
An intrinsic is a stdlib function the compiler recognizes and replaces with inline machine
instructions instead of a real call. math/bits says so: "Functions in this package may be implemented
directly by the compiler, for better performance. For those functions the code in this package will not
be used. Which functions are implemented by the compiler depends on the architecture and the Go
release" (math/bits). The authoritative list lives in the compiler at
src/cmd/compile/internal/ssagen/intrinsics.go — every claim here is checked against it (Go 1.26).
This skill is for the proven-hot inner loop, after the algorithm is right — reach for it last, per
go-perf-methodology. Wider data-parallel vectorization → go-perf-simd; profile-guided
devirtualization → go-perf-pgo. All Go below builds clean under gofmt/go vet on Go 1.26.
1. math/bits — One Function, One Instruction
math/bits "implements bit counting and manipulation functions for the predeclared unsigned integer
types" (math/bits). On amd64/arm64 the hot ones lower to a single
instruction — no call, no loop. Confirmed in intrinsics.go:
math/bits function | SSA op | Typical amd64 instr | Source (intrinsics.go) |
|---|
OnesCount{,8,16,32,64} (popcount) | OpPopCount* | POPCNT | L1202-1256 |
TrailingZeros{,8,16,32,64} | OpCtz* | TZCNT/BSF | L934-985 |
LeadingZeros{,8,16,32,64} | via Len | LZCNT/BSR | L1057 ("trivially calls Len") |
Len{,8,16,32,64} | OpBitLen* | BSR+adjust | L1010-1055 |
RotateLeft{,8,16,32,64} | OpRotateLeft* | ROL | L1083-1103 |
Reverse{,8,16,32,64} | OpBitRev* | RBIT (arm64) | L1058-1081 |
ReverseBytes{,16,32,64} | OpBswap* | BSWAP | L986-1008 |
So the rule: never hand-roll these. A shift-and-mask loop to count set bits is slower and less
readable than bits.OnesCount64(x), one POPCNT. Likewise bits.TrailingZeros64 for "lowest set
bit," bits.Len64(x) for ⌊log2 x⌋+1, and bits.RotateLeft64(x, k) for a rotate (x<<k | x>>(64-k)
is not a rotate — UB-adjacent for k==0/k>=64 and may not pattern-match). Bit-math correctness →
go-naming-and-style.
2. Wide Arithmetic — Mul64, Add64, Sub64, Div64
The full-width and carry-aware primitives are intrinsics too, giving the CPU's native 128-bit result
that plain Go operators discard:
bits.Mul64(x, y) (hi, lo uint64) → OpMul64uhilo, one MULQ for the full 128-bit product (L1258);
plain x*y keeps only the low 64 bits.
bits.Add64(x, y, carry) → OpAdd64carry (ADC, L1264); bits.Sub64 → OpSub64borrow (SBB,
L1271) — these chain into multi-word (bignum) add/subtract without branching on the carry.
bits.Div64(hi, lo, y) → a DIVQ-backed op that "check[s] for divide-by-zero/overflow and panic[s]"
(L1277, amd64).
math/big itself multiplies this way — intrinsics.go aliases math/big.mulWW to bits.Mul64
(L1348). For 128-bit hashing, checksums, or fixed-point scaling these beat manual hi/lo splitting.
3. GOAMD64 v1–v4 — Raising the Instruction Floor
The compiler emits an intrinsic only if the target is guaranteed to have the instruction. GOAMD64
(Go 1.18+) picks the microarchitecture floor (MinimumRequirements):
| Level | Adds over previous (selected) | Default? |
|---|
v1 | baseline; every 64-bit x86 CPU | yes |
v2 | POPCNT, SSE3, SSE4.1/4.2, SSSE3, CMPXCHG16B | |
v3 | AVX, AVX2, BMI1, BMI2, LZCNT, MOVBE, FMA, F16C | |
v4 | AVX512F/BW/CD/DQ/VL | |
The concrete payoff is visible in the source. bits.OnesCount64 at the default v1 does not
emit a bare POPCNT — because a baseline x86-64 CPU might lack it, the compiler emits a runtime
feature check branching on X86HasPOPCNT, falling back to the pure-Go loop (intrinsics.go L1107-1135).
At GOAMD64=v2 or higher the same call compiles to a single unconditional POPCNT (if cfg.goamd64 >= 2,
L1107). math.FMA is the same story keyed to v3 (L786). So for a fleet you know is Haswell-or-newer,
GOAMD64=v3 go build removes those runtime checks and raises the autovectorization/BMI floor for the
whole binary — a free, deployment-wide win when you control the hardware. The cost: the binary
SIGILLs on older CPUs, so only set it when the deployment target is known.
4. GOARM64 / GOARM / GORISCV64 — the Non-amd64 Equivalents
Same idea, different knobs (MinimumRequirements):
GOARM64 (Go 1.23+): v8.{0-9} / v9.{0-5}, default v8.0, plus options ,lse and
,crypto. arm64 intrinsifies the same math/bits set unconditionally (RBIT/CLZ are in the base
ISA), and ,lse lets atomics (§6) use single-instruction LDADD/SWP/CAS instead of a
load-linked/store-conditional loop — gated on cfg.goarm64.LSE (intrinsics.go L403).
GORISCV64 (rv64g baseline): the Zbb extension (goriscv64 >= 22) unlocks the direct
POPCNT/CTZ lowering, else a feature-check fallback (intrinsics.go L1170, L220). GOARM:
5/6/7 (soft float / VFPv1 / VFPv3) for 32-bit ARM.
5. The Register-Based Calling Convention (Go 1.17+)
Before Go 1.17 every argument and result went on the stack. Since 1.17 they go in registers: the
change "has shown about a 5% performance improvement in Go programs and reduction in binary sizes of
around 2% for amd64 platforms" (Go 1.17). Why it matters for this skill:
- Small functions are cheap now. A leaf helper taking a few words no longer pays stack
stores/loads on entry/exit, so the call overhead a register ABI removes is most of what a tiny
function cost. This is why the intrinsics above are worth so little to inline-by-hand — the call
they replace is already nearly free, and inlining (
go-perf-inlining) erases what remains.
- Don't reason about call cost with a pre-1.17 mental model. Stack-spilling args on every call is
no longer the default; budget call overhead as a handful of register moves, not a frame setup.
Hand-written assembly must opt in via
//go:registerparams
(Compiler Directives). The conclusion:
small, focused functions are the right default for performance too.
6. Atomic Intrinsics
sync/atomic's Load*, Store*, Swap*, CompareAndSwap*, Add*, and And*/Or* are intrinsics:
intrinsics.go aliases each to an internal/runtime/atomic op (L1299-1345) lowering to a
LOCK-prefixed instruction on amd64 (or LDADD/CAS on arm64 with ,lse, §4) — no call. Two notes:
- They are disabled under
-race ("Note: these are disabled by flag_race", L1298), so a race build
measures differently from a production build — don't benchmark atomics with -race on.
atomic.Int64/atomic.Pointer[T] methods compile down to these same ops, so the typed API costs
nothing over the raw functions.
This skill owns only that atomics are intrinsics (no call overhead). Atomic-vs-mutex cost →
go-perf-atomics-vs-locks; memory-ordering correctness → go-race-and-memory-model.
7. Devirtualization — Direct Calls from Interface Values
An interface method call is an indirect call through the itable; it also blocks inlining because
the compiler can't see the callee. When the concrete type is provable at compile time, the compiler
does static devirtualization: "Devirtualization converts indirect calls on interface values whose
type can be determined statically into direct calls to the concrete method (which often enables
inlining of the call)" (PGO blog).
What the profile adds is the probabilistic case: PGO "can replace r.Read(b) with" a type guard
(if f, ok := r.(*os.File); ok { f.Read(b) } else { r.Read(b) }) when profiling shows one concrete
type dominates — "Inlining and devirtualization are the two PGO-driven optimizations available in Go
1.21" (PGO blog). PGO collection and default.pgo lifecycle →
go-perf-pgo. Takeaway: a hot interface call you can make concrete is cheaper than one left
indirect.
8. When to Reach for These — and When Not
- Reach for
math/bits/wide-arithmetic intrinsics whenever the operation fits, hot path or not —
more readable than the hand-rolled equivalent and never slower; "use the fast thing" has no clarity
cost here.
- Reach for a
GOAMD64/GOARM64 level bump only when you control the deployment hardware and a
profile shows the code matters — a reversible, fleet-wide build flag.
- Do NOT hand-write assembly where an intrinsic exists (§1 covers most bit ops); verify the lowering
with
go build -gcflags=-S (look for POPCNT/TZCNT/ROL, not a CALL).
- Do NOT raise
GOAMD64 speculatively — a v3 binary SIGILLs on a v1 host; default v1 is the
safe floor. And measure first (go-perf-methodology): a POPCNT on a cold path moves nothing.
9. Don't
- Don't hand-roll popcount/clz/ctz/rotate.
bits.OnesCount64, bits.LeadingZeros64,
bits.TrailingZeros64, bits.RotateLeft64 lower to single instructions (math/bits; intrinsics.go).
- Don't write
x<<k | x>>(64-k) for a rotate — it misbehaves at k==0/k>=64 and may not match;
use bits.RotateLeft64.
- Don't assume
bits.OnesCount64 is a bare POPCNT at the default GOAMD64=v1 — it is a
feature-checked branch until v2+ (intrinsics.go L1107).
- Don't set
GOAMD64=v3/v4 for binaries that may run on unknown CPUs — they crash with an illegal
instruction on older hardware (MinimumRequirements).
- Don't reason about call cost with a pre-1.17 stack-ABI model — args/results are in registers now,
~5% faster calls (Go 1.17).
- Don't benchmark atomics under
-race — atomic intrinsics are disabled in race builds
(intrinsics.go L1298).
- Don't keep a hot call interface-typed when the concrete type is knowable — it blocks
devirtualization and inlining (PGO blog).
- Don't reach here before the algorithm and profile justify it (
go-perf-methodology).
10. Routing to Related Skills
This plugin (go-perf-* depth):
go-perf-methodology — the measure-first gate; intrinsics are a last, proven-hot-loop lever.
go-perf-inlining — mid-stack inlining and the budget; why small register-ABI calls are cheap to inline.
go-perf-simd — wide data-parallel vectorization (the experimental simd package, Plan 9 assembly).
go-perf-pgo — collecting default.pgo; PGO-driven (probabilistic) devirtualization.
go-perf-bounds-check-elimination — the other "single-instruction loop" lever, paired with §1.
go-perf-atomics-vs-locks — the cost tradeoff of the atomic intrinsics in §6.
Marketplace (go-* correctness):
go-naming-and-style / go-performance — bit-twiddling correctness and idiom.
go-race-and-memory-model — happens-before/ordering correctness of atomics.
go-version-feature-map — version floors: register ABI 1.17, GOAMD64 1.18, GOARM64 1.23.
11. Reference Files
Intrinsics/microarchitecture anti-patterns in LLM-generated Go, with wrong/right contrast and citations:
${CLAUDE_SKILL_DIR}/references/common-mistakes.md
Source provenance for every claim in this skill:
${CLAUDE_SKILL_DIR}/references/sources.yaml