Blog

Two small Go 1.1 speedups

I rebuilt a little indexing program with Go 1.1 and it became faster without receiving any of my expert optimization, which is generally the safest kind.

The program mostly fills maps and then produces enough temporary objects to keep the garbage collector socially engaged:

for _, word := range words {
	counts[word]++
}

Go 1.1 includes a new map implementation and substantial runtime improvements. Map operations are faster, particularly for common key types, and the garbage collector does less work in several important paths. Programs heavy on either can improve simply by recompiling.

This is not a universal percentage discount. Key sizes, hit rates, map growth, allocation rate, live heap, processor count, and workload shape all matter. My toy index is evidence about my toy index.

The right comparison is to build the same source with both releases, run multiple times on an otherwise quiet machine, and look at distributions rather than choosing the friendliest result. If the program is a service, latency and pauses may matter more than total runtime.

Runtime speedups are welcome because they improve ordinary code without making it strange. I will still provide map size hints when I know them and avoid pointless allocation. A faster collector is not a licence to create garbage professionally.

The race detector found my clever cache

Go 1.1’s race detector found a bug in a cache I had described as “effectively read-only.” Adverbs remain an important source of software defects.

The cache was reduced to this:

var cached map[string]string

func lookup(key string) string {
	if cached == nil {
		cached = loadAll()
	}
	return cached[key]
}

Two handlers could observe cached == nil and assign it concurrently. Worse, one could read the map while another was publishing it. Running tests with go test -race reported the conflicting accesses and the goroutine stacks involved.

The detector instruments memory accesses in the compiled program and tracks synchronization events. When two goroutines access the same location concurrently, at least one access is a write, and no synchronization orders them, it can report a data race. The stack traces are the useful part: they connect an abstract memory-model violation to the two paths I actually wrote.

I fixed the cache with a mutex and a clear initialization path. A channel-based owner goroutine would also have worked, but building a miniature cache server around one assignment would have been enthusiasm rather than design.

The detector only sees executions that happen. A test that never drives two requests through initialization cannot reveal this race. I added a test that starts several goroutines together and exercises the path repeatedly. Even then, a clean run is evidence, not proof that every possible schedule is safe.

Race-enabled binaries are slower and use more memory because instrumentation and bookkeeping are not free. I would use them in tests and representative staging workloads rather than deciding the overhead is a mysterious production regression. The point is to make races observable, not to benchmark the detector against uninstrumented code and appear shocked.

A race report is also not permission to sprinkle locks around the named lines. The conflicting accesses may reveal confused ownership, publication without synchronization, or a larger invariant spanning several fields. Protecting one variable while leaving the invariant split can silence one report and preserve the bug.

Nor should the test ignore a report because the final value happened to be correct. The detected execution violated the memory model. A passing assertion after that is luck arriving late and asking to be mistaken for synchronization.

The race detector does not find deadlocks, logical races built from properly synchronized but wrongly ordered operations, or races in code it never executes. It finds data races, and that is already an excellent service. Mine was real, small, and previously certified by the prestigious It Seems Fine Institute.

Join us on App.Net

I liked the idea behind App.Net (or ADN for the initiated) from the start; I’ve happily signed up during the initial funding effort and before it even existed. It is quite like Twitter, although it does have some pretty interesting API advantages that allow clients to do things that are not possible in Twitter such as creating private chat rooms (with Patter.) I found a text by Matt Gemmell, App.Net for conversations, that sums it up nicely:

The interesting part, though, is what you won’t be used to from Twitter. There are no ads, anywhere. Because it’s a paid service, there’s no spam at all; I’ve certainly never seen any. There’s an active _and happy _developer community, which ADN actually financially rewards. There’s a rich, modern, relentlessly improved API. And again because it’s a paid service, there’s a commensurately (and vanishingly) low number of Bieber fans, teenagers, illiterates, and sociopaths.

But the real difference I notice is in the conversations. On Twitter, the back-and-forth tends to be relatively brief, not only in terms of the 140-character limit, but also the number of replies. There’s a certain fire-and-forget sensibility to Twitter; it’s a noticeboard rather than a chatroom. Then there’s the keyword-spam (woe betide the person who mentions iPads, or MacBooks, or broadband, or just about anything). Oh, and let’s not forget the fact that any malcontent with internet access can create an account (or two, or ten) in seconds. Not a happy mixture.

