This is a creation in Article, where the information may have evolved or changed.
This article was translated from: Interface-allocs
A few weeks ago, Peter Bourgon opened a post on standardized logging in Golang-dev . Logs are often used, so performance is quickly improved. The Go-kit log package uses structured logs with the following interfaces:
type Logger interface Log (keyvals ... interface{} ) error
}
calling code:
logger. log "Transport" , "HTTP" , , addr, , "listening" )
Note that all content that goes into the log call is converted to interface{}. This means that it allocates a lot of memory.
Compare to zap with another structured log library. Zap is used to avoid memory allocations and interface{}, resulting in a more ugly API:
Logger. Info ("Failed to fetch URL.", Zap. String("url", url) , Zap. Int("Attempt", Trynum) , Zap. Duration("Backoff", sleepfor) ,)
Logger. The parameter of info is Logger.field. Logger. field is a union-ish structure that includes a string, an int, and a interface{}. Therefore, the interface does not have to be used to pass the most common values.
About logging the first discussion here. Next, why is there memory allocation when converting a specific value to interface{}?
interface{} is represented as a type pointer and a value pointer. Russ Cox wrote an essay explaining the problem.
His article is a little out of date. But he pointed out an optimization method: When the value is less than or equal to the pointer size, we can put the values directly into the second field. However, with the advent of concurrent garbage collection, the optimization was canceled. The second field in the interface is now always a pointer.
Consider the following code:
fmt.Println(1)
Before go 1.4, this code did not have memory allocations because the value 1 could be placed directly into the second field.
In other words, the compiler handles this:
fmt.Println({int, 1})
where {Typ,val} represents two fields in an interface.
Starting with Go 1.4, this code starts allocating memory because 1 is not a pointer, and the second word must contain a pointer. So, the compiler + runtime handles this:
I:= new(int) //allocates!
*I=1Fmt. Println({int, i})
The 1th optimization of memory allocation is to ensure that the generated interface does not escape. In this case, the temporary value can be placed on the stack instead of on the heap. Use our sample code above:
I: =New(int)//Now doesn ' t allocate, as long as E doesn ' t escape*i =1varEInterface{} = {intI//Do things with E that don ' t make it escape
Unfortunately, many interface{} will escape, including in the call to FMT. Println and the interface{} used in the log example above.
Fortunately, Go 1.9 will bring more optimizations, and some optimizations are inspired by logging.
The first optimization is to no longer convert constants to interfaces. So FMT. PRINTLN (1) will no longer allocate memory. The compiler places the value 1 in a read-only global variable, roughly as follows:
var i int = //at the top level, marked as ReadOnly
FMT. println
Because constants are immutable, each interface transformation obtains the same value, including recursion and concurrent calls.
This is directly inspired by Loggin. In a structured log, many parameters are constants. Go-kit Example:
logger. log "Transport" , "HTTP" , , addr, , "listening" )
This code will reduce the memory allocation from 6 to 1 times, because five of these parameters are constant strings.
The second new optimization is to not convert bool and byte to interfaces. This optimization works by adding a global [256] byte array named Staticbytes, where all B's staticbytes [b] = B. When the compiler wants to put a bool or uint8 or other single-byte value into the interface, it uses a pointer to the array instead. Is that:
var staticbytes [256 ]byte = {0 , 1 , 2 , 3 , 4 , 5 , ...} I: = uint8 (1 ) fmt. Println ({int , & Staticbytes[i]})
The third new optimization recommendation is still review, and this optimization is a common 0-value optimization for conversions. It applies to integers, floating-point numbers, strings, and slices. This optimization works by checking whether a value is 0 (or "" or nil) at run time. If it is a value of 0, it uses a pointer to an existing chunk of 0 memory instead of allocating some memory and zeroing it.
If all goes well, Go 1.9 should eliminate a significant amount of memory allocations during interface conversions. But it does not eliminate all memory allocations, which makes the performance issues discussed above still exist.
The selection API requires performance considerations. This is also why IO. Reader requires/allows callers to use their own buffers.
Performance is largely the result of design implementations. We have seen in this article that the implementation details of the interface can greatly improve memory allocation.
Many design and implementation decisions depend on what kind of code people write. The compiler and the author of the runtime want to optimize the actual, generic code. For example, in Go 1.4, it was decided to keep the interface values at two words instead of changing them to three, which made the call to FMT. PRINTLN (1) allocates additional memory.
Because people write code that is often shaped by the APIs they use, it's an organic feedback loop, which is also interesting and challenging to manage.
If you design an API and worry about performance issues, don't forget what the existing compilers and runtimes actually do or what they can do. Write the current code, but design the future API.