Compiler Intrinsics
A compiler intrinsic is a standard-library function that the Go
gccompiler does not call but instead recognizes by name and replaces in place with a small sequence of SSA operations — typically a single hardware instruction. Themath/bitspopulation-count functionbits.OnesCount64, for example, is not compiled into aCALL; on anamd64CPU with thePOPCNTfeature it becomes onePOPCNTQinstruction inlined directly into the caller. The mechanism lives incmd/compile/internal/ssagen/intrinsics.go, which maintains a table mapping(architecture, package, function name)to a builder that emits the replacement SSA (intrinsics.go). Intrinsics are how Go gets access to CPU instructions — atomic compare-and-swap, byte-swap, square-root, count-trailing-zeros — that have no Go-language syntax, without the overhead of a real function call.
Mental Model
Intrinsics solve a tension. Go has no language syntax for “atomic compare-and-swap” or “count leading zeros” — these are CPU instructions, not operators. The portable solution is to expose them as ordinary functions in packages like sync/atomic, math/bits, and math. But an ordinary function call has overhead — argument marshalling, a CALL, a RET — that dwarfs the one-instruction operation it wraps.
The intrinsic mechanism resolves this: the function exists as real Go (so gccgo, TinyGo, an architecture without the instruction, and go doc all work), but the gc compiler special-cases it. When gc sees a call to a known intrinsic on an architecture that supports it, it discards the call and substitutes the operation directly.
flowchart TD A["call bits.OnesCount64(x)"] --> B{Is package.function in the<br/>intrinsicBuilders table<br/>for this GOARCH?} B -->|"Yes — amd64 has POPCNT"| C[Run the builder:<br/>emit SSA OpPopCount64] C --> D[SSA op lowers to POPCNTQ —<br/>no CALL, no frame] B -->|"No — arch lacks the instruction"| E[Ordinary call to the<br/>pure-Go implementation in math/bits] E --> F[Function executes the<br/>fallback Go algorithm]
Figure: the intrinsic dispatch. Insight: the same math/bits source serves both paths — the pure-Go body is the correctness reference and the fallback; the intrinsic is a compiler-side shortcut that simply never reaches that body.
Mechanical Walk-through
The intrinsic table
intrinsics.go (as of Go 1.26.3) builds a package-level intrinsics map of type intrinsicBuilders, keyed by an intrinsicKey struct with three fields (intrinsics.go @ go1.26):
- arch — a
*sys.Arch(one ofsys.AMD64,sys.I386,sys.ARM,sys.ARM64,sys.Loong64,sys.MIPS,sys.MIPS64,sys.PPC64,sys.RISCV64,sys.S390X,sys.Wasm). - pkg — the import path of the function’s package (
"math/bits","internal/runtime/atomic","internal/runtime/sys","internal/runtime/math","math","runtime"). - fn — the function name (
"OnesCount64","TrailingZeros32","Sqrt").
Note that encoding/binary is not in the intrinsic table — binary.LittleEndian.Uint64 and friends are made fast by inlining and ordinary load-coalescing in SSA, not by an intrinsic lowering. The covered packages are exactly the runtime-adjacent and numeric ones above.
The table is populated at compiler-init time by helper functions:
add(pkg, fn, builder, archs ...*sys.Arch)— registers a builder for one named architecture (or a small list).addF(pkg, fn, builder, archs ...*sys.Arch)— same signature, but conventionally used to register a family of architectures in a single call (theFis for “family”); bothaddandaddFare present in the current file and the distinction is largely organizational.alias(pkg, fn, targetPkg, targetFn, archs ...*sys.Arch)— points one(pkg, fn)at an already-registered intrinsic, used heavily so thatsync/atomic.LoadInt32aliasesinternal/runtime/atomic.Load,sync/atomic.CompareAndSwapInt64aliasesinternal/runtime/atomic.Cas64, and so on across the full atomic surface.
The builder
The value in the table is an intrinsicBuilder — a function func(s *state, n *ir.CallExpr, args []*ssa.Value) *ssa.Value. When the SSA-generation phase (ssagen) encounters a call expression, it looks up (GOARCH, package, function). On a hit, it does not generate the call. Instead it:
- Evaluates the call’s arguments into SSA values (
args). - Invokes the builder with those SSA values.
- The builder emits one or a few SSA ops — e.g.
s.newValue1(ssa.OpPopCount64, types.Types[types.TINT], args[0])— and returns the resulting SSA value. - That returned value is the call’s result; standard call-generation is skipped entirely.
The emitted SSA op then flows through the normal backend: the lower pass turns the generic OpPopCount64 into the architecture-specific machine op (OpAMD64POPCNTQ), and code generation emits the actual POPCNTQ instruction. Because the result is an ordinary SSA value, it participates in constant folding, common-subexpression elimination, and register allocation like any other operation.
What is intrinsified
The principal intrinsified domains, per the Go 1.26 intrinsics.go:
math/bits—OnesCount*(population count →POPCNT/POPCNTQ),TrailingZeros*(→BSF/TZCNT),LeadingZeros*(handled implicitly throughLen*),RotateLeft*,ReverseBytes*(→BSWAP),Len*, plus the multi-word arithmetic helpersAdd64,Sub64,Mul64, and (AMD64-only)Div64. The package documentation states outright that “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” (pkg.go.dev/math/bits).sync/atomicandinternal/runtime/atomic—Load,Store,Swap,CompareAndSwap,Add, in their typed widths, mapped to the architecture’s atomic instructions (LOCK XADD,CMPXCHGonamd64;LDAR/STLRand theLDADDAL/SWPALfamily onarm64). Thesync/atomicsymbols are registered asaliases ofinternal/runtime/atomicentries, so the user-facing API and the runtime-internal API share a single intrinsic lowering.math—Sqrt(→SQRTSD),Abs,Floor,Ceil,Trunc,Round,RoundToEven,FMA(fused multiply-add). The rounding family is unconditional onarm64,ppc64,s390x, andwasmbut onamd64is gated throughmakeRoundAMD64on anSSE4.1runtime-feature check (the SSE4.1ROUNDSDinstruction is the lowering);FMAis unconditional onarm64/loong64/ppc64/riscv64/s390xbut onamd64andarmis gated on a runtime CPU-feature check (FMAonamd64,VFPv4onarm) with a fallback to the pure-Go body when absent.runtime,internal/runtime/sys,internal/runtime/math— low-level helpers:runtime.KeepAlive,runtime.memequal(intrinsified at least onarm64via theOpMemEqSSA op),getcallerpc,getcallersp, multi-word multiplication helpers, and the cycle-counter helpers ininternal/runtime/sys.
The fallback
Crucially, every intrinsified function still has a real, pure-Go body. If the target architecture lacks the instruction — or the compiler is gccgo/TinyGo, which do not implement Go’s intrinsic table — the ordinary function is compiled and called. math/bits.OnesCount64’s fallback is the classic SWAR (SIMD-within-a-register) bit-twiddling algorithm. This is why intrinsics never compromise portability: the worst case is a normal function call running a correct, if slower, algorithm.
Runtime feature gating versus baseline assumption
A second layer of conditionality sits inside the intrinsic. For some (arch, instruction) pairs the compiler emits not a bare instruction but a feature-test branch that checks internal/cpu’s capability flags at process startup and falls back to the pure-Go body when absent. The principal gated cases in the Go 1.26 table are:
amd64math/bits.OnesCount*— gated oninternal/cpu.X86.HasPOPCNT.POPCNTpost-dates the original SSE2-eraamd64baseline; a binary built with the defaultGOAMD64=v1must work on CPUs without it. Building withGOAMD64=v2or higher raises the baseline and (in practice) avoids the branch because the feature is statically known to be present.amd64math.Floor/Ceil/Trunc/Round/RoundToEven— gated onSSE4.1(theROUNDSDinstruction), viamakeRoundAMD64.amd64math.FMA— gated on theFMACPUID bit;armFMAis gated onVFPv4.loong64andriscv64OnesCount*/TrailingZeros*/RotateLeft*/Len*— gated on the LSX (Loong64) and Zbb (RISC-V) extensions, with the RISC-VRotateLeft/Len/TrailingZeroslowering also conditional ongoriscv64 >= 22(theGORISCV64profile —rva22enables Zbb in the baseline).ppc64ReverseBytes*— has a Power10 (BREVinstruction) fast path with fallback for older Power CPUs.
Other architectures (arm64, s390x, wasm) emit the instruction unconditionally because their Go baselines already mandate it. The matrix changes every release; consult intrinsics.go at the exact release tag for the version-specific list.
Reading the diagnostics
Intrinsics do not appear in -gcflags=-m (that flag is escape analysis and inlining). Two ways to see them:
# 1. Dump the assembly and look for the instruction, not a CALL.
go build -gcflags='-S' ./... 2>&1 | grep -A2 OnesCount
# you should see POPCNTQ, not CALL math/bits.OnesCount64
# 2. Disassemble the built binary.
go tool objdump -s 'main.popcount' ./yourbinaryThere is also a dedicated debug flag:
go build -gcflags='-d=intrinsics=1' ./...-d=intrinsics=1 makes the compiler print a line each time it applies an intrinsic, e.g. intrinsic substitution for OnesCount64. If you expect an intrinsic and this line does not appear, the call is going through the slow path — usually because the architecture is unsupported or the function is not (yet) in the table.
For a quick sanity check, compile this and inspect the assembly:
package main
import "math/bits"
func popcount(x uint64) int { return bits.OnesCount64(x) } // line 5go build -gcflags='-S' on amd64 shows the body of popcount as essentially a single POPCNTQ plus the return — no CALL. Mark it //go:noinline and you still see POPCNTQ, confirming the substitution is the intrinsic, not inlining of the math/bits body.
Failure Modes and Common Misunderstandings
- “Intrinsics are the same as inlining.” No. Function Inlining copies a function’s Go body into the caller. An intrinsic discards the body and substitutes hand-written SSA. A function can be both inlined and intrinsified, or intrinsified on one arch and merely inlined on another.
- “
POPCNTis always emitted on amd64.” Not at the defaultGOAMD64=v1baseline. The compiler emits a runtime feature-test branch (internal/cpu.X86.HasPOPCNT) and dispatches between the barePOPCNTQinstruction and the pure-Go SWAR fallback. Building withGOAMD64=v2(or higher) raises the baseline so the branch can be eliminated. The same shape of gate applies toSSE4.1-onlymath.Round*, toFMAonamd64/arm, and to the Zbb-gated bit ops onriscv64. - “My helper does the same bit-twiddling, so it’s just as fast.” Hand-written SWAR code is not an intrinsic — the compiler does not pattern-match arbitrary bit-twiddling back to
POPCNT. Callmath/bits.OnesCount64to get the instruction; a manual loop will not. gccgo/TinyGo. These toolchains do not sharegc’s intrinsic table. Code that relies onbits.OnesCountbeing one instruction for performance will be a real call there.- Intrinsics and the race detector. Atomic intrinsics interact with the race detector’s instrumentation; under
-racesome atomic operations are routed through instrumented runtime calls rather than the bare intrinsic so the detector can observe them.
Alternatives and When to Choose Them
- Plan 9 assembly (Go Plan 9 Assembly). Before a function is intrinsified, or for instructions the compiler will never expose, the escape hatch is a hand-written
.sfile. This is howmath/big,crypto, and parts ofruntimereach SIMD and carry-flag operations. The cost is one assembly file per architecture and no inlining across the boundary. cgoto a C intrinsic. Calling a C function that uses a compiler intrinsic. Almost never worth it — the cgo call overhead is far larger than the instruction it would reach.- Just use the standard library. The right default.
math/bits,sync/atomic, andmathare intrinsified precisely so that ordinary, portable Go gets hardware speed for free. Reaching for assembly orcgoto do what an intrinsic already does is a regression.
Production Notes
The practical rule is: prefer the standard-library function over a hand-rolled equivalent, because the standard-library version is the one the compiler knows how to intrinsify. Daniel Lemire’s and Dave Cheney’s writeups on Go bit-manipulation both make the same point — math/bits is fast not because the Go fallback is clever but because the compiler replaces it (dave.cheney.net). The math/bits package itself was added in Go 1.9 specifically to give the compiler a stable surface to intrinsify (issue #18616); before it, programmers wrote their own slower bit tricks. When micro-optimizing a hot path, check go build -gcflags=-S to confirm the instruction you expect is actually emitted — an unexpected CALL to math/bits is a sign your target architecture is not covered, and the fix is rarely to write assembly but to confirm the build’s GOARCH/GOAMD64 settings.
See Also
- Function Inlining — a related but distinct call-elimination optimization
- Static Single Assignment Form — the IR intrinsic builders emit into
- Atomic Operations in Go —
sync/atomicis heavily intrinsified - Bit Manipulation Tricks — the algorithms
math/bitsintrinsics replace - Go Plan 9 Assembly — the escape hatch when no intrinsic exists
- The Go Assembler — how the emitted instructions are encoded
- Compiler Optimization Diagnostics —
-gcflags=-S,-d=intrinsics - Go Internals MOC — parent map