Search code examples
gobinarylinkerdependenciesexecutable

How to check the size of packages linked into my Go code


Following up with How do I check the size of a Go project?

The conclusion was:

in order to get a true sense of how much extra weight importing certain packages, one has to look at all of the pkg's sub-dependencies as well.

That's totally understandable. My question is,

Is there anyway that I can know how much space each component is taking in my compiled binary, the Go runtime, the dependencies and sub-dependencies packages, and my own code.

I vaguely remember reading something like this before (when go enhanced its linker maybe).
If there has never been such discussion before, then is there any way the go or even c linker can look into the my compiled binary, and reveal something that I can further parse myself?


Solution

  • The binary will contain debug symbols which we can use to figure out how many space each package takes up.

    I wrote a basic program to do this since I don't know of any tool that does this:

    package main
    
    import (
        "debug/elf"
        "fmt"
        "os"
        "runtime"
        "sort"
        "strings"
    
        "github.com/go-delve/delve/pkg/proc"
    )
    
    func main() {
        // Use delve to decode the DWARF section
        binInfo := proc.NewBinaryInfo(runtime.GOOS, runtime.GOARCH)
        err := binInfo.AddImage(os.Args[1], 0)
        if err != nil {
            panic(err)
        }
    
        // Make a list of unique packages
        pkgs := make([]string, 0, len(binInfo.PackageMap))
        for _, fullPkgs := range binInfo.PackageMap {
            for _, fullPkg := range fullPkgs {
                exists := false
                for _, pkg := range pkgs {
                    if fullPkg == pkg {
                        exists = true
                        break
                    }
                }
                if !exists {
                    pkgs = append(pkgs, fullPkg)
                }
            }
        }
        // Sort them for a nice output
        sort.Strings(pkgs)
    
        // Parse the ELF file ourselfs
        elfFile, err := elf.Open(os.Args[1])
        if err != nil {
            panic(err)
        }
    
        // Get the symbol table
        symbols, err := elfFile.Symbols()
        if err != nil {
            panic(err)
        }
    
        usage := make(map[string]map[string]int)
    
        for _, sym := range symbols {
            if sym.Section == elf.SHN_UNDEF || sym.Section >= elf.SectionIndex(len(elfFile.Sections)) {
                continue
            }
    
            sectionName := elfFile.Sections[sym.Section].Name
    
            symPkg := ""
            for _, pkg := range pkgs {
                if strings.HasPrefix(sym.Name, pkg) {
                    symPkg = pkg
                    break
                }
            }
            // Symbol doesn't belong to a known package
            if symPkg == "" {
                continue
            }
    
            pkgStats := usage[symPkg]
            if pkgStats == nil {
                pkgStats = make(map[string]int)
            }
    
            pkgStats[sectionName] += int(sym.Size)
            usage[symPkg] = pkgStats
        }
    
        for _, pkg := range pkgs {
            sections, exists := usage[pkg]
            if !exists {
                continue
            }
    
            fmt.Printf("%s:\n", pkg)
            for section, size := range sections {
                fmt.Printf("%15s: %8d bytes\n", section, size)
            }
            fmt.Println()
        }
    }
    

    Now the actual space used is divided over multiple section(.text for code, .bss for zero initialized data, .data for global vars, ect.). This example lists the size per section, but you can modify the code to get the total if that is what you prefer.

    Here is the outputs it generates from its own binary:

    bufio:
              .text:    12733 bytes
         .noptrdata:       64 bytes
               .bss:      176 bytes
            .rodata:       72 bytes
    
    bytes:
               .bss:       48 bytes
            .rodata:       64 bytes
              .text:    12617 bytes
         .noptrdata:      320 bytes
    
    compress/flate:
              .text:    20385 bytes
         .noptrdata:      248 bytes
               .bss:     2112 bytes
          .noptrbss:       12 bytes
            .rodata:       48 bytes
    
    compress/zlib:
              .text:     4138 bytes
         .noptrdata:       96 bytes
               .bss:       48 bytes
    
    container/list:
              .text:     4016 bytes
    
    context:
              .text:      387 bytes
         .noptrdata:       72 bytes
               .bss:       40 bytes
    
    crypto:
              .text:    20982 bytes
         .noptrdata:      416 bytes
               .bss:       96 bytes
            .rodata:       58 bytes
          .noptrbss:        3 bytes
    
    debug/dwarf:
            .rodata:     1088 bytes
              .text:   113878 bytes
         .noptrdata:      247 bytes
               .bss:       64 bytes
    
    debug/elf:
            .rodata:      168 bytes
              .text:    36557 bytes
         .noptrdata:      112 bytes
              .data:     5160 bytes
               .bss:       16 bytes
    
    debug/macho:
              .text:    22980 bytes
         .noptrdata:       96 bytes
              .data:      456 bytes
            .rodata:       80 bytes
    
    debug/pe:
              .text:    26004 bytes
         .noptrdata:       96 bytes
            .rodata:      288 bytes
    
    encoding/base64:
               .bss:       32 bytes
            .rodata:       48 bytes
              .text:      846 bytes
         .noptrdata:       56 bytes
    
    encoding/binary:
              .text:    27108 bytes
         .noptrdata:       72 bytes
               .bss:       56 bytes
            .rodata:      136 bytes
    
    encoding/hex:
               .bss:       16 bytes
              .text:      288 bytes
         .noptrdata:       64 bytes
    
    encoding/json:
            .rodata:      108 bytes
              .text:     2930 bytes
         .noptrdata:      128 bytes
               .bss:       80 bytes
    
    errors:
            .rodata:       48 bytes
              .text:      744 bytes
         .noptrdata:       40 bytes
               .bss:       16 bytes
    
    fmt:
              .text:    72010 bytes
         .noptrdata:      136 bytes
              .data:      104 bytes
               .bss:       32 bytes
            .rodata:      720 bytes
    
    github.com/cilium/ebpf:
              .text:   170860 bytes
         .noptrdata:     1405 bytes
               .bss:      608 bytes
            .rodata:     3971 bytes
              .data:       16 bytes
          .noptrbss:        8 bytes
    
    github.com/go-delve/delve/pkg/dwarf/frame:
              .text:    18304 bytes
         .noptrdata:       80 bytes
               .bss:        8 bytes
            .rodata:      211 bytes
    
    github.com/go-delve/delve/pkg/dwarf/godwarf:
              .text:    40431 bytes
         .noptrdata:      144 bytes
            .rodata:      352 bytes
    
    github.com/go-delve/delve/pkg/dwarf/line:
               .bss:       48 bytes
            .rodata:      160 bytes
              .text:    24069 bytes
         .noptrdata:       96 bytes
    
    github.com/go-delve/delve/pkg/dwarf/loclist:
         .noptrdata:       64 bytes
            .rodata:       64 bytes
              .text:     4538 bytes
    
    github.com/go-delve/delve/pkg/dwarf/op:
              .text:    31142 bytes
         .noptrdata:       80 bytes
               .bss:       72 bytes
            .rodata:     5313 bytes
    
    github.com/go-delve/delve/pkg/dwarf/reader:
         .noptrdata:       72 bytes
               .bss:       16 bytes
            .rodata:       24 bytes
              .text:     8037 bytes
    
    github.com/go-delve/delve/pkg/dwarf/regnum:
               .bss:       40 bytes
            .rodata:     2760 bytes
              .text:     3943 bytes
         .noptrdata:       48 bytes
    
    github.com/go-delve/delve/pkg/dwarf/util:
              .text:     4028 bytes
         .noptrdata:       64 bytes
            .rodata:       96 bytes
    
    github.com/go-delve/delve/pkg/elfwriter:
              .text:     3394 bytes
         .noptrdata:       48 bytes
            .rodata:       48 bytes
    
    github.com/go-delve/delve/pkg/goversion:
         .noptrdata:      104 bytes
               .bss:       64 bytes
            .rodata:      160 bytes
              .text:     4415 bytes
    
    github.com/go-delve/delve/pkg/logflags:
               .bss:       32 bytes
            .rodata:       40 bytes
              .text:     2610 bytes
         .noptrdata:      136 bytes
          .noptrbss:        3 bytes
    
    github.com/go-delve/delve/pkg/proc:
              .text:   432477 bytes
         .noptrdata:      718 bytes
              .data:     1448 bytes
               .bss:      592 bytes
            .rodata:    10106 bytes
    
    github.com/go-delve/delve/pkg/version:
              .text:     1509 bytes
         .noptrdata:       72 bytes
              .data:      112 bytes
            .rodata:       40 bytes
    
    github.com/hashicorp/golang-lru/simplelru:
              .text:     3911 bytes
         .noptrdata:       32 bytes
            .rodata:      160 bytes
    
    github.com/sirupsen/logrus:
          .noptrbss:       20 bytes
            .rodata:      696 bytes
              .text:    40175 bytes
         .noptrdata:      204 bytes
              .data:       64 bytes
               .bss:       56 bytes
    
    go/ast:
              .text:    24407 bytes
         .noptrdata:      104 bytes
              .data:      112 bytes
            .rodata:      120 bytes
    
    go/constant:
               .bss:        8 bytes
            .rodata:      824 bytes
              .text:    33910 bytes
         .noptrdata:       88 bytes
    
    go/parser:
            .rodata:     1808 bytes
              .text:    78751 bytes
         .noptrdata:      136 bytes
               .bss:       32 bytes
    
    go/printer:
              .text:    77202 bytes
         .noptrdata:      113 bytes
              .data:       24 bytes
            .rodata:     1504 bytes
    
    go/scanner:
            .rodata:      240 bytes
              .text:    18594 bytes
         .noptrdata:       93 bytes
              .data:       24 bytes
    
    go/token:
         .noptrdata:       72 bytes
              .data:     1376 bytes
               .bss:        8 bytes
            .rodata:      192 bytes
              .text:     7154 bytes
    
    golang.org/x/arch/arm64/arm64asm:
            .rodata:      856 bytes
              .text:   116428 bytes
         .noptrdata:       80 bytes
               .bss:       80 bytes
              .data:    46128 bytes
    
    golang.org/x/arch/x86/x86asm:
         .noptrdata:    29125 bytes
               .bss:      112 bytes
              .data:    20928 bytes
            .rodata:     1252 bytes
              .text:    76721 bytes
    
    golang.org/x/sys/unix:
              .text:     1800 bytes
         .noptrdata:      128 bytes
            .rodata:       70 bytes
              .data:       80 bytes
    
    hash/adler32:
              .text:     1013 bytes
         .noptrdata:       40 bytes
    
    internal/bytealg:
            .rodata:       56 bytes
          .noptrbss:        8 bytes
              .text:     1462 bytes
         .noptrdata:       32 bytes
    
    internal/cpu:
            .rodata:      500 bytes
          .noptrbss:      416 bytes
         .noptrdata:        8 bytes
               .bss:       24 bytes
              .text:     3017 bytes
    
    internal/fmtsort:
              .text:     7443 bytes
         .noptrdata:       40 bytes
            .rodata:       40 bytes
    
    internal/oserror:
              .text:      500 bytes
         .noptrdata:       40 bytes
               .bss:       80 bytes
    
    internal/poll:
              .text:    31565 bytes
            .rodata:      192 bytes
         .noptrdata:      112 bytes
              .data:       96 bytes
               .bss:       64 bytes
          .noptrbss:       12 bytes
    
    internal/reflectlite:
              .text:    13761 bytes
         .noptrdata:       32 bytes
              .data:      456 bytes
               .bss:       24 bytes
            .rodata:      496 bytes
    
    internal/syscall/unix:
            .rodata:       72 bytes
              .text:      708 bytes
         .noptrdata:       40 bytes
          .noptrbss:        4 bytes
    
    internal/testlog:
              .text:      827 bytes
         .noptrdata:       32 bytes
          .noptrbss:       12 bytes
               .bss:       16 bytes
            .rodata:       72 bytes
    
    io:
         .noptrdata:      240 bytes
               .bss:      272 bytes
              .data:       56 bytes
          .noptrbss:        0 bytes
            .rodata:      128 bytes
              .text:    10824 bytes
    
    log:
              .text:      188 bytes
         .noptrdata:       80 bytes
               .bss:        8 bytes
    
    main:
              .text:     3002 bytes
         .noptrdata:       80 bytes
            .rodata:      104 bytes
    
    math:
              .data:      136 bytes
               .bss:     2672 bytes
              .text:   184385 bytes
         .noptrdata:    10211 bytes
            .rodata:     2076 bytes
          .noptrbss:        2 bytes
    
    net:
              .text:    24417 bytes
         .noptrdata:      236 bytes
              .data:      240 bytes
               .bss:      584 bytes
          .noptrbss:       16 bytes
            .rodata:       48 bytes
    
    os:
               .bss:      264 bytes
              .data:       32 bytes
            .rodata:      352 bytes
              .text:    46276 bytes
         .noptrdata:      296 bytes
          .noptrbss:        1 bytes
    
    path:
              .text:     9378 bytes
         .noptrdata:      136 bytes
               .bss:       48 bytes
            .rodata:       48 bytes
    
    reflect:
          .noptrbss:        1 bytes
              .text:    97417 bytes
         .noptrdata:       72 bytes
            .rodata:     1728 bytes
              .data:      456 bytes
               .bss:      160 bytes
    
    regexp:
            .rodata:      968 bytes
              .text:   126451 bytes
         .noptrdata:      558 bytes
               .bss:      296 bytes
          .noptrbss:       16 bytes
              .data:      816 bytes
    
    runtime:
          .noptrbss:    20487 bytes
              .data:     8520 bytes
               .bss:   184836 bytes
              .tbss:        8 bytes
          .typelink:     9020 bytes
         .gopclntab:        0 bytes
              .text:   408713 bytes
         .noptrdata:     4347 bytes
            .rodata:    23102 bytes
          .itablink:     2952 bytes
    
    sort:
              .text:    13055 bytes
         .noptrdata:       32 bytes
              .data:       16 bytes
            .rodata:       24 bytes
    
    strconv:
              .text:    45928 bytes
         .noptrdata:    17015 bytes
              .data:     1680 bytes
               .bss:       32 bytes
            .rodata:      144 bytes
    
    strings:
              .text:    21070 bytes
         .noptrdata:      320 bytes
            .rodata:      168 bytes
    
    sync:
            .rodata:      476 bytes
         .noptrdata:       56 bytes
               .bss:       56 bytes
          .noptrbss:        8 bytes
              .text:    14288 bytes
    
    syscall:
         .noptrdata:      127 bytes
            .rodata:      978 bytes
          .noptrbss:       76 bytes
               .bss:      264 bytes
              .data:     2720 bytes
              .text:    33728 bytes
    
    text/tabwriter:
              .data:       96 bytes
            .rodata:       88 bytes
              .text:     8002 bytes
         .noptrdata:       46 bytes
    
    text/template:
              .text:   166284 bytes
         .noptrdata:      316 bytes
          .noptrbss:        8 bytes
               .bss:      176 bytes
              .data:      376 bytes
            .rodata:     3152 bytes
    
    time:
              .text:    83290 bytes
         .noptrdata:      164 bytes
              .data:      912 bytes
               .bss:      208 bytes
          .noptrbss:       20 bytes
            .rodata:      832 bytes
    
    unicode:
         .noptrdata:    50398 bytes
              .data:    15248 bytes
               .bss:       40 bytes
          .noptrbss:        0 bytes
              .text:    27198 bytes
    

    Note this program isn't perfect, it only works on Linux/Mac since it relies on ELF. I am sure you can do a similar thing for Windows PE files, but that would have taken me to much time.

    Also this program ignores some parts of the go runtime, but I am guessing that is not the most important to you.