Skip to content

Instantly share code, notes, and snippets.

@SammyOina
Created May 16, 2023 09:35
Show Gist options
  • Save SammyOina/6eb54babd618ab6a850e8f1af4f4ac7d to your computer and use it in GitHub Desktop.
Save SammyOina/6eb54babd618ab6a850e8f1af4f4ac7d to your computer and use it in GitHub Desktop.
Using empty structs as context keys

Using empty structs as context keys

It is common to use integer types as keys in maps due low memeory allocation leading to better performance. For example int8 occupies a single byte of memory. An empty struct in golang have a minimum size of zero bytes, but they may have a size greater than zero due to padding. The size of an empty struct in Go is implementation-dependent, and it is usually 1 byte or larger to ensure that each instance of a struct has a unique memory address.

package main

import (
	"fmt"
	"unsafe"
)

type EmptyStruct struct{}

func main() {
	size := unsafe.Sizeof(EmptyStruct{})
	fmt.Println(size) // Output: 0
}

We can therfore assume in terms of performance and memory use, using empty structs as keys for context values in Go can be a more efficient choice compared to using int8 values.

We can confirm this using a benchmark test.

package main

import (
	"context"
	"testing"
)

func insertGetCtx(key, value interface{}) {
	ctx := context.WithValue(context.Background(), key, value)
	_ = ctx.Value(key)
}

func BenchmarkInsertGetCtx(b *testing.B) {
	val := "some-value"

	b.Run("string-key", func(b *testing.B) {
		for i := 0; i < b.N; i++ {
			type ctxKey string
			const myKey ctxKey = "string-key"
			insertGetCtx(myKey, val)
		}
	})
	b.Run("int-key", func(b *testing.B) {
		for i := 0; i < b.N; i++ {
			type ctxKey int
			const myKey ctxKey = 1
			insertGetCtx(myKey, val)
		}
	})
	b.Run("empty-struct-key", func(b *testing.B) {
		for i := 0; i < b.N; i++ {
			type ctxKey struct{}
			insertGetCtx(ctxKey{}, val)
		}
	})

}

The results of the benchmark form 10,000,000 iterations show our earlier aassumption.

go test -bench=. -benchtime=10000000x                                                                                   ✔ 
goos: linux
goarch: amd64
pkg: test
cpu: Intel(R) Core(TM) i7-8550U CPU @ 1.80GHz
BenchmarkInsertGetCtx/string-key-8         	10000000	        90.75 ns/op
BenchmarkInsertGetCtx/int-key-8            	10000000	        83.70 ns/op
BenchmarkInsertGetCtx/empty-struct-key-8   	10000000	        79.37 ns/op
PASS
ok  	test	2.543s
@orenshk
Copy link

orenshk commented Apr 7, 2024

This for this, great summary. Why are you creating the type in every iteration?

@SammyOina
Copy link
Author

This for this, great summary. Why are you creating the type in every iteration?

The idea was to isolate the performance impact of using different key types. I was trying to avoid compiler optimizations for previously used types. Of course for real world use this would not be the case where the key can be defined once and used multiple times.

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