close
Skip to content

cmd/compile: break false dependencies for x86 BSF/BSR instructions #82133

Description

@zolstein

Go version

go1.27.1 amd64/linux

Output of go env in your module/workspace:

GOVERSION='go1.27.1'
GOOS=linux
GOARCH=amd64
GOAMD64=v1

What did you do?

Ran a benchmark of bits.TrailingZeros64, inspected the generated assembly, and recorded performance counters on an Intel Core Ultra 7 258V processor.

Benchmark program
package main

import (
	"flag"
	"fmt"
	"math/bits"
)

var result uint64

//go:noinline
func scanLow(iterations uint64) uint64 {
	mask := uint64(1)
	var sum uint64
	for i := uint64(0); i < iterations; i++ {
		sum += uint64(bits.TrailingZeros64(mask))
		mask = bits.RotateLeft64(mask, 1)
	}
	return sum
}

//go:noinline
func scanHigh(iterations uint64) uint64 {
	mask := uint64(1)
	var sum uint64
	for i := uint64(0); i < iterations; i++ {
		sum += uint64(bits.Len64(mask) - 1)
		mask = bits.RotateLeft64(mask, 1)
	}
	return sum
}

func main() {
	mode := flag.String("mode", "low", "bit scan to exercise: low or high")
	iterations := flag.Uint64("iterations", 1_000_000_000, "number of bit scans")
	flag.Parse()
	if flag.NArg() != 0 {
		panic("unexpected positional arguments")
	}
	switch *mode {
	case "low":
		result = scanLow(*iterations)
	case "high":
		result = scanHigh(*iterations)
	default:
		panic("mode must be low or high")
	}
	tail := *iterations % 64
	want := (*iterations/64)*2016 + tail*(tail-1)/2
	if result != want {
		panic(fmt.Sprintf("incorrect result: got %d, want %d", result, want))
	}
	fmt.Printf("mode=%s iterations=%d checksum=%d\n", *mode, *iterations, result)
}

Build with:

GOARCH=amd64 GOAMD64=v1 go build -o main-amd64 main.go
GOARCH=386 GO386=sse2 go build -o main-386 main.go

Inspect the generated scans with:

go tool objdump -s 'main.scan(Low|High)' main-amd64
go tool objdump -s 'main.scanLow' main-386

scanHigh exercises BSRQ on amd64, but bits.Len64 does not generate BSRL on 386.

Measure the performance:

perf stat ./main-amd64
perf stat ./main-amd64 -mode=high
perf stat ./main-386

What did you see happen?

BSF and BSR have a dependency on the previous value of the destination register. The amd64 compiler emits BSFQ DX, SI without first breaking this dependency.

When register allocation reuses the destination register across loop iterations, otherwise independent bit scans become serialized, significantly reducing throughput.

What did you expect to see?

When the source and destination registers differ, the compiler should emit a zeroing idiom before BSF/BSR to break the false destination dependency:

XORL SI, SI
BSFQ DX, SI

The amd64 backend already uses this technique for POPCNT, TZCNT, and LZCNT. The same should be done for BSFQ, BSFL, BSRQ, and BSRL, with corresponding changes in the 386 backend.

I tested with a local compiler patch to emit the XORL instructions. One before/after measurement per target produced:

Target Metric Before After
amd64 Elapsed time 1.1398 s 0.4157 s
amd64 Cycles 5,270,880,887 1,887,807,205
amd64 Instructions 8,000,592,473 9,000,460,803
386 Elapsed time 3.7005 s 1.6483 s
386 Cycles 17,112,607,164 7,615,912,946
386 Instructions 40,501,557,071 41,000,961,436

That is a 2.74× speedup on amd64 and 2.25× on 386, despite executing more instructions.

Real-world impact

I encountered this while optimizing the GC. Scanning performance was highly sensitive to whether register allocation produced dependencies between loop iterations. In one comparison, a build with the dependency showed the following regression relative to a baseline without it:

Measurement Change from baseline
Benchmark time +97.14%
Cycles +95.25%
Instructions -9.24%
Branches -11.89%

Tweaking the source code could make the compiler produce BSFQ src, src, avoiding the dependency. However, small source changes can cause the compiler to choose different registers and reintroduce the dependency. This makes performance improvements fragile; changes that reduce instructions or register pressure can still introduce a costly dependency between loop iterations and cause regressions.

Activity

  1. changed the title [-]cmd/compile: avoid false destination dependencies for x86 BSF/BSR[/-] [+]cmd/compile: break false dependencies for x86 BSF/BSR instructions[/+] on Oct 10, 2026
  2. gopherbot commented on Oct 11, 2026

    @gopherbot
    Contributor

    Change https://go.dev/cl/848189 mentions this issue: cmd/compile: break false destination dependencies for BSF and BSR

  3. nightlyone commented on Oct 11, 2026

    @nightlyone
    Contributor

    @zolstein Is there more information which Intel processors/generations are affected?

    That information would be helpful for long-term maintenance when that is fixed in later generations or can be assumed fixed on well maintained machines because a microcode update did fix it.

  4. zolstein commented on Oct 11, 2026

    @zolstein
    Author

    Hmm. I was under the impression that this was exclusive to Intel chips, but this actually seems to be universal among x86_64 chips. uops.info shows a non-zero operand 1 → 1 latency on all processors tested. This is distinct from e.g. LZCNT where the latency is non-zero only on pre-Skylake Intel chips. Also, it seems like AMD and Intel both now document the behavior that the destination register is unchanged on a zero source, so the dependency is only false when the source is non-zero.

    Intel documentation: https://cdrdv2.intel.com/v1/dl/getContent/671199 (Page 3-107)
    AMD documentaiton: https://kib.kiev.ua/x86docs/AMD/AMD64/24594_APM_v3-r3.37.pdf (Page 123)

    https://uops.info/html-instr/BSR_R64_R64.html
    https://uops.info/html-instr/BSF_R64_R64.html
    https://uops.info/html-instr/BSR_R32_R32.html
    https://uops.info/html-instr/BSF_R32_R32.html
    https://uops.info/html-instr/BSR_R16_R16.html
    https://uops.info/html-instr/BSF_R16_R16.html

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    compiler/runtimeIssues related to the Go compiler and/or runtime.

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions