May 27, 2015
Go: The price of interface{}
Go’s empty interface{}
is the interface that everything implements. It allows functions that can be passed any type. The function func f(any interface{})
can be called with a string f("a string")
, an integer f(42)
, a custom type, or anything else.
This flexibility comes at a cost. When you assign a value to a type interface{}
, Go will call runtime.convT2E to create the interface structure (read more about Go interface internals). That requires a memory allocation. More memory allocations means more garbage on the heap, which means longer garbage collection pauses.
Here’s an example. A log function two ways, neither of which do anything because we’re logging at ‘prod’ level, and the messages are ‘debug’. One is more expensive than the other.
package main import "fmt" const ( debug byte = iota prod ) var logLevel = prod func main() { logIface(debug, "Test interface") logString(debug, "Test string") } func logIface(level byte, msg interface{}) { if level >= logLevel { fmt.Println(msg) } } func logString(level byte, msg string) { if level >= logLevel { fmt.Println(msg) } }
And here are benchmarks:
package main import "testing" func BenchmarkIface(b *testing.B) { for i := 0; i < b.N; i++ { logIface(debug, "test iface") } } func BenchmarkString(b *testing.B) { for i := 0; i < b.N; i++ { logString(debug, "test string") } }
Here's the output of go test -bench Benchmark -benchmem
on my machine, go1.4.2 linux/amd64:
BenchmarkIface 10000000 126 ns/op 16 B/op 1 allocs/op BenchmarkString 200000000 6.11 ns/op 0 B/op 0 allocs/op
To see what is happening ask Go for the assembly output:
go build -gcflags -S <filename>.go
Notice the CALL runtime.convT2E
before CALL logIface
, but not before CALL logString
.
Or use the fantastic Go Compiler Exporer. The call to runtime.convT2E
is on line 27.
Even though logIface
doesn't do anything, the runtime still needs to convert the string to an interface to call the function, and that's the memory allocation. It's good to look out for interface{}
usage in your inner-most loops - look for fmt
and log
package functions, container types, and many other places.
interface{}
is a wonderful thing to have, except in the most frequently called sections of you code.
Vasilesk said,
December 28, 2019 at 22:02
Sorry, I have run the example in the wrong way! Now I see that the x20 difference still remains for go1.13
Vasilesk said,
December 28, 2019 at 21:46
The article need an update. On go1.13.4 linux/amd64 the count of allocs equals zero for both cases. So the example illustrates nothing :(
Ralph Corderoy said,
May 29, 2015 at 16:28
Nick, see https://en.wikipedia.org/wiki/Escape_analysis
Nick said,
May 28, 2015 at 21:23
Roger. Can you please go into more detail about what you mean by “prove the value doesn’t escape.”
I plan on using interfaces for some important code, so I’d like to make sure that I don’t inadvertently cause extra issues.
Roger Peppe said,
May 27, 2015 at 17:02
Assigning to an interface only requires a memory allocation if you’re not assigning a pointer. Also, it may not require an allocation if the compiler can prove the value doesn’t escape.