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?
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.