Skip to content

Commit 835d5ba

Browse files
committed
performance: readFile -> mmap
1 parent 849ce50 commit 835d5ba

4 files changed

Lines changed: 80 additions & 3 deletions

File tree

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ require (
1212
github.com/ianlancetaylor/demangle v0.0.0-20260505044615-1ff4bf46051f
1313
github.com/muesli/termenv v0.16.0
1414
golang.org/x/arch v0.28.0
15+
golang.org/x/sys v0.38.0
1516
gopkg.in/yaml.v3 v3.0.1
1617
)
1718

@@ -32,6 +33,5 @@ require (
3233
github.com/muesli/cancelreader v0.2.2 // indirect
3334
github.com/rivo/uniseg v0.4.7 // indirect
3435
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
35-
golang.org/x/sys v0.38.0 // indirect
3636
golang.org/x/text v0.3.8 // indirect
3737
)

internal/binfile/binfile.go

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,8 @@ type File struct {
115115
Symbols []Symbol // sorted by Name
116116
Info *Info
117117

118-
raw []byte // entire file contents (source for the raw view + section data)
118+
raw []byte // entire file contents (mmap'd; source for the raw view + section data)
119+
unmap func() error // releases the mmap backing raw (nil-safe via Close)
119120
arch disasm.Arch
120121
entry uint64
121122
addrWidth int // hex digits in a printed address (8 or 16)
@@ -141,29 +142,34 @@ type lineEntry struct {
141142

142143
// Open reads path, detects its container format, and builds the neutral model.
143144
func Open(path string) (*File, error) {
144-
raw, err := os.ReadFile(path)
145+
raw, closer, err := mapFile(path)
145146
if err != nil {
146147
return nil, err
147148
}
148149
f := &File{
149150
Path: path,
150151
raw: raw,
152+
unmap: closer,
151153
sources: map[string][]string{},
152154
}
153155
switch {
154156
case len(raw) >= 4 && raw[0] == 0x7f && raw[1] == 'E' && raw[2] == 'L' && raw[3] == 'F':
155157
if err := f.loadELF(); err != nil {
158+
f.Close()
156159
return nil, err
157160
}
158161
case isMachO(raw):
159162
if err := f.loadMachO(); err != nil {
163+
f.Close()
160164
return nil, err
161165
}
162166
case len(raw) >= 2 && raw[0] == 'M' && raw[1] == 'Z':
163167
if err := f.loadPE(); err != nil {
168+
f.Close()
164169
return nil, err
165170
}
166171
default:
172+
f.Close()
167173
return nil, fmt.Errorf("unrecognised file format (not ELF, Mach-O, or PE)")
168174
}
169175

@@ -172,6 +178,18 @@ func Open(path string) (*File, error) {
172178
return f, nil
173179
}
174180

181+
// Close releases the memory-mapped file. Safe to call more than once; after it
182+
// the raw bytes (and anything slicing into them) must no longer be used.
183+
func (f *File) Close() error {
184+
if f == nil || f.unmap == nil {
185+
return nil
186+
}
187+
err := f.unmap()
188+
f.unmap = nil
189+
f.raw = nil
190+
return err
191+
}
192+
175193
// finalizeSymbols sorts symbols, fills in missing sizes, and builds the
176194
// address-indexed copy used for reverse lookups. Loaders append unsorted
177195
// symbols with their Size left at 0 when the container doesn't record one

internal/binfile/mmap_other.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
//go:build !unix
2+
3+
package binfile
4+
5+
import "os"
6+
7+
// mapFile falls back to reading the whole file on platforms where we don't wire
8+
// up mmap (e.g. Windows). The closer is a no-op.
9+
func mapFile(path string) (data []byte, closer func() error, err error) {
10+
b, err := os.ReadFile(path)
11+
if err != nil {
12+
return nil, nil, err
13+
}
14+
return b, func() error { return nil }, nil
15+
}

internal/binfile/mmap_unix.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
//go:build unix
2+
3+
package binfile
4+
5+
import (
6+
"os"
7+
8+
"golang.org/x/sys/unix"
9+
)
10+
11+
// mapFile memory-maps path read-only and returns the bytes together with a
12+
// closer that unmaps them. Mapping avoids copying the whole file into the heap
13+
// (a 140 MB binary is otherwise a 140 MB allocation + read); pages fault in from
14+
// the page cache only as the parser touches them. The returned slice aliases the
15+
// mapping, so it stays valid until closer is called — keep it for the File's
16+
// lifetime. Empty files fall back to a nil slice (mmap rejects zero length).
17+
func mapFile(path string) (data []byte, closer func() error, err error) {
18+
fh, err := os.Open(path)
19+
if err != nil {
20+
return nil, nil, err
21+
}
22+
defer fh.Close()
23+
fi, err := fh.Stat()
24+
if err != nil {
25+
return nil, nil, err
26+
}
27+
size := fi.Size()
28+
if size <= 0 {
29+
return nil, func() error { return nil }, nil
30+
}
31+
b, err := unix.Mmap(int(fh.Fd()), 0, int(size), unix.PROT_READ, unix.MAP_SHARED)
32+
if err != nil {
33+
// Fall back to a plain read when the file can't be mapped (e.g. a
34+
// special file); correctness over speed.
35+
data, rerr := os.ReadFile(path)
36+
if rerr != nil {
37+
return nil, nil, rerr
38+
}
39+
return data, func() error { return nil }, nil
40+
}
41+
// Advise the kernel we'll read it roughly sequentially; best-effort.
42+
_ = unix.Madvise(b, unix.MADV_WILLNEED)
43+
return b, func() error { return unix.Munmap(b) }, nil
44+
}

0 commit comments

Comments
 (0)