新聞中心
大家常規(guī)的認知是,Go 程序中聲明的類型越多,生成的二進制文件就越大。這個符合直覺,畢竟如果你寫的代碼不去操作定義的類型,那么定義一堆類型就沒有意義了。然而,鏈接器的部分工作就是檢測沒有被程序引用的函數(shù)(比如說它們是一個庫的一部分,其中只有一個子集的功能被使用),然后把它們從最后的編譯產(chǎn)出中刪除。常言道,“類型越多,二進制文件越大”,對于多數(shù) Go 程序還是正確的。

十載的南宮網(wǎng)站建設(shè)經(jīng)驗,針對設(shè)計、前端、開發(fā)、售后、文案、推廣等六對一服務(wù),響應(yīng)快,48小時及時工作處理。成都全網(wǎng)營銷推廣的優(yōu)勢是能夠根據(jù)用戶設(shè)備顯示端的尺寸不同,自動調(diào)整南宮建站的顯示方式,使網(wǎng)站能夠適用不同顯示終端,在瀏覽器中調(diào)整網(wǎng)站的寬度,無論在任何一種瀏覽器上瀏覽網(wǎng)站,都能展現(xiàn)優(yōu)雅布局與設(shè)計,從而大程度地提升瀏覽體驗。創(chuàng)新互聯(lián)從事“南宮網(wǎng)站設(shè)計”,“南宮網(wǎng)站推廣”以來,每個客戶項目都認真落實執(zhí)行。
本文中我會深入講解在 Go 程序的上下文中“相等”的意義,以及為什么像這樣的修改會對 Go 程序的大小有重大的影響。
定義兩個值相等
Go 的語法定義了“賦值”和“相等”的概念。賦值是把一個值賦給一個標識符的行為。并不是所有聲明的標識符都可以被賦值,如常量和函數(shù)就不可以。相等是通過檢查標識符的內(nèi)容是否相等來比較兩個標識符的行為。
作為強類型語言,“相同”的概念從根源上被植入標識符的類型中。兩個標識符只有是相同類型的前提下,才有可能相同。除此之外,值的類型定義了如何比較該類型的兩個值。
例如,整型是用算數(shù)方法進行比較的。對于指針類型,是否相等是指它們指向的地址是否相同。映射和通道等引用類型,跟指針類似,如果它們指向相同的地址,那么就認為它們是相同的。
上面都是按位比較相等的例子,即值占用的內(nèi)存的位模式是相同的,那么這些值就相等。這就是所謂的 memcmp,即內(nèi)存比較,相等是通過比較兩個內(nèi)存區(qū)域的內(nèi)容來定義的。
記住這個思路,我過會兒再來談。
結(jié)構(gòu)體相等
除了整型、浮點型和指針等標量類型,還有復(fù)合類型:結(jié)構(gòu)體。所有的結(jié)構(gòu)體以程序中的順序被排列在內(nèi)存中。因此下面這個聲明:
type S struct {a, b, c, d int64}
會占用 32 字節(jié)的內(nèi)存空間;a 占用 8 個字節(jié),b 占用 8 個字節(jié),以此類推。Go 的規(guī)則說如果結(jié)構(gòu)體所有的字段都是可以比較的,那么結(jié)構(gòu)體的值就是可以比較的。因此如果兩個結(jié)構(gòu)體所有的字段都相等,那么它們就相等。
a := S{1, 2, 3, 4}b := S{1, 2, 3, 4}fmt.Println(a == b) // 輸出 true
編譯器在底層使用 memcmp 來比較 a 的 32 個字節(jié)和 b 的 32 個字節(jié)。
填充和對齊
然而,在下面的場景下過分簡單化的按位比較的策略會返回錯誤的結(jié)果:
type S struct {a byteb uint64c int16d uint32}func main()a := S{1, 2, 3, 4}b := S{1, 2, 3, 4}fmt.Println(a == b) // 輸出 true}
編譯代碼后,這個比較表達式的結(jié)果還是 true,但是編譯器在底層并不能僅依賴比較 a 和 b 的位模式,因為結(jié)構(gòu)體有填充。
Go 要求結(jié)構(gòu)體的所有字段都對齊。2 字節(jié)的值必須從偶數(shù)地址開始,4 字節(jié)的值必須從 4 的倍數(shù)地址開始,以此類推 1。編譯器根據(jù)字段的類型和底層平臺加入了填充來確保字段都對齊。在填充之后,編譯器實際上看到的是 2:
type S struct {a byte_ [7]byte // 填充b uint64c int16_ [2]int16 // 填充d uint32}
填充的存在保證了字段正確對齊,而填充確實占用了內(nèi)存空間,但是填充字節(jié)的內(nèi)容是未知的。你可能會認為在 Go 中 填充字節(jié)都是 0,但實際上并不是 — 填充字節(jié)的內(nèi)容是未定義的。由于它們并不是被定義為某個確定的值,因此按位比較會因為分布在 s 的 24 字節(jié)中的 9 個填充字節(jié)不一樣而返回錯誤結(jié)果。
Go 通過生成所謂的相等函數(shù)來解決這個問題。在這個例子中,s 的相等函數(shù)只比較函數(shù)中的字段略過填充部分,這樣就能正確比較類型 s 的兩個值。
類型算法
呵,這是個很大的設(shè)置,說明了為什么,對于 Go 程序中定義的每種類型,編譯器都會生成幾個支持函數(shù),編譯器內(nèi)部把它們稱作類型的算法。如果類型是一個映射的鍵,那么除相等函數(shù)外,編譯器還會生成一個哈希函數(shù)。為了維持穩(wěn)定,哈希函數(shù)在計算結(jié)果時也會像相等函數(shù)一樣考慮諸如填充等因素。
憑直覺判斷編譯器什么時候生成這些函數(shù)實際上很難,有時并不明顯,(因為)這超出了你的預(yù)期,而且鏈接器也很難消除沒有被使用的函數(shù),因為反射往往導(dǎo)致鏈接器在裁剪類型時變得更保守。
通過禁止比較來減小二進制文件的大小
現(xiàn)在,我們來解釋一下 Brad 的修改。向類型添加一個不可比較的字段 3,結(jié)構(gòu)體也隨之變成不可比較的,從而強制編譯器不再生成相等函數(shù)和哈希函數(shù),規(guī)避了鏈接器對那些類型的消除,在實際應(yīng)用中減小了生成的二進制文件的大小。作為這項技術(shù)的一個例子,下面的程序:
package mainimport "fmt"func main() {type t struct {// _ [0][]byte // 取消注釋以阻止比較a byteb uint16c int32d uint64}var a tfmt.Println(a)}
用 Go 1.14.2(darwin/amd64)編譯,大小從 2174088 降到了 2174056,節(jié)省了 32 字節(jié)。單獨看節(jié)省的這 32 字節(jié)似乎微不足道,但是考慮到你的程序中每個類型及其傳遞閉包都會生成相等和哈希函數(shù),還有它們的依賴,這些函數(shù)的大小隨類型大小和復(fù)雜度的不同而不同,禁止它們會大大減小最終的二進制文件的大小,效果比之前使用 -ldflags="-s -w" 還要好。
最后總結(jié)一下,如果你不想把類型定義為可比較的,可以在源碼層級強制實現(xiàn)像這樣的奇技淫巧,會使生成的二進制文件變小。
新聞名稱:通過禁止比較讓Go二進制文件變小
標題路徑:http://fisionsoft.com.cn/article/cdppggs.html


咨詢
建站咨詢
