Go 1.19 atomic types

August 24, 2022
go golang

Prologue

Go 1.19 was released, which have many changes to the toolchain and libraries. IMHO, one of the coolest one is the introduction of atomic types.

Atomic types

In pre-go1.19 world, you will write code like this:

package p

import "sync/atomic"

type S struct {
	// doing is set to non-zero when doing something.
	// Read/Write operations are done atomically.
	doing uint32
	child atomic.Value // of *S, created lazily.
}

func (s *S) do() {
	atomic.StoreUint32(&s.doing, 1)
	// doing thing here
}

func (s *S) check() {
	if atomic.LoadUint32(&s.doing) != 0 {
		panic("already doing")
	}
}

func (s *S) getChild() *S {
	child := s.child.Load()
	if child == nil {
		child = new(S)
		s.child.Store(child)
	}
	return child.(*S)
}

So far so good, but:

With atomic types, the code would become:

package p

import "sync/atomic"

type S struct {
	doing atomic.Bool       // doing is set to non-zero when doing something.
	child atomic.Pointer[S] // created lazily.
}

func (s *S) do() {
	s.doing.Store(true)
	// doing thing here
}

func (s *S) check() {
	if s.doing.Load() {
		panic("already doing")
	}
}

func (s *S) getChild() *S {
	child := s.child.Load()
	if child == nil {
		child = new(S)
		s.child.Store(child)
	}
	return child
}

Now the code is self-document and type safe.

Another cool property (but maybe less noticable) is the alignment guarantee.

Int64 and Uint64 are automatically aligned to 64-bit boundaries in structs and allocated data, even on 32-bit systems.

That would simplify a lot of Go code out there and make Go developers life easier. For example, in go1.18, sync.WaitGroup was defined as:

type WaitGroup struct {
	noCopy noCopy

	// 64-bit value: high 32 bits are counter, low 32 bits are waiter count.
	// 64-bit atomic operations require 64-bit alignment, but 32-bit
	// compilers only guarantee that 64-bit fields are 32-bit aligned.
	// For this reason on 32 bit architectures we need to check in state()
	// if state1 is aligned or not, and dynamically "swap" the field order if
	// needed.
	state1 uint64
	state2 uint32
}

// state returns pointers to the state and sema fields stored within wg.state*.
func (wg *WaitGroup) state() (statep *uint64, semap *uint32) {
	if unsafe.Alignof(wg.state1) == 8 || uintptr(unsafe.Pointer(&wg.state1))%8 == 0 {
		// state1 is 64-bit aligned: nothing to do.
		return &wg.state1, &wg.state2
	} else {
		// state1 is 32-bit aligned but not 64-bit aligned: this means that
		// (&state1)+4 is 64-bit aligned.
		state := (*[3]uint32)(unsafe.Pointer(&wg.state1))
		return (*uint64)(unsafe.Pointer(&state[1])), &state[0]
	}
}

The complicated part of the code comes from the alignment requirement for atomic operations (see all comments above). Since when the code must be portable and work on both 32-bit and 64-bit systems.

Using atomic types, the code can be simplified to just:

type WaitGroup struct {
	noCopy noCopy

	state atomic.Uint64 // high 32 bits are counter, low 32 bits are waiter count.
	sema  uint32
}

WaitGroup.state is now guaranteed to have 64-bit alignment, so it’s always safe to perform atomic operations on it. That also leads to the removal of WaitGroup.state() method.

Doing less, enable more!


Note

See CL 424835 for details and benchmark.



Epilogue

I hope you enjoy go1.19, it’s probably best Go release ever!

Thanks for reading so far.

Till next time!


GopherCon 2023

October 2, 2023
gophercon community go golang

Improving parallel calls from C to Go performance

June 29, 2023
go golang cgo runtime

A practical optimization for Go compiler

May 25, 2023
go golang compiler