本篇文章给大家谈谈消除未使用的代码并精简可执行文件,以及对应的知识点,文章可能有点长,但是希望大家可以阅读完,增长自己的知识,最重要的是希望对各位有所帮助,可以解决了您的问题,不要忘了收藏本站喔。
这时候Go初学者就会有一个疑问:这些直接依赖包和间接依赖包中的代码都会进入最终的可执行文件吗?即使我们只是使用依赖包中的导出函数。
先说结论:不!在本文中,我们将探讨这个主题并了解其背后的支持机制及其对Go 可执行文件大小的影响。
1. 实验:哪些函数进入到最终的可执行文件中了?
让我们看一个具有间接依赖关系的示例:
//死代码消除/demo2$tree . go.mod main.go pkga pkga.go pkgb pkgb.go//pkga/pkga.gopackage pkgaimport ( 'demo/pkgb' 'fmt')func Foo() string { pkgb.Zoo() return 'Hello from Foo!'}func Bar() { fmt.Println('这是Bar.')}在这个例子中,我们在pkga.Foo函数中调用了一个新包pkgb的Zoo函数。让我们编译新的示例,看看哪些函数进入了最终的可执行文件:
$go build -gcflags='-l -N'$go tool nm demo|grep demo 1093b40 T demo/pkga.Foo 1093aa0 T demo/pkgb.Zoo 我们看到:只有程序执行路径能够到达(调用)的函数才会输入最终的可执行文件!
在复杂的例子中,我们还可以通过go build命令加上-ldflags='-dumpdep'来查看这个调用依赖关系(这里以demo2为例):
$go build -ldflags='-dumpdep' -gcflags='-l -N' deps.txt 21$grep demo deps.txt# demomain.main -demo/pkga.Foodemo/pkga.Foo -demo/pkgb.Zoodemo/pkga.Foo -go:string.'来自Foo 的你好!'demo/pkgb.Zoo -math/rand.Int31ndemo/pkgb.Zoo -demo/pkgb.stmp_0demo/pkgb.stmp_0 -go:string.'pkgb 中的动物园'转到这里,我们知道Go使用一定的机制来保证只有实际使用的代码最终才会进入可执行文件,即使有些代码(如pkga.Bar)和实际使用的代码(如pkga.Foo)位于同一个包内。这也确保了最终可执行文件的大小在控制范围内。
接下来我们就来看看Go的这个机制。
2. 未用代码消除(dead code elimination)
我们先回顾一下go build的构建过程。以下是go build 命令的步骤概述:
读取go.mod和go.sum:如果当前目录包含go.mod文件,go build会读取该文件以确定项目的依赖关系。它还根据go.sum 文件中的校验和验证依赖项的完整性。计算包依赖关系图:go build 分析正在构建的包中的导入语句及其依赖关系,以构建依赖关系图。该图表示包之间的关系,允许编译器确定包的构建顺序。确定要构建哪些包:根据构建缓存和依赖关系图,go build 确定需要构建哪些包。它检查构建缓存以查看编译的包是否是最新的。如果自上次构建以来某个包或其依赖项发生了更改,go build 将重建这些包。调用编译器(go toolcompile):对于每个需要构建的包,gobuild都会调用Go编译器(gotoolcompile)。编译器将Go源代码转换为特定目标平台的机器代码并生成目标文件(.o文件)。调用链接器(go 工具链接):编译完所有必需的包后,go build 调用Go 链接器(go 工具链接)。链接器将编译器生成的目标文件组合成可执行的二进制文件或包存档。它解析包之间的符号和引用,执行必要的重定位,并生成最终输出。上述整个构建流程可以用下图来表示:
在构建过程中,go build 命令还会执行各种优化,例如未使用的代码消除和内联,以提高生成的二进制文件的性能并减小二进制文件的大小。消除未使用的代码是保证Go 生成的二进制文件大小可控的重要机制。
未使用的检测算法的实现位于文件$GOROOT/src/cmd/link/internal/ld/deadcode.go 中。该算法通过图的遍历来进行,具体过程如下:
从系统入口点开始,标记所有可通过重定位到达的符号。重定位是两个符号之间的依赖关系。通过遍历重定位关系,算法标记了所有可以从入口点访问的符号。例如,如果在主函数main.main中调用了pkga.Foo函数,那么main.main中就会有这个函数的重定位信息。标记完成后,算法将所有未标记的符号标记为不可访问和未使用。这些未标记的符号表示无法通过入口点或其他可到达的符号访问的代码。然而,这里有一个特殊的语法元素需要注意,那就是带有方法的类型。 type方法是否进入最终的可执行文件取决于不同的情况。在deadcode.go 中,用于标记可达符号的函数实现将可达类型的调用方法分为三种:
通过可达接口类型直接调用通过反射调用:reflect.Value.Method(或MethodByName)或reflect.Type.Method(或MethodByName) 第一种情况,可以将被调用的方法直接标记为可达。第二种情况是通过将所有可到达的接口类型分解为方法签名来处理的。遇到的每个方法都会与接口方法签名进行比较,如果匹配,则将其标记为可达。这种做法很保守,但是简单又正确。
第三种情况是通过查找编译器标记为REFLECTMETHOD 的函数来处理的。函数F 上的REFLECTMETHOD 表示F 使用反射进行方法查找,但编译器在静态分析阶段无法确定方法名称。因此,所有调用reflect.Value.Method或reflect.Type.Method的函数都是REFLECTMETHOD。使用非常量参数调用reflect.Value.MethodByName 或reflect.Type.MethodByName 的函数也是REFLECTMETHOD。如果我们找到REFLECTMETHOD,则放弃静态分析,并将所有可达类型的导出方法标记为可达。
这是资源中的示例:
//死代码消除/demo3/main.gotype X struct{}type Y struct{}func (*X) One() { fmt.Println('hello 1') }func (*X) Two() { fmt.Println('hello 2') }func (*X) Three() { fmt.Println('hello 3') }func (*Y) Four() { fmt.Println('hello 4') }func ( *Y) Five() { fmt.Println('hello 5') }func main() { var name string fmt.Scanf('%s', name)reflect.ValueOf(X{}).MethodByName(name). Call(nil) var y Y y.Five()} 在此示例中,类型*X 有三个方法,类型*Y 有两个方法。在main函数中,我们通过反射调用X实例的方法,通过Y实例直接调用Y的方法。我们看看X和Y最终都通过哪些方法进入最终的可执行文件:
$go build -gcflags='-l -N'$go tool nm ./demo|grep main 11d59c0 D go:main.inittasks 10d4500 T main.(*X).一个10d4640 T main.(*X).三个10d45a0 T main .(*X).Two 10d46e0 T main.(*Y).Five 10d4780 T main.main.我们看到可达类型Y的代码中只有直接调用的方法Five进入最终的可执行文件,并且X所有通过反射调用的方法都可以在最终的可执行文件中找到!这与前面提到的第三种情况是一致的。
3. 小结
通过这种未使用代码剔除机制,Go语言可以控制最终可执行文件的大小,实现可执行文件的瘦身。
4. 参考资料
充分利用死代码消除[4] - https://golab.io/talks/getting-the-most-out-of-dead-code-eliminationall: 二进制文件太大且不断增长[5] - https://github.com/golang /go/issues/6853aarzilli/whydeadcode[6] - https://github.com/aarzilli/whydeadcode 参考
[1] Go版本1.22.0 : https://tonybai.com/2024/02/18/some-changes-in-go-1-22/
[2] 内联优化: https://tonybai.com/2022/10/17/understand-go-inlined-optimizes-by-example
[3]这里: https://github.com/bigwhite/experiments/tree/master/dead-code-elimination
[4] 充分利用死代码消除: https://golab.io/talks/getting-the-most-out-of-dead-code-elimination
[5] all: 二进制文件太大且不断增长: https://github.com/golang/go/issues/6853
[6] aarzilli/whydeadcode: https://github.com/aarzilli/whydeadcode
[7] 地鼠部落知识星球: https://public.zsxq.com/groups/51284458844544
[8]链接地址: https://m.do.co/c/bff6eed92687
用户评论
这篇文章说的有点道理! 确实有很多开发工具都是为了提升效率而存在,有些功能真的不是经常用到,反而会增加项目的体积。像 Go 这种语言本身就很注重简洁性和性能,在一些特定场景下确实会有瘦身优势。
有6位网友表示赞同!
同意作者观点,Go语言本身就比较简洁,代码量相对较少,这一点很有优势。可执行文件太大了,部署和传输都麻烦。希望未来更多开发者重视可执行文件的大小,提升效率!
有6位网友表示赞同!
我之前也遇到过这个问题,项目依赖很多第三方库,最终导致可执行文件很大,影响了性能和部署速度。Go语言自带的工具确实有很多好处,值得一试!
有14位网友表示赞同!
对于 Go 未用代码消除与可执行文件瘦身这一观点,我是持保留态度的。虽然 Go 以简洁著称,但一些复杂的项目可能依旧需要庞大的依赖库支撑。过度追求“瘦身”或许会影响到项目的完整性和功能性。
有8位网友表示赞同!
文章提到的方法很有启发性,确实可以有效减少可执行文件的大小,降低部署成本。希望未来开发工具能够更加注重效率和代码臃肿的问题,为开发者提供更轻量化的解决方案!
有18位网友表示赞同!
Go 语言追求简洁高效一直很让人佩服,但是对于大型项目来说,过度追求代码瘦身可能会限制开发的灵活性。我觉得要找到一个平衡点,既保证代码的简洁性,又保障项目的完整性和功能多样性!
有9位网友表示赞同!
可执行文件过大确实会带来很多问题,就像老电脑运行起来很慢一样,Go 的开发思路很有意思,能有效解决这个问题。希望更多开发语言能够汲取 Go 的经验,注重代码的轻量化设计!
有15位网友表示赞同!
我比较赞成 Go 未用代码消除与可执行文件瘦身这种思路,毕竟在现代软件世界中,效率和性能至上。Go 语言在这方面做得很好,希望能引领更多开发者的目光注意这些细节问题,提高软件的整体质量!
有12位网友表示赞同!
其实不止是 Go 语言,有很多编程语言都面临着代码臃肿和可执行文件过大的问题。需要开发社区一起努力探索更有效的解决方案,提升整个软件行业的效率和性能!
有12位网友表示赞同!
这段文字描述得很清晰了Go语言的优点,简洁、高效、对瘦身很重要,但是实际项目里会遇到很多复杂场景,是否真的能像文章那样完美“瘦身”可能还需要进一步实践和验证。
有7位网友表示赞同!
我觉得 Go 这种强调紧凑性,追求“瘦身”的设计理念挺好的,可以帮助开发者更加注重代码的结构和性能优化,避免冗余代码带来的负担。在开发过程中要保持这样的意识是十分重要的!
有17位网友表示赞同!
对于我来说,使用编程语言最重要的还是其功能性和易用性,而并非仅仅追求代码简洁程度或可执行文件的大小。 Go 虽然在这方面做得不错,但我更看重的是它能否满足我的项目需求。
有6位网友表示赞同!
文章的观点很有启发性,但也应该注意到,对于复杂的系统和应用程序来说,过于强调“瘦身”可能会造成功能的丢失或性能问题的出现,需要根据实际情况进行权衡!
有19位网友表示赞同!
在软件开发过程中,代码瘦身确实是一个很重要的目标,可以提升效率和性能。Go 语言的简洁特性在这个方面很有优势,值得开发者学习借鉴。但同时也要记住,功能完整性和可维护性同样重要!
有7位网友表示赞同!
我之前也尝试过 Go 开发项目,它的简洁风格的确让我印象深刻。 可执行文件大小的问题确实比较常见,希望更多开发工具能够像 Go 一样重视代码的效率和性能!
有11位网友表示赞同!
文章分析的很好,Go语言的编写思路確實注重性能和代码的可维护性。我认为在未来软件开发中,代码瘦身将会越来越重要,需要开发者们不断提升效率和创新思维!
有20位网友表示赞同!
Go 的设计理念确实很值得学习。它提醒我们应该注重代码的简洁性和可执行文件的大小,这对于提升软件的性能和用户体验有着重要的意义!”
有17位网友表示赞同!