Skip to content

Commit cc6923e

Browse files
DanielMorsinggopherbot
authored andcommitted
cmd/compile: reduce lock/scheduler contention
During the parallel section of compilation, we limit the amount of parallelism to prevent scheduler churn. We do this with a worker scheduler, but it does not have insight on when a compilation is blocked due to lock contention. The symbol table was protected with a lock. While most lookups were quick lock->read->unlock affairs, sometimes there would be initialization logic involved. This caused every lookup to stall, waiting for init. Since our worker scheduler couldn't see this, it would not launch new goroutine to "cover" the gap. Fix by splitting the symbol lock into 2 cases, initialization and lookup. If symbols need initialization simultaneously, they will wait for each other, but the common case of looking up a symbol will be handled by a syncmap. In practice, I have yet to see this lock being blocked on. Additionally, get rid of the scheduler goroutine and have each compilation goroutine grab work from a central queue. When multiple compilations finished at the same time, the work scheduler would sometime not get run immediately. This ended up starving the system of work. These 2 changes together cuts -1.37% off the build time of typescriptgo on systems with a lot of cores (specifically, the c3h88 perf builder). Updates golang#73044. Change-Id: I6d4b3be56fd00a4fdd4df132bcbd52e4b2a3e91f Reviewed-on: https://go-review.googlesource.com/c/go/+/724623 Reviewed-by: Keith Randall <khr@google.com> Reviewed-by: Dmitri Shuralyov <dmitshur@google.com> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Keith Randall <khr@golang.org> Auto-Submit: Keith Randall <khr@golang.org>
1 parent f809fae commit cc6923e

File tree

4 files changed

+91
-91
lines changed

4 files changed

+91
-91
lines changed

src/cmd/compile/internal/gc/compile.go

Lines changed: 25 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -142,73 +142,47 @@ func compileFunctions(profile *pgoir.Profile) {
142142
// Compile the longest functions first,
143143
// since they're most likely to be the slowest.
144144
// This helps avoid stragglers.
145+
// Since we remove from the end of the slice queue,
146+
// that means shortest to longest.
145147
slices.SortFunc(compilequeue, func(a, b *ir.Func) int {
146-
return cmp.Compare(len(b.Body), len(a.Body))
148+
return cmp.Compare(len(a.Body), len(b.Body))
147149
})
148150
}
149151

150-
// By default, we perform work right away on the current goroutine
151-
// as the solo worker.
152-
queue := func(work func(int)) {
153-
work(0)
154-
}
152+
var mu sync.Mutex
153+
var wg sync.WaitGroup
154+
mu.Lock()
155155

