# cpuid: CPU feature detection for Go, without cgo

> The klauspost/cpuid library detects the features of the CPU running a Go program once at startup and keeps the result for fast access, covering x86, amd64, arm64 and riscv64 with no cgo and no external C code. It is MIT licensed, the current release is v2.4.0 from 2026-06-30, and the last push was on 2026-10-16.

**klauspost/cpuid** — CPU feature identification for Go

- Repository: https://github.com/klauspost/cpuid
- Stars: 1,220 · Forks: 150
- Language: Go
- License: MIT
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/klauspost-cpuid

## Detect once at startup, then read a struct field

The cpuid package provides information about the CPU running the current program, and its central design decision is when that information is gathered.

CPU features are detected on startup and kept for fast access through the life of the application. That is the whole architecture, and it is a sensible one for a library that will be consulted from hot paths. The alternative, asking the operating system on each call, is either slow or involves a syscall per query, and in Go a syscall in a hot loop is expensive enough to matter.

The access pattern is a shared variable, `cpuid.CPU`, which holds the detected information. The example program reads from it directly:

```go
fmt.Println("Name:", CPU.BrandName)
fmt.Println("PhysicalCores:", CPU.PhysicalCores)
fmt.Println("ThreadsPerCore:", CPU.ThreadsPerCore)
fmt.Println("LogicalCores:", CPU.LogicalCores)
fmt.Println("Family", CPU.Family, "Model:", CPU.Model, "Vendor ID:", CPU.VendorID)
fmt.Println("Features:", strings.Join(CPU.FeatureSet(), ","))
fmt.Println("Cacheline bytes:", CPU.CacheLine)
```

So a program can ask what the brand name is, how many physical cores there are and how many threads per core, the family and model and vendor, the full feature set, the cache line size, the L1 data, L1 instruction, L2 and L3 cache sizes, and the frequency in hertz.

The architecture coverage is what makes this useful beyond a single platform. x86 and x64, meaning AMD64 and i386, ARM with ARM64, and RISC-V with RV64 are supported, and no external C code is used, which should make the library very easy to use. The no-cgo property is not a small detail in Go. Cgo has consequences for build times, for cross compilation, for static linking and for whether the binary can be built in a restricted environment, and a pure Go library sidesteps all of them.

## Has is the fast path and Supports is the flexible one

The usage section draws a distinction that matters for anyone writing a hot loop, and it is the difference between two methods on the same object.

`cpuid.CPU.Supports()` is the general check for CPU features. A faster `cpuid.CPU.Has()` is provided which will usually be inlined by the gc compiler. So `Has` is the one to use in code that runs often, and `Supports` is the one to use when you are checking a list of features in a less critical place.

There is a third form for the case where you need to test many features at once. They can be combined using `f := CombineFeatures(CMOV, CMPXCHG8, X87, FXSR, MMX, SYSCALL, SSE, SSE2)`, and this can be used with `cpuid.CPU.HasAll(f)` to quickly test if all features are supported.

That is the right shape for the kind of code these libraries exist to serve. A compression or encryption library that has an AVX2 path and a generic path does not want to call a function per feature check in the middle of a buffer, it wants to compute a bitmask once and test it.

The README is also candid about the limits of detection. For some CPU and OS combinations some features will not be detected, and amd64 has rather good support and should work reliably on all platforms. That last sentence is the practical guidance, and it is a fair summary: this library is at its best on amd64 and approximate elsewhere.

One more caveat that catches people out on virtualised servers. Hypervisors may not pass through all CPU features through to the guest OS, so even if your host supports a feature it may not be visible on guests. A container that is told it has AVX-512 and then gets a core without it is one class of problem, and a hypervisor that masks a feature is another.

## ARM64 detection is partial, and there is an unsafe path you have to opt into

The ARM64 section is where the library is most candid about its own limits, and it is worth reading before assuming parity with x86.

Not all operating systems provide ARM features directly and there is no safe way to do so for the rest. Currently `arm64/linux` and `arm64/freebsd` should be quite reliable. `arm64/darwin` adds features expected from the M1 processor, but a lot remains undetected.

That is the honest position for macOS on Apple Silicon, and it is a meaningful limitation for anyone porting a library that dispatches on AVX2 or NEON, because a feature you cannot detect is a feature you cannot safely use.

There is an escape hatch, and it comes with a warning. A `DetectARM()` can be used if you are able to control your deployment, it will detect CPU features, but may crash if the OS doesn't intercept the calls. A `-cpu.arm` flag for detecting unsafe ARM features can be added.

The repository layout shows how that flag is implemented. There is `detect_arm64.go` at the top level, and the platform files `os_linux_arm64.go`, `os_darwin_arm64.go` and `os_other_arm64.go`, plus `os_safe_linux_arm64.go` and `os_unsafe_linux_arm64.go`, which are the safe and unsafe variants of the Linux implementation. So the unsafe detection path is a separate file that is only compiled in when the flag asks for it, which is a sane way to structure a feature that can crash.

One more limitation to note. On ARM, only features are detected, and no additional information is currently available. So on ARM64 you get a feature set and nothing else, where on x86 you also get the cache sizes, the core counts, the family and model and the brand name. A program that reads `CPU.Cache.L3` on an ARM machine is reading zero.

## Detection is an init-time side effect you can re-run after flag parsing

The flags section describes a small but real ordering constraint that anyone integrating the library into a program with its own flags needs to understand.

The `Flags()` command provides flags that affect CPU detection. It must be called before `flag.Parse()`, and after the flags have been parsed `Detect()` must be called. This means that any detection used in `init()` functions will not contain these flags.

That last sentence is the important one. The default behaviour is that detection happens during package initialisation, which happens before your `main` runs, which is before you can parse flags. If a user passes the flag that enables unsafe ARM detection, that flag arrives too late to affect the automatic detection, so the library gives you a manual `Detect()` call to redo the work with the new settings in place.

The example shows the pattern:

```go
func main() {
	cpuid.Flags()
	flag.Parse()
	cpuid.Detect()

	if cpuid.CPU.Supports(cpuid.SSE, cpuid.SSE2) {
		fmt.Println("We have Streaming SIMD 2 Extensions")
	}
}
```

The ordering constraint is a bit of a smell, and it is the kind of thing that bites when a library needs configuration that arrives at the command line. But the alternative, making detection lazy, would cost the startup-time property that makes the library fast, and re-detecting is cheap enough that anyone who needs the flag path can pay it.

The featureid_string.go file at the top level is what turns the numeric feature identifiers into the names you see in the output, the ADX and AESNI and AVX2 list, and that file is generated, which is why it is a separate file rather than a map in the main source.

## A command line tool with JSON output and a microarchitecture level check

The repository ships a `cmd/` directory, and the binary it builds is a diagnostic tool for exactly the information the library exposes.

Installing it from source is `go install github.com/klauspost/cpuid/v2/cmd/cpuid@latest`, and prebuilt binaries are on the release page. There is also a Homebrew route for macOS and Linux users with `brew install cpuid`.

Running it with no arguments prints the human-readable summary, which includes the brand name, the vendor string, the vendor ID, the physical cores, threads per core and logical cores, the CPU family and model, the full feature list, the microarchitecture level, the cache line bytes and the four cache sizes. The `--json` flag emits the same information as structured output, which is the form you want if you are collecting it from a fleet of machines.

The JSON structure is worth reading for what it exposes beyond the basics. Alongside the brand name, vendor, core counts, family, model, cache line and cache sizes, there is an `SGX` object with `Available`, `LaunchControl`, `SGX1Supported`, `SGX2Supported`, `MaxEnclaveSizeNot64`, `MaxEnclaveSize64` and `EPCSections`, and an `X64Level` field for the microarchitecture level.

The `--check-level` flag turns the tool into a pass or fail test, which is how you would use it in a deployment script:

```
λ cpuid --check-level=3
2022/03/18 17:04:40 Microarchitecture level 3 is supported. Max level is 3.
Exit Code 0

λ cpuid --check-level=4
2022/03/18 17:06:18 Microarchitecture level 4 not supported. Max level is 3.
Exit Code 1
```

The exit code is the interface. A script can require a minimum microarchitecture level and fail cleanly when the hardware does not meet it, which is more useful than parsing the human-readable output.

## The source layout is the portability story

The top-level file listing explains how the library covers three architectures without cgo, and it is a good example of how a Go project handles platform differences.

The assembly files `cpuid_386.s`, `cpuid_amd64.s` and `cpuid_arm64.s` are the per-architecture instruction execution, since the x86 CPUID instruction has to be executed from assembly. The detection logic in Go sits alongside them in `detect_x86.go`, `detect_arm64.go` and `detect_riscv64.go`, with `detect_ref.go` and `detect_ref_arm64.go` as the reference implementations, which is the pattern for testing a specific implementation against a portable one.

The operating system layer is the `os_` prefixed files, one per platform: `os_darwin_arm64.go`, `os_linux_arm64.go`, `os_linux_riscv64.go`, `os_other_arm64.go` and `os_other_riscv64.go`, with the safe and unsafe Linux ARM variants. `os_other_*` is the fallback for platforms the library does not know about, which is what lets a program that imports it still build on a platform nobody has tested.

Topology is a separate concern with its own directory, `topology/`, plus `topology.go`, `topology_scan.go`, `topology_scan_linux_amd64.go`, `topology_scan_stub.go` and `topology_scan_windows_amd64.go`. Core counts, threads per core and the cache hierarchy are not always in the same place on every platform, and on Linux they come from scanning `/proc` or the sysfs tree, which is why there is a separate scan implementation per operating system and a stub for the rest.

The testing setup is also visible. There is a `mockcpu_test.go`, which is how you test code that dispatches on CPU features without a machine that has them, a `testdata/` directory, and `test-architectures.sh` for running the suite against the other architectures, which is how the reference implementations earn their keep.

The module requires Go 1.24.0 and a single dependency, `golang.org/x/sys` at v0.41.0. A single dependency on the system calls package is about as small as a Go dependency list gets, and for a library that ends up in the dependency tree of compression and encryption code, that matters.

## Conclusion

cpuid fits a Go program that needs to pick a code path at runtime based on what the processor can do, especially one that must not depend on cgo, since the library is pure Go across all three architectures. It does not fit a program that needs to know precisely which instructions are present, since detection is best effort and a hypervisor may not pass features through to a guest. Before you rely on it, check the `Supports` and `Has` distinction in your hot path, and note that detection happens at startup, so a program that changes CPU affinity or runs under a nested virtualisation layer should test the result rather than assume it.

## FAQ

### How do I install the cpuid Go library?

Run `go get -u github.com/klauspost/cpuid/v2` using modules, and drop `v2` for other import paths. The command line tool is installed with `go install github.com/klauspost/cpuid/v2/cmd/cpuid@latest`, or with `brew install cpuid` on macOS and Linux.

### What is the difference between cpuid.CPU.Has and cpuid.CPU.Supports?

`Supports()` is the general feature check, while `Has()` is a faster variant that will usually be inlined by the gc compiler, which makes it the right choice in a hot path. For many features at once, combine them with `CombineFeatures` and test with `HasAll`.

### Is CPU feature detection reliable on ARM64?

Not fully. `arm64/linux` and `arm64/freebsd` should be quite reliable, while `arm64/darwin` adds the features expected from the M1 processor but leaves a lot undetected. A `DetectARM()` call can detect features where you control the deployment, but it may crash if the OS does not intercept the calls, and only features are detected on ARM with no additional information.

### How do I get cpuid to respect command line flags?

Call `cpuid.Flags()` before `flag.Parse()`, and call `cpuid.Detect()` after the flags have been parsed. Detection done inside `init()` functions happens before flags are parsed and so will not include them.

## Sources

- [Issues](https://github.com/klauspost/cpuid/issues)
- [klauspost/cpuid on GitHub](https://github.com/klauspost/cpuid)
- [License: MIT](https://github.com/klauspost/cpuid/blob/master/LICENSE)
- [README](https://github.com/klauspost/cpuid/blob/master/README.md)
- [Releases](https://github.com/klauspost/cpuid/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/klauspost-cpuid
