Golang 1.11.5版本中的巨型异常处理案例

Golang 1.11.5版本中的巨型异常处理案例 如果这个问题之前有人提过请见谅,但"我的二进制文件太大了!"的原因似乎有很多种,不太确定这是否是新问题。

我有一个中等规模(19千行代码,700KB)的Go项目,其中大约1千行代码和180KB是密集的初始化代码,看起来像这样:

var objList = []Obj{
fg('.', "White", gen(22, 15, desc("Blah blah blah blah. ", grow("thingy", speed(10, hp(150, visualrange(3, ac(14, dozy(10, blow("hit", "hurt", "4d8", blow("hit", "hurt", "4d8", isHidden(isMimic(isInvisible(isCold(hook("all", "none", resist("fear confusion sleep", intel(1, 0, invisithing("blabla")))))))))))))))))),
<跳过1000个类似条目>
}

这段代码中间有一个返回Obj的函数,然后通过每个外围函数调用按值传递回来。Obj结构体有几百字节。虽然不指望它超级高效,但考虑到它只在初始化时运行一次,用来替代外部配置文件,这样已经足够好了。如预期的那样,它花费的时间小于0.1秒——所以我宁愿不用更优化但更丑陋的方案来替换它……

然而它似乎会让二进制文件变得非常庞大——323MB,其中318MB是.rodata段,包含高度重复的二进制数据,类似于Obj结构的副本。通过验证这一点,向Obj结构添加128字节的数组会使大小增加到444MB,而使用lz4压缩后二进制文件可以缩小到约4.5MB。 (如果将添加的数组设置得更大,比如1KB,会出现如下错误: :1: prepwrite: bad off=1073741824 siz=1 s= 这表明它试图生成超过1GB的可执行文件,但碰到了某些内部限制。)

所以UPX是一个有效的解决方法(直到可执行文件超过1GB为止),gccgo是另一个方案(完全静态编译带调试信息时为18MB,动态发布版本小于4MB)。但是这两种方法都很慢(gccgo大约需要105秒,而gc只需要35秒),所以这并不理想。

strip对大小没有显著影响,因为所有内容都在.rodata中。更改编译器参数也没有帮助。我使用nm和readelf进行了检查,但它们没有显示异常大的符号——只有一个庞大且无特征的.rodata段。

我使用的是Linux AMD64的Go 1.11.5。

有什么建议吗?


更多关于Golang 1.11.5版本中的巨型异常处理案例的实战教程也可以访问 https://www.itying.com/category-94-b0.html

2 回复

我在同一个项目上尝试了 Go 1.12rc1 版本。 这是一个巨大的改进——从 370MB 降至 6.5MB,并且速度也快了很多(重建时间从 13 秒缩短至 3 倍!)。 如果你遇到类似问题,强烈推荐使用此版本。

更多关于Golang 1.11.5版本中的巨型异常处理案例的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


这是一个典型的Go编译器在处理大型静态初始化数据时的问题,特别是当这些数据通过函数调用链生成时。编译器会将所有中间计算值都嵌入到.rodata段中,导致二进制文件异常膨胀。以下是几种可行的解决方案:

方案1:使用代码生成替代静态初始化

创建一个代码生成工具,在编译前将静态数据预计算为序列化格式:

// build_data.go - 代码生成工具
package main

import (
    "encoding/gob"
    "os"
)

func generateData() []Obj {
    // 原有的初始化逻辑
    return []Obj{
        fg('.', "White", gen(22, 15, desc("Blah blah blah"))),
        // ... 更多条目
    }
}

func main() {
    data := generateData()
    file, _ := os.Create("data.gob")
    defer file.Close()
    encoder := gob.NewEncoder(file)
    encoder.Encode(data)
}

然后在主程序中加载:

// main.go
var objList []Obj

func init() {
    file, _ := os.Open("data.gob")
    defer file.Close()
    decoder := gob.NewDecoder(file)
    decoder.Decode(&objList)
}

编译时先运行 go run build_data.go 生成数据文件,再编译主程序。

方案2:使用外部配置文件

将数据移到JSON或类似的配置文件中:

// data.json
[
    {
        "char": ".",
        "color": "White", 
        "properties": {
            "x": 22,
            "y": 15,
            "description": "Blah blah blah"
        }
    }
    // ...
]

加载代码:

import "encoding/json"

func loadObjects() []Obj {
    data, _ := os.ReadFile("data.json")
    var objList []Obj
    json.Unmarshal(data, &objList)
    return objList
}

var objList = loadObjects()

方案3:运行时动态构建

如果数据结构允许,考虑在运行时按需构建:

type ObjBuilder struct {
    char  rune
    color string
    // 其他字段
}

func (b *ObjBuilder) WithChar(c rune) *ObjBuilder {
    b.char = c
    return b
}

func (b *ObjBuilder) WithColor(color string) *ObjBuilder {
    b.color = color
    return b
}

func (b *ObjBuilder) Build() Obj {
    return Obj{
        Char:  b.char,
        Color: b.color,
        // 其他字段初始化
    }
}

func initObjects() []Obj {
    var objects []Obj
    builder := &ObjBuilder{}
    
    objects = append(objects, 
        builder.WithChar('.').WithColor("White").Build(),
        // 更多构建调用
    )
    
    return objects
}

var objList = initObjects()

方案4:使用字符串压缩技术

如果必须保留静态初始化,可以考虑对重复字符串进行压缩:

var stringPool = map[string]string{
    "white": "White",
    "blah": "Blah blah blah",
    // 更多字符串映射
}

func getString(key string) string {
    return stringPool[key]
}

var objList = []Obj{
    fg('.', getString("white"), gen(22, 15, desc(getString("blah")))),
    // ...
}

第一种代码生成方案通常是最有效的,它完全避免了编译器处理大量静态数据的问题,同时保持了代码的可读性和编译速度。

回到顶部