156-
if nWorkers := base.Flag.LowerC; nWorkers > 1 {
157-
// For concurrent builds, we allow the work queue
158-
// to grow arbitrarily large, but only nWorkers work items
159-
// can be running concurrently.
160-
workq := make(chan func(int))
161-
done := make(chan int)
156+
for workerId := range base.Flag.LowerC {
157+
// TODO: replace with wg.Go when the oldest bootstrap has it.
158+
// With the current policy, that'd be go1.27.
159+
wg.Add(1)
162160
go func() {
163-
ids := make([]int, nWorkers)
164-
for i := range ids {
165-
ids[i] = i
166-
}
167-
var pending []func(int)
161+
defer wg.Done()
162+
var closures []*ir.Func
168163
for {
169-
select {
170-
case work := <-workq:
171-
pending = append(pending, work)
172-
case id := <-done:
173-
ids = append(ids, id)
174-
}
175-
for len(pending) > 0 && len(ids) > 0 {
176-
work := pending[len(pending)-1]
177-
id := ids[len(ids)-1]
178-
pending = pending[:len(pending)-1]
179-
ids = ids[:len(ids)-1]
180-
go func() {
181-
work(id)
182-
done <- id
183-
}()
164+
mu.Lock()
165+
compilequeue = append(compilequeue, closures...)
166+
remaining := len(compilequeue)
167+
if remaining == 0 {
168+
mu.Unlock()
169+
return
184170
}
171+
fn := compilequeue[len(compilequeue)-1]
172+
compilequeue = compilequeue[:len(compilequeue)-1]
173+
mu.Unlock()
174+
ssagen.Compile(fn, workerId, profile)
175+
closures = fn.Closures
185176
}
186177
}()
187-
queue = func(work func(int)) {
188-
workq <- work
189-
}
190-
}
191-
192-
var wg sync.WaitGroup
193-
var compile func([]*ir.Func)
194-
compile = func(fns []*ir.Func) {
195-
wg.Add(len(fns))
196-
for _, fn := range fns {
197-
fn := fn
198-
queue(func(worker int) {
199-
ssagen.Compile(fn, worker, profile)
200-
compile(fn.Closures)
201-
wg.Done()
202-
})
203-
}
204178
}
205179

206180
types.CalcSizeDisabled = true // not safe to calculate sizes concurrently
207181
base.Ctxt.InParallel = true
208182

209-
compile(compilequeue)
210-
compilequeue = nil
183+
mu.Unlock()
211184
wg.Wait()
185+
compilequeue = nil
212186

213187
base.Ctxt.InParallel = false
214188
types.CalcSizeDisabled = false

src/cmd/internal/obj/line_test.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import (
1212

1313
func TestGetFileSymbolAndLine(t *testing.T) {
1414
ctxt := new(Link)
15-
ctxt.hash = make(map[string]*LSym)
1615
ctxt.statichash = make(map[string]*LSym)
1716

1817
afile := src.NewFileBase("a.go", "a.go")

src/cmd/internal/obj/link.go

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1169,21 +1169,22 @@ type Link struct {
11691169
Flag_maymorestack string // If not "", call this function before stack checks
11701170
Bso *bufio.Writer
11711171
Pathname string
1172-
Pkgpath string // the current package's import path
1173-
hashmu sync.Mutex // protects hash, funchash
1174-
hash map[string]*LSym // name -> sym mapping
1175-
funchash map[string]*LSym // name -> sym mapping for ABIInternal syms
1176-
statichash map[string]*LSym // name -> sym mapping for static syms
1177-
PosTable src.PosTable
1178-
InlTree InlTree // global inlining tree used by gc/inl.go
1179-
DwFixups *DwarfFixupTable
1180-
DwTextCount int
1181-
Imports []goobj.ImportedPkg
1182-
DiagFunc func(string, ...any)
1183-
DiagFlush func()
1184-
DebugInfo func(ctxt *Link, fn *LSym, info *LSym, curfn Func) ([]dwarf.Scope, dwarf.InlCalls)
1185-
GenAbstractFunc func(fn *LSym)
1186-
Errors int
1172+
Pkgpath string // the current package's import path
1173+
1174+
hashmu sync.Mutex // protects hash, funchash
1175+
hash sync.Map // name(string) -> sym(*syncOnce) mapping
1176+
funchash sync.Map // name(string) -> sym(*syncOnce) mapping for ABIInternal syms
1177+
statichash map[string]*LSym // name -> sym mapping for static syms
1178+
PosTable src.PosTable
1179+
InlTree InlTree // global inlining tree used by gc/inl.go
1180+
DwFixups *DwarfFixupTable
1181+
DwTextCount int
1182+
Imports []goobj.ImportedPkg
1183+
DiagFunc func(string, ...any)
1184+
DiagFlush func()
1185+
DebugInfo func(ctxt *Link, fn *LSym, info *LSym, curfn Func) ([]dwarf.Scope, dwarf.InlCalls)
1186+
GenAbstractFunc func(fn *LSym)
1187+
Errors int
11871188

11881189
InParallel bool // parallel backend phase in effect
11891190
UseBASEntries bool // use Base Address Selection Entries in location lists and PC ranges
@@ -1217,6 +1218,13 @@ type Link struct {
12171218
Fingerprint goobj.FingerprintType // fingerprint of symbol indices, to catch index mismatch
12181219
}
12191220

1221+
// symOnce is a "marker" value for our sync.Maps. We insert it on lookup and
1222+
// use the atomic value to check whether initialization has been completed.
1223+
type symOnce struct {
1224+
sym LSym
1225+
inited atomic.Bool
1226+
}
1227+
12201228
// Assert to vet's printf checker that Link.DiagFunc is a printf-like.
12211229
func _(ctxt *Link) {
12221230
ctxt.DiagFunc = func(format string, args ...any) {

src/cmd/internal/obj/sym.go

Lines changed: 43 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,11 @@ import (
4242
"log"
4343
"math"
4444
"sort"
45+
"sync"
4546
)
4647

4748
func Linknew(arch *LinkArch) *Link {
4849
ctxt := new(Link)
49-
ctxt.hash = make(map[string]*LSym)
50-
ctxt.funchash = make(map[string]*LSym)
5150
ctxt.statichash = make(map[string]*LSym)
5251
ctxt.Arch = arch
5352
ctxt.Pathname = objabi.WorkingDir()
@@ -90,28 +89,34 @@ func (ctxt *Link) LookupABI(name string, abi ABI) *LSym {
9089
// If it does not exist, it creates it and
9190
// passes it to init for one-time initialization.
9291
func (ctxt *Link) LookupABIInit(name string, abi ABI, init func(s *LSym)) *LSym {
93-
var hash map[string]*LSym
92+
var hash *sync.Map
9493
switch abi {
9594
case ABI0:
96-
hash = ctxt.hash
95+
hash = &ctxt.hash
9796
case ABIInternal:
98-
hash = ctxt.funchash
97+
hash = &ctxt.funchash
9998
default:
10099
panic("unknown ABI")
101100
}
102101

103-
ctxt.hashmu.Lock()
104-
s := hash[name]
105-
if s == nil {
106-
s = &LSym{Name: name}
107-
s.SetABI(abi)
108-
hash[name] = s
109-
if init != nil {
110-
init(s)
102+
c, _ := hash.Load(name)
103+
if c == nil {
104+
once := &symOnce{
105+
sym: LSym{Name: name},
111106
}
107+
once.sym.SetABI(abi)
108+
c, _ = hash.LoadOrStore(name, once)
112109
}
113-
ctxt.hashmu.Unlock()
114-
return s
110+
once := c.(*symOnce)
111+
if init != nil && !once.inited.Load() {
112+
ctxt.hashmu.Lock()
113+
if !once.inited.Load() {
114+
init(&once.sym)
115+
once.inited.Store(true)
116+
}
117+
ctxt.hashmu.Unlock()
118+
}
119+
return &once.sym
115120
}
116121

117122
// Lookup looks up the symbol with name name.
@@ -124,17 +129,31 @@ func (ctxt *Link) Lookup(name string) *LSym {
124129
// If it does not exist, it creates it and
125130
// passes it to init for one-time initialization.
126131
func (ctxt *Link) LookupInit(name string, init func(s *LSym)) *LSym {
127-
ctxt.hashmu.Lock()
128-
s := ctxt.hash[name]
129-
if s == nil {
130-
s = &LSym{Name: name}
131-
ctxt.hash[name] = s
132-
if init != nil {
133-
init(s)
132+
c, _ := ctxt.hash.Load(name)
133+
if c == nil {
134+
once := &symOnce{
135+
sym: LSym{Name: name},
134136
}
137+
c, _ = ctxt.hash.LoadOrStore(name, once)
135138
}
136-
ctxt.hashmu.Unlock()
137-
return s
139+
once := c.(*symOnce)
140+
if init != nil && !once.inited.Load() {
141+
// TODO(dmo): some of our init functions modify other fields
142+
// in the symbol table. They are only implicitly protected since
143+
// we serialize all inits under the hashmu lock.
144+
// Consider auditing the functions and have them lock their
145+
// concurrent access values explicitly. This would make it possible
146+
// to have more than one than one init going at a time (although this might
147+
// be a theoretical concern, I have yet to catch this lock actually being waited
148+
// on).
149+
ctxt.hashmu.Lock()
150+
if !once.inited.Load() {
151+
init(&once.sym)
152+
once.inited.Store(true)
153+
}
154+
ctxt.hashmu.Unlock()
155+
}
156+
return &once.sym
138157
}
139158

140159
func (ctxt *Link) rodataKind() (suffix string, typ objabi.SymKind) {

0 commit comments

Comments
 (0)