I’d add that there seems to be less of a popular clique on ADN. Popular users seem to be much more engaging with “regular people” than on Twitter. And there’s the developers… although most of the rush is now behind us, it was fun to follow the developers working on ADN clients. It was a very collaborative effort, with alpha builds floating around and discussions about whether this or that should be done in a certain way.

As for the developers of ADN proper, well, you can try asking ADN CEO and Founder Dalton something to see if he’ll answer you in about 30 seconds. He actually does. 🙂

It all feels like a big community where everyone feels a bit like they own the place as well and want it to thrive. Again I think Matt is on the money on why this is so:

We value what we pay for. We not only pay for things which we deem to be of value, but we also retrospectively assign and justify value based on what we’ve paid. Any consumer is familiar with the simple psychology of cost equating as much to value after the transaction as value does to cost beforehand (likely moreso, from my own experience). At its core, I don’t think that the reason for the noticeably different, warmer, more discursive “feel” of ADN is any more complicated than that.

I personally love the service and I think you should consider it too. There is a free tier account that allows you to follow up to 40 people for free, as long as you’re invited by a current user. If you’re interested, I have a few invites.

Feel free to comment on this post by using this Google+ thread or also by talking to me on, where else, ADN, where I’m @robteix. And of course Twitter isn’t going anywhere and I’m there too.

Go 1.1 and the G-M-P scheduler

Go 1.1 was released this week, so naturally I ignored the pleasant surface additions and went looking for the scheduler changes. This is normal behaviour in some households.

The new scheduler is commonly described with three letters: G, M, and P. A G is a goroutine, including its stack and scheduling state. An M is an operating-system thread, the machine on which Go code eventually runs. A P is a processor-like runtime resource required to execute Go code. GOMAXPROCS controls the number of Ps and therefore how much Go code can execute simultaneously.

This extra P may seem unnecessary. Why not put runnable goroutines directly on threads? The answer becomes clearer when a thread blocks.

The naive picture

Imagine each M owns a queue of goroutines:

M0: G1 G2 G3
M1: G4 G5

If G1 enters a blocking system call, M0 blocks with it. The runtime must somehow preserve access to G2 and G3, arrange another thread, and keep global scheduling state coherent. If queues and caches belong permanently to Ms, every blocked call complicates their ownership.

In the G-M-P design, runnable work and resources are associated with P. An M must acquire a P to execute Go code. If its goroutine enters a blocking system call, the M can detach from its P. Another M acquires that P and continues running other goroutines. When the system call returns, its G becomes eligible to run again, possibly on a different M.

The identities are separate because their lifetimes and reasons for existing are separate. G represents work. M represents a kernel execution context. P represents permission and resources to execute Go work.

Local queues

Each P has a local run queue. Creating a runnable goroutine can usually place it on the current P’s queue instead of contending on one global scheduler lock. The current M draws work from its P locally, which improves locality and reduces synchronization.

Local queues create an obvious question: what happens when one P has no work while another has a long queue? Idle Ps steal runnable goroutines from busy Ps. Work stealing balances execution without forcing every enqueue and dequeue through a single shared structure.

There is still a global queue for cases that need it, and the scheduler periodically considers it so global work is not ignored. “Local queues” does not mean each P becomes an isolated principality with its own foreign policy.

This organization also gives runtime caches a natural home. Per-P allocation state, for example, can be used by whichever M currently runs that P. If such state belonged to M, blocking and replacing threads would either strand it or require more coordination.

A small scheduler experiment

I used a deliberately crude program:

func burn(done chan bool) {
	x := uint64(1)
	for i := 0; i < 200000000; i++ {
		x = x*1664525 + 1013904223
	}
	fmt.Println(x)
	done <- true
}

func main() {
	runtime.GOMAXPROCS(2)
	done := make(chan bool)
	go burn(done)
	go burn(done)
	<-done
	<-done
}

With two Ps and enough operating-system support, the two CPU-bound goroutines can execute in parallel. With one P they execute one at a time and, because these loops contain no scheduling point, one may finish before the other starts running. The distinction matters. Goroutines make concurrency cheap; GOMAXPROCS and available processors determine parallel Go execution.

