Golang Internals Part 2: Nice benefits of named return values
You may know that Golang offers the ability to name return values. Thus far at MinIO we have not been using this feature much, but that’ll change since there are some nice hidden benefits as we will explain in this blog post.
If you are like us, you may have considerable amounts of code as shown down below whereby for every return
statement you are instantiating a new object in order to return a ‘default’ value:
type objectInfo struct {
arg1 int64
arg2 uint64
arg3 string
arg4 []int
}
func NoNamedReturnParams(i int) (objectInfo) {
if i == 1 {
// Do one thing
return objectInfo{}
}
if i == 2 {
// Do another thing
return objectInfo{}
}
if i == 3 {
// Do one more thing still
return objectInfo{}
}
// Normal return
return objectInfo{}
}
If you look at the actual code that the Golang compiler generates, you’ll end up with something like this:
"".NoNamedReturnParams t=1 size=243 args=0x40 locals=0x0
0x0000 TEXT "".NoNamedReturnParams(SB), $0-64
0x0000 MOVQ $0, "".~r1+16(FP)
0x0009 LEAQ "".~r1+24(FP), DI
0x000e XORPS X0, X0
0x0011 ADDQ $-16, DI
0x0015 DUFFZERO $288
0x0028 MOVQ "".i+8(FP), AX
0x002d CMPQ AX, $1
0x0031 JEQ $0, 199
0x0037 CMPQ AX, $2
0x003b JEQ $0, 155
0x003d CMPQ AX, $3
0x0041 JNE 111
0x0043 MOVQ "".statictmp_2(SB), AX
0x004a MOVQ AX, "".~r1+16(FP)
0x004f LEAQ "".~r1+24(FP), DI
0x0054 LEAQ "".statictmp_2+8(SB), SI
0x005b DUFFCOPY $854
0x006e RET
0x006f MOVQ "".statictmp_3(SB), AX
0x0076 MOVQ AX, "".~r1+16(FP)
0x007b LEAQ "".~r1+24(FP), DI
0x0080 LEAQ "".statictmp_3+8(SB), SI
0x0087 DUFFCOPY $854
0x009a RET
0x009b MOVQ "".statictmp_1(SB), AX
0x00a2 MOVQ AX, "".~r1+16(FP)
0x00a7 LEAQ "".~r1+24(FP), DI
0x00ac LEAQ "".statictmp_1+8(SB), SI
0x00b3 DUFFCOPY $854
0x00c6 RET
0x00c7 MOVQ "".statictmp_0(SB), AX
0x00ce MOVQ AX, "".~r1+16(FP)
0x00d3 LEAQ "".~r1+24(FP), DI
0x00d8 LEAQ "".statictmp_0+8(SB), SI
0x00df DUFFCOPY $854
0x00f2 RET
All fine and dandy, but if that looks a bit repetitive to you, you are quite right. Essentially for each of the return
statements the object to be returned is more or less allocated/initialized (or more precisely copied via the DUFFCOPY
macro).
After all that is what we asked for by returning via return objectInfo{}
in every case.
Naming the return value
Now look at what happens if we make a very simple change, essentially just giving the return value a name (oi
) and using the ‘naked’ return feature of Golang (dropping the argument for the return
statement, although this is not strictly required, more on that later):
func NamedReturnParams(i int) (oi objectInfo) {
if i == 1 {
// Do one thing
return
}
if i == 2 {
// Do another thing
return
}
if i == 3 {
// Do one more thing still
return
}
// Normal return
return
}
Again looking at the code generated by the compiler, we get the following:
"".NamedReturnParams t=1 size=67 args=0x40 locals=0x0
0x0000 TEXT "".NamedReturnParams(SB), $0-64
0x0000 MOVQ $0, "".oi+16(FP)
0x0009 LEAQ "".oi+24(FP), DI
0x000e XORPS X0, X0
0x0011 ADDQ $-16, DI
0x0015 DUFFZERO $288
0x0028 MOVQ "".i+8(FP), AX
0x002d CMPQ AX, $1
0x0031 JEQ $0, 66
0x0033 CMPQ AX, $2
0x0037 JEQ $0, 65
0x0039 CMPQ AX, $3
0x003d JNE 64
0x003f RET
0x0040 RET
0x0041 RET
0x0042 RET
That is a pretty massive difference with all four occurrences of the object initialization and DUFFCOPY
stuff gone (even for this trivial case). It reduces the size of the function down from 243
to 67
bytes. Also as an additional benefit you will save some CPU cycles upon exiting out because there is no need anymore to do anything in order to setup the return value.
Note that if you don’t like or prefer the naked return that Golang offers, you can use return oi
while still getting the same benefit, like so:
if i == 1 {
return oi
}
Real world example in minio server
Examining a bit further in MinIO server we took the following case:
// parse credentialHeader string into its structured form.
func parseCredentialHeader(credElement string) (credentialHeader) {
creds := strings.Split(strings.TrimSpace(credElement), "=")
if len(creds) != 2 {
return credentialHeader{}
}
if creds[0] != "Credential" {
return credentialHeader{}
}
credElements := strings.Split(strings.TrimSpace(creds[1]), "/")
if len(credElements) != 5 {
return credentialHeader{}
}
if false /*!isAccessKeyValid(credElements[0])*/ {
return credentialHeader{}
}
// Save access key id.
cred := credentialHeader{
accessKey: credElements[0],
}
var e error
cred.scope.date, e = time.Parse(yyyymmdd, credElements[1])
if e != nil {
return credentialHeader{}
}
cred.scope.region = credElements[2]
if credElements[3] != "s3" {
return credentialHeader{}
}
cred.scope.service = credElements[3]
if credElements[4] != "aws4_request" {
return credentialHeader{}
}
cred.scope.request = credElements[4]
return cred
}
Looking at the assembly we get the following function header (we’ll spare you the full listing…):
"".parseCredentialHeader t=1 size=1157 args=0x68 locals=0xb8
If we modify the code to use a named return parameter (second source code block below), check out what happens to the size of the function:
"".parseCredentialHeader t=1 size=863 args=0x68 locals=0xb8
It is shaving off some 300 bytes out of a total of 1150 bytes which is not bad for a such a minimal change to the source code. And depending where you are coming from, you may prefer the ‘cleaner’ look of the source code too:
// parse credentialHeader string into its structured form.
func parseCredentialHeader(credElement string) (ch credentialHeader) {
creds := strings.Split(strings.TrimSpace(credElement), "=")
if len(creds) != 2 {
return
}
if creds[0] != "Credential" {
return
}
credElements := strings.Split(strings.TrimSpace(creds[1]), "/")
if len(credElements) != 5 {
return
}
if false /*!isAccessKeyValid(credElements[0])*/ {
return
}
// Save access key id.
cred := credentialHeader{
accessKey: credElements[0],
}
var e error
cred.scope.date, e = time.Parse(yyyymmdd, credElements[1])
if e != nil {
return
}
cred.scope.region = credElements[2]
if credElements[3] != "s3" {
return
}
cred.scope.service = credElements[3]
if credElements[4] != "aws4_request" {
return
}
cred.scope.request = credElements[4]
return cred
}
Note that actually the ch
variable is a normal local variable just like any other local variable that is defined within the function. And as such you can change its value from the default ‘zero’ state (but of course then the modified version will be returned upon exiting out).
Other uses of named return values
As pointed out by several persons, another benefit of named return values is the use in closures (i.e. defer statements). Thus one may access the named return value in a function that is called as the result of a defer
statement and act accordingly.
About this series
In case you missed the first part of this series, here is a link to it:
- About autogenerated functions
Conclusion
So we will be gradually adopting named return values more and more, both for new code as well as for existing code.
In fact we are also investigating if we can develop a little utility to help or automate this process. Think along the lines of gofmt
but then modifying the source automatically to make the changes outlined above. Especially in the case where the return value is not yet named (and so the utility would have to give it a name), it necessarily cannot be the case that this return variable is changed in any way in the existing source, and thus using return ch
(in case of the listing above) will not result in any functional changes of the program whatsoever. So stay tuned for that.
We hope that this post was useful to you and provides some new insights into how Go operates internally and on how to improve your Golang code.
Update
An issue has been filed for Golang to optimize the compiler to generate identical code for the cases described above which would be a good thing.