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 DUFFCOPYmacro).

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:

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.

Previous Post Next Post