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填充全部消失的所有四个情况下,这都是巨大的差异(即使对于这种琐碎的情况)。它将函数的大小从减小243到67字节。另外,另一个好处是退出时将节省一些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优化编译器生成上述这将是一件好事描述的情况相同的代码。