This benchmark is useful for observing CPU use and almost useless for choosing an application’s GOMAXPROCS. Real programs block, allocate, communicate, and touch memory. A tight arithmetic loop mostly proves that arithmetic loops enjoy processors.

Blocking, parking, and spinning

Not all waiting has the same consequence. A goroutine waiting on a channel or runtime-managed network operation can be parked, allowing the M and P to run another G. A blocking system call may hold the M, so the P is handed off. A call into C may similarly occupy a thread the scheduler cannot use for Go work until the call returns.

The scheduler also needs to avoid excessive thread creation. It tracks spinning workers searching for work and parked workers waiting to be awakened. Waking every idle M whenever one G becomes runnable would produce a thundering herd. Waking none would leave parallel capacity unused. The details are subtle because scheduling is largely the art of replacing one kind of waste with a smaller kind.

What this means in programs

The design makes several common Go patterns scale better, but it does not absolve application code.

A goroutine that blocks while holding a mutex can prevent unrelated goroutines from making useful progress even though the scheduler itself is healthy. A producer can create runnable goroutines faster than Ps can execute them. CPU-bound goroutines can compete with latency-sensitive work. The scheduler distributes runnable work; it cannot determine which business request deserves sympathy.

The safest optimization remains architectural: block goroutines on channels or network operations when they have no work, bound queues where overload is possible, and measure latency under realistic contention. Reaching into scheduler behaviour to arrange exact execution order is not a design. It is a hostage negotiation with an implementation detail.

I like the G-M-P model because it separates concerns that were tangled in earlier schedulers. It gives runnable work local queues, lets processor resources survive blocked threads, and creates better places for per-execution caches. Go 1.1 should spend less time fighting a global scheduler lock and more time running the program.

Scheduler improvements cannot make sequential code parallel, remove lock contention, or turn infinite work into finite work. They do make the straightforward goroutine model a better foundation, which beats an application-level scheduler I would have to debug myself.

database/sql is not a database connection

I put sql.Open inside an HTTP handler because the function was named Open and I wanted to open the database. The code worked, which delayed the useful realization that *sql.DB is not one database connection.

sql.Open creates a database handle using a registered driver and its data source name. It may not contact the server immediately. The first operation can therefore be where a bad password, missing server, or malformed configuration finally becomes visible. When startup must verify the database, run a trivial query and handle its error.

The handle manages a pool of underlying connections. It is intended to be long-lived and shared by concurrent goroutines. Opening one per request creates needless pool machinery and closing each one prevents useful connection reuse.

The practical shape is:

db, err := sql.Open("driver-name", dataSource)
if err != nil {
	log.Fatal(err)
}
defer db.Close()

var one int
if err := db.QueryRow("select 1").Scan(&one); err != nil {
	log.Fatal(err)
}

The driver import and data source are database-specific. database/sql deliberately supplies common operations while drivers deal with wire protocols and server peculiarities. This boundary is useful, but it does not make SQL dialects identical or transactions portable by divine intervention.

Rows carry a connection resource until they are exhausted or closed. That makes this pattern important:

rows, err := db.Query("select name from people")
if err != nil {
	return err
}
defer rows.Close()

for rows.Next() {
	var name string
	if err := rows.Scan(&name); err != nil {
		return err
	}
}
return rows.Err()

Checking only the initial Query error misses errors encountered while iterating. Forgetting Close can keep the underlying connection unavailable to other work. A leak in a pooled abstraction tends to present itself as an unrelated request mysteriously waiting, which is a thoughtful way for a bug to meet new people.

Transactions bind operations to the transaction’s connection. Once Begin succeeds, calls that belong to the transaction must use tx.Query, tx.Exec, and friends, not the original db handle. Calling db.Exec in the middle may use another connection and therefore sit outside the transaction entirely.

Statements and transactions need closing too. I keep their lifetime in the smallest useful scope and arrange cleanup immediately after successful creation. This is less about tidiness than returning scarce server and connection resources on every error path.

I now create one *sql.DB for a configured database, verify it during startup when appropriate, pass it to code that needs it, close rows promptly, and inspect iteration errors. Boring database plumbing is good database plumbing.