Go version
go1.26-devel_2a7f1d47b0 linux/amd64
Output of go env
in your module/workspace:
AR='ar'
CC='gcc'
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_ENABLED='1'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
CXX='g++'
GCCGO='gccgo'
GO111MODULE=''
GOAMD64='v1'
GOARCH='amd64'
GOAUTH='netrc'
GOBIN=''
GOCACHE='/home/amusman/.cache/go-build'
GOCACHEPROG=''
GODEBUG=''
GOENV='/home/amusman/.config/go/env'
GOEXE=''
GOEXPERIMENT=''
GOFIPS140='off'
GOFLAGS=''
GOGCCFLAGS='-fPIC -m64 -pthread -Wl,--no-gc-sections -fmessage-length=0 -ffile-prefix-map=/tmp/go-build3835983328=/tmp/go-build -gno-record-gcc-switches'
GOHOSTARCH='amd64'
GOHOSTOS='linux'
GOINSECURE=''
GOMOD='/dev/null'
GOMODCACHE='/home/amusman/go/pkg/mod'
GONOPROXY=''
GONOSUMDB=''
GOOS='linux'
GOPATH='/home/amusman/go'
GOPRIVATE=''
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/home/amusman/ws/gs'
GOSUMDB='sum.golang.org'
GOTELEMETRY='local'
GOTELEMETRYDIR='/home/amusman/.config/go/telemetry'
GOTMPDIR=''
GOTOOLCHAIN='auto'
GOTOOLDIR='/home/amusman/ws/gs/pkg/tool/linux_amd64'
GOVCS=''
GOVERSION='go1.26-devel_2a7f1d47b0 Tue Sep 2 16:27:43 2025 -0700'
GOWORK=''
PKG_CONFIG='pkg-config'
What did you do?
There are some tests related to number of statement lines (with percentage triggered by https://go-review.googlesource.com/c/go/+/608115 , however that's independent issue and we don't that CL to reproduce). This issue is to better document that and has corresponding CL https://go-review.googlesource.com/c/go/+/696675 suggesting a fix. Consider the following small example:
package main
type Data struct {
Field1 int
Field2 *int
Field3 int
Field4 *int
Field5 int
Field6 *int
Field7 int
Field8 *int
}
//go:noinline
func InitializeData(d *Data) {
d.Field1++
d.Field2 = d.Field4
d.Field3++ // line 18
d.Field4 = d.Field6 // line 19
d.Field5++
d.Field6 = d.Field8
d.Field7++ // line 22
d.Field8 = d.Field2 // line 23
}
func main() {
var data Data
InitializeData(&data)
}
Writebarrier lowering needs to split one block function InitializeData
into several blocks and in this case offset calculation instruction (which contain statement marks) sometimes appear in top block (separate from their users).
What did you see happen?
Lines 18, 19, 22 and 23 in the above example have no stmt lines. For example (on amd64 target), it can be observed in ssa dump:
(18) v26 = ADDQconstmodify <mem> [val=1,off=16] v11 v19
(19) v29 = MOVQload <*int> [40] v11 v26
The offset was folded into stroring/loading instruction itself and the lower phase can in most cases preserve the offsets, however it does that only for offset from the same basic block.
What did you expect to see?
It would be better to preserve stmt line markers for these instructions.
Comment From: prattmic
cc @golang/compiler
Comment From: gopherbot
Change https://go.dev/cl/696675 mentions this issue: cmd/compile: propagate statement marks through write barrier splits
Comment From: gabyhelp
Related Issues
Related Code Changes
- cmd/compile: propagate statement marks through write barrier splits
- cmd/compile: rescue stmt boundaries from OpArgXXXReg and OpSelectN.
- cmd/compile: enable stack maps everywhere except unsafe points
- cmd/compile: add a writebarrier phase in SSA
- cmd/compile: move raw writes out of write barrier code
- cmd/compile: add write barrier for implicit zeroing
- cmd/compile: try to preserve IsStmt marks from OpConvert
- cmd/compile: add IsStmt breakpoint info to src.lico
- cmd/compile: eliminate write barrier for b = b[n:]
(Emoji vote if this was helpful or unhelpful; more detailed feedback welcome in this discussion.)
Comment From: randall77
I'm not seeing what you are here. Compiling your example, I see instructions with all of those lines. A selection:
0x000e 00014 (/Users/khr/gowork/issue75249.go:16) INCQ (AX)
0x002e 00046 (/Users/khr/gowork/issue75249.go:17) MOVQ CX, 8(AX)
0x0032 00050 (/Users/khr/gowork/issue75249.go:18) INCQ 16(AX)
0x0053 00083 (/Users/khr/gowork/issue75249.go:19) MOVQ CX, 24(AX)
0x0057 00087 (/Users/khr/gowork/issue75249.go:20) INCQ 32(AX)
0x0078 00120 (/Users/khr/gowork/issue75249.go:21) MOVQ CX, 40(AX)
0x007c 00124 (/Users/khr/gowork/issue75249.go:22) INCQ 48(AX)
0x009d 00157 (/Users/khr/gowork/issue75249.go:23) MOVQ CX, 56(AX)
Stores should ~always maintain their line number, I think.
The write barrier code also looks to have the right line number. e.g., all the write barrier code generated from line 19 has line 19 in the assembly output.
Comment From: amusman
The lost part is 'boundary' for stepping in debugger, e.g. I can see the difference as following for the example above:
$ dlv exec --check-go-version=false -- ./ref
(dlv) b fld2.go:16
(dlv) c
> [Breakpoint 1] main.InitializeData() ./example/fld2.go:16
(dlv) n
> main.InitializeData() ./example/fld2.go:17
(dlv) n
> main.InitializeData() ./example/fld2.go:20
...
Lines 18 and 19 are not stopped when stepping through function using next
debugger's command.
With the suggested patch in CL all the lines with stores are stopped.
Comment From: randall77
Hm, then maybe the problem shows up in the generated dwarf? It does not show up in the -S
output or the go tool objdump
output. Both of those look as one would expect, all lines present and in order.
Comment From: amusman
One of possible ways to dump dwarf for the generated binary, e.g. on Mac go build -ldflags="-compressdwarf=false" -o ref src/fld2.go && dwarfdump --debug-line ref > ref.lines
, it will print flags for each file/line - here are missing is_stmt
flags on lines 18 and 19:
file_names[ 2]:
name: "fld2.go"
dir_index: 1
mod_time: 0x00000000
length: 0x00000000
Address Line Column File ISA Discriminator OpIndex Flags
------------------ ------ ------ ------ --- ------------- ------- -------------
0x0000000100068200 15 0 2 0 0 0 is_stmt
0x000000010006820c 15 0 2 0 0 0 is_stmt prologue_end
0x0000000100068218 16 0 2 0 0 0 is_stmt
0x000000010006821c 16 0 2 0 0 0
0x0000000100068224 17 0 2 0 0 0 is_stmt
0x0000000100068228 17 0 2 0 0 0
0x0000000100068244 18 0 2 0 0 0
0x0000000100068250 19 0 2 0 0 0
0x0000000100068270 20 0 2 0 0 0 is_stmt
0x0000000100068274 20 0 2 0 0 0
0x000000010006827c 21 0 2 0 0 0 is_stmt
But in ssa dump it looks more convenient, the is_stmt lines are marked with '+' sign.
Comment From: randall77
Ok, I dove into this a bit more.
I don't think this really has anything to do with writebarriers per se.
The source of the problem is that during CSE, we have two nilcheck operations that get CSEd, one of which is a statement marker and one of which isn't. We sometimes use the latter as the representative, which forces the statement marker off the nilcheck that is going away and onto its user, an OffPtr.
When we CSE two values, we should really pick the one with the statement marker as the representative, all else being equal.
See the mailed CL for a fix.
At a more fundamental level, how we fix up statement markers is a bit ad-hoc. During lower, we often combine things like (Load (OffPtr [c] ptr))
to (LoadWithOffset [c] ptr)
. If OffPtr
has a statement marker, our rewrite engine only does some best-effort work to propagate the statement marker from the OffPtr
to the LoadWithOffset
. That best-effort work is particularly fragile if the OffPtr
and Load
are in different blocks (I think that's why this is showing up particularly for write barriers). But I'm not sure how to make this step more reliable, and it would probably be harder to solve than my fix for this issue.
Comment From: gopherbot
Change https://go.dev/cl/701295 mentions this issue: cmd/compile: when CSEing two values, prefer the statement marked one
Comment From: gopherbot
Change https://go.dev/cl/608115 mentions this issue: cmd/compile: CSE loads across disjoint stores
Comment From: amusman
@randall77 Thank you for the thorough analysis and the fix in CL 701295. Your identification of CSE as the root cause rather than write barriers per se was very insightful, and the fix works well for the example in this issue.
However, I have another related CL 608115 (cmd/compile: CSE loads across disjoint stores) where I'm seeing more cases of similar debugging information loss. Unfortunately, your suggested change in CL 701295, while effective here, doesn't fully address the broader set of cases that emerge with CL 608115 - the stmtlines_test.go
still fails with that change applied.
This led me to explore an alternative approach. You mentioned that during CSE, statement markers get moved from the matched value to its user (often an OffPtr
), which then leads to the fragile best-effort propagation during lowering that you described.
Since CSE matches NilCheck
s but doesn't actually remove them, I'm wondering if we could modify CSE to avoid moving statement boundary marks from NilCheck
to the user in the first place. This would keep the statement markers there until nil check elimination pass, which better preserves them.
I've incorporated this approach into CL 608115, and it appears to address both the example in this issue the additional cases that emerge from the CSE load optimization.
Would you please take a look at CL 608115 when you have time (I've only added this nil check special handling)? I'm fine with extracting the NilCheck-related change to a separate CL, but putting it together may be better because the other changes in that CL add more cases where this fix is needed.