Skip to content

proposal: simd: CPU feature vet check under GOEXPERIMENT=simd #76175

@aclements

Description

@aclements

Proposal Details

Background

#73787 proposed a package of architecture-specific SIMD intrinsics, guarded by GOEXPERIMENT=simd for Go 1.26. Because these intrinsics are architecture-specific and tightly tied to underlying CPU instructions, they will panic if the necessary CPU features are not supported by the hardware. As a result, the user of the simd package has to be careful to perform the appropriate CPU feature checks before using operations.

However, the space of CPU features is complex. For example, Intel’s AVX-512 alone has 21 different feature flags. While each intrinsic documents which features it requires, keeping track of this in any non-trivial SIMD code is difficult and error-prone. Furthermore, even with complete test coverage, a missed feature check may not result in a panic in CI if the CI hardware happens to support that feature, and may only panic in production on different hardware.

Proposal

I propose that we add directive comments to mark CPU feature requirements and a vet check that uses these directives to statically verify that the necessary feature checks are performed.

The minimal proposal has just one directive, which can only be applied to a function or method:

//cpu:requires <feature> [&& <feature> ...]
func F(...) { ... }

This directive indicates that the caller of F must ensure all of the listed features are available by calling the appropriate feature check functions before calling F.

The feature names follow the naming convention of the feature check methods in the simd package: for example, X86.AVX or X86.AVX512.

We will annotate all functions and methods in the simd package with feature requirement directives, but the cpu:requires directive is not limited to the simd package.

We will add a “cpu” vet check that verifies all feature annotations are satisfied. It will work by performing a flow analysis of every function G that calls an annotated function F to check that every call to F is reachable only if the necessary feature check functions have returned true. If G itself has a feature annotation, then the flow analysis assumes that all listed features are available on entry to G.

Some CPU features imply other features. For example, a CPU can only support AVX2 if it also supports AVX. The vet check will understand this: if a function requires some feature, the vet check will assume that all implied features are also available within that function.

Function values, interfaces, and constraints

The vet check will ensure that capturing a function H as a function value meets H’s requirements at the point of capture. For example:

//cpu:requires AVX
func H() { ... }

var c1 func()

func I() {
	if !simd.X86.AVX() {
		return
	}
	// H can be captured as a function value here because
	// its requirements are satisfed.
	c1 = H
}

Unlike a function, there is no way to directly annotate the requirements of a function literal. Instead, the vet check will assume that any features guaranteed at the point where the function value is created are available within that function value’s body. For example:

var c2 func()

func J() {
	if !simd.X86.AVX() {
		return
	}
	c2 = func() {
		// This code can assume AVX is available, even though
		// it may be called after J returns.
	}
}

In a sense, the closure also closes over CPU features. This works because the supported features are static during an executable’s lifetime.

Interfaces are trickier. The conservative solution would be to disallow converting a type to an interface if any of the type’s methods have requirements that aren’t satisfied at the point of conversion. It’s not enough to only check the methods matched by the interface because the interface value could be later type-asserted to a wider interface.

Instead, we chose to simply not check interface assignments. We assume these will be rare in SIMD code given its orientation toward performance, and the worst that can happen is that a method panics because a required feature was not checked by the caller. However, see “future directions” for a more complete solution.

While interfaces are fundamentally dynamic, constraints can be checked statically. For a generic instantiation of type T to constraint type C, the vet check assures that any methods of T that satisfy C have their requirements met at the instantiation point.

Compatibility

This proposal is additive only. For now, it would only be in effect when GOEXPERIMENT=simd. The details may change before we standardize the simd package without the GOEXPERIMENT.

Implementation

I (@aclements) already have a prototype implementation of this vet check. The intent would be to land this as part of the Go 1.26 simd experiment.

Future directions

Above is a minimal proposal, but we plan to extend this in a few ways for the final version.

Requirement expressions

We plan to extend the cpu:requires directive to accept boolean expressions using both && and || (but not !). In particular, this will allow functions that have different requirements on different CPU architectures, but it needn’t be limited to that use case.

This complicates the vet check’s analysis somewhat. If G has requirement expression g and calls function F with requirement expression f, then vet must check that g ⇒ f is valid (universally true). Because requirement expressions are monotone formulas (no “not” operator), checking this is relatively straightforward and at worst quadratic in the length of the expression (no SAT solvers required 😀).

Custom feature check functions

In the minimal proposal, there’s no way to write a feature check function outside the simd package. We plan to allow user-written feature check functions by adding an additional directive comment:

//cpu:ensures <feature> [&& <feature>]
func Check() bool { ... }

The feature check function must return bool, and the vet check will ensure that it only returns true if the listed features have actually been checked in its body. We may extend this to functions that panic if the listed features are not available.

Dynamic interface call checks

While there’s no practical solution to statically checking interface method calls, with some help from the compiler we could transform any method requirements into dynamic checks on interface calls. To do this, the compiler would construct wrapper functions for any method with requirements. The wrapper would dynamically check the method’s requirements and produce a clear panic. These wrappers would appear in the interface method tables rather than the underlying methods. This way, direct calls to the method would skip these dynamic checks, while dynamic calls via an interface value would get dynamic checks. This is similar to transformations the compiler already does in other situations involving interfaces.

Feature-based optimization

Some SIMD operations can map to more or less efficient hardware instructions depending on which CPU features are available. While the compiler is able to infer this from explicit CPU feature checks, this is limited to intra-procedural analysis. With cpu:requires directives, it could optimize entire functions under stronger feature assumptions and give programmers more control over these optimizations, while still ensuring inter-procedural safety via the vet check.

Metadata

Metadata

Assignees

No one assigned

    Labels

    ProposalToolProposalIssues describing a requested change to a Go tool or command-line program.

    Type

    No type

    Projects

    Status

    Active

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions