Golang内部构件第2部分:命名返回值的好处

Golang内部构件第2部分:命名返回值的好处

您可能知道Golang提供了命名返回值的功能。到目前为止,在MinIO上,我们并未使用太多此功能,但是由于有一些不错的隐藏好处,这种情况将有所改变,我们将在此博客文章中进行解释。

如果您像我们一样,则可能有大量的代码,如下所示,由此对于每个return语句,您都实例化一个新对象以返回“默认”值:

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{}
}

如果查看Golang编译器生成的实际代码,您将得到如下结果:

"".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

一切都很好,但如果您觉得这样有点重复,那您就说对了。本质上,对于每个return语句,要返回的对象或多或少地被分配/初始化(或更精确地通过DUFFCOPY复制)。

毕竟,return objectInfo{}在每种情况下,我们都要求通过via返回

命名返回值

现在来看一下如果我们做一个非常简单的更改会发生什么,本质上就是给返回值起一个名字(oi),然后使用Golang“裸”返回功能(删除该return语句的参数,尽管并非严格要求,请参见稍后):

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
}

再次查看编译器生成的代码,我们得到以下信息:

"".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

在对象初始化和DUFFCOPY填充全部消失的所有四个情况下,这都是巨大的差异(即使对于这种琐碎的情况)。它将函数的大小从减小24367字节。另外,另一个好处是退出时将节省一些CPU周期,因为不再需要执行任何操作来设置返回值。

请注意,如果您不喜欢或不喜欢Golang提供的裸收益,则可以return oi在仍然获得相同收益的情况下使用,例如:

	if i == 1 {
		return oi
	}

minio服务器中的真实示例

MinIO服务器中进一步研究了以下情况:

// 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
}

查看程序集,我们得到以下函数头(我们将为您省去完整的清单……):

"".parseCredentialHeader t=1 size=1157 args=0x68 locals=0xb8

如果我们修改代码以使用命名的返回参数(下面的第二个源代码块),请检查函数大小会发生什么:

"".parseCredentialHeader t=1 size=863 args=0x68 locals=0xb8

它削减了总共1150个字节中的大约300个字节,这对于对源代码进行如此最小的更改也不错。根据您的来源,您可能也更喜欢源代码的“更干净”外观:

// 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
}

请注意,实际上该ch变量是普通的局部变量,就像该函数中定义的任何其他局部变量一样。这样,您可以将其值从默认的“零”状态更改(但当然,修改后的版本将在退出时返回)。

命名返回值的其他用途

正如一些人指出的,命名返回值的另一个好处是在闭包(即defer语句)中的使用。因此,可以在作为defer语句结果调用的函数中访问命名的返回值,并相应地采取措施。

关于本系列

如果您错过了本系列的第一部分,请点击以下链接:

结论

因此,对于新代码和现有代码,我们将越来越多地逐渐采用命名返回值。

实际上,我们也在研究是否可以开发一些实用程序来帮助或自动化该过程。考虑以下内容,gofmt然后自动修改源以进行上面概述的更改。尤其是在还没有命名返回值的情况下(因此该实用程序必须为其命名),不一定可以是在现有源中以任何方式更改此返回变量的情况,因此使用return ch( (在上面列出的情况下)将不会导致程序的任何功能更改。因此,请继续关注。

我们希望本文对您有所帮助,并为您提供有关Go内部运行方式以及如何改进Golang代码的新见解。

更新资料

一个问题已经申请了Golang优化编译器生成上述这将是一件好事描述的情况相同的代码。


上一篇 下一篇