Skip to content

Commit

Permalink
perf(gnovm/pkg/gnolang): make ReadMemPackageFromList singly use bytes…
Browse files Browse the repository at this point in the history
…lices from os.ReadFile

This change takes advantage of the fact that we can singly use the
byteslices that were already allocated from os.ReadFile, given that
the places that their string(bz) are invoked don't have any unsafe
usages that then would modify those strings.

This change shaves off considerable CPU and RAM and is in the spirit
of having a fast VM that can be relied on and not stymied per se by
garbage collection. With it, all the RAM contributions for:
PackageNameFromFile and gnovm.MemFile disappear shaving off 4+GB
and no longer even appearing as a direct RAM contributor in the top 10.

```shell
$ benchstat before.txt after.txt
name              old time/op    new time/op    delta
ReadMemPackage-8     101µs ± 8%      89µs ± 4%  -11.54%  (p=0.000 n=9+10)

name              old alloc/op   new alloc/op   delta
ReadMemPackage-8    64.1kB ± 0%    34.2kB ± 0%  -46.72%  (p=0.001 n=8+9)

name              old allocs/op  new allocs/op  delta
ReadMemPackage-8      59.0 ± 0%      56.0 ± 0%   -5.08%  (p=0.000 n=10+10)
```

and then now RAM usage profiles show

```shell
Total: 7.41GB
ROUTINE ======================== github.com/gnolang/gno/gnovm/pkg/gnolang.ReadMemPackageFromList in /Users/emmanuelodeke/go/src/github.com/gnolang/gno/gnovm/pkg/gnolang/nodes.go
      34MB     6.98GB (flat, cum) 94.14% of Total
         .          .   1310:func ReadMemPackageFromList(list []string, pkgPath string) (*gnovm.MemPackage, error) {
   14.50MB    14.50MB   1311:	memPkg := &gnovm.MemPackage{Path: pkgPath}
         .          .   1312:	var pkgName Name
         .          .   1313:	for _, fpath := range list {
         .          .   1314:		fname := filepath.Base(fpath)
         .     4.35GB   1315:		bz, err := os.ReadFile(fpath)
         .          .   1316:		if err != nil {
         .          .   1317:			return nil, err
         .          .   1318:		}
         .          .   1319:		// XXX: should check that all pkg names are the same (else package is invalid)
         .          .   1320:		if pkgName == "" && strings.HasSuffix(fname, ".gno") {
         .          .   1321:			// The string derived from bz is never modified inside PackageNameFromFileBody
         .          .   1322:			// hence shave these unnecessary allocations by an unsafe
         .          .   1323:			// byteslice->string conversion per #3598
         .     2.59GB   1324:			pkgName, err = PackageNameFromFileBody(fname, unsafeBytesliceToStr(bz))
         .          .   1325:			if err != nil {
         .          .   1326:				return nil, err
         .          .   1327:			}
         .          .   1328:			if strings.HasSuffix(string(pkgName), "_test") {
         .          .   1329:				pkgName = pkgName[:len(pkgName)-len("_test")]
         .          .   1330:			}
         .          .   1331:		}
    6.50MB     6.50MB   1332:		memPkg.Files = append(memPkg.Files,
      13MB       13MB   1333:			&gnovm.MemFile{
         .          .   1334:				Name: fname,
         .          .   1335:				// The string derived from bz is never modified, hence to shave
         .          .   1336:				// unnecessary allocations, perform this unsafe byteslice->string
         .          .   1337:				// conversion per #3598
         .          .   1338:				// and the Go garbage collector will knows to garbage collect
         .          .   1339:				// bz when no other object points to that memory.
         .          .   1340:				Body: unsafeBytesliceToStr(bz),
         .          .   1341:			})
         .          .   1342:	}
         .          .   1343:
         .          .   1344:	// If no .gno files are present, package simply does not exist.
         .          .   1345:	if !memPkg.IsEmpty() {
         .     7.01MB   1346:		if err := validatePkgName(string(pkgName)); err != nil {
         .          .   1347:			return nil, err
         .          .   1348:		}
         .          .   1349:		memPkg.Name = string(pkgName)
         .          .   1350:	}
```

but previously looked like

```shell
ROUTINE ======================== github.com/gnolang/gno/gnovm/pkg/gnolang.ReadMemPackageFromList in /Users/emmanuelodeke/go/src/github.com/gnolang/gno/gnovm/pkg/gnolang/nodes.go
    5.55GB    11.46GB (flat, cum) 97.01% of Total
         .          .   1309:func ReadMemPackageFromList(list []string, pkgPath string) (*gnovm.MemPackage, error) {
      13MB       13MB   1310:	memPkg := &gnovm.MemPackage{Path: pkgPath}
         .          .   1311:	var pkgName Name
         .          .   1312:	for _, fpath := range list {
         .          .   1313:		fname := filepath.Base(fpath)
         .     3.66GB   1314:		bz, err := os.ReadFile(fpath)
         .          .   1315:		if err != nil {
         .          .   1316:			return nil, err
         .          .   1317:		}
         .          .   1318:		// XXX: should check that all pkg names are the same (else package is invalid)
         .          .   1319:		if pkgName == "" && strings.HasSuffix(fname, ".gno") {
    2.02GB     4.27GB   1320:			pkgName, err = PackageNameFromFileBody(fname, string(bz))
         .          .   1321:			if err != nil {
         .          .   1322:				return nil, err
         .          .   1323:			}
         .          .   1324:			if strings.HasSuffix(string(pkgName), "_test") {
         .          .   1325:				pkgName = pkgName[:len(pkgName)-len("_test")]
         .          .   1326:			}
         .          .   1327:		}
    4.50MB     4.50MB   1328:		memPkg.Files = append(memPkg.Files,
    9.50MB     9.50MB   1329:			&gnovm.MemFile{
         .          .   1330:				Name: fname,
    3.50GB     3.50GB   1331:				Body: string(bz),
         .          .   1332:			})
         .          .   1333:	}
         .          .   1334:
         .          .   1335:	// If no .gno files are present, package simply does not exist.
         .          .   1336:	if !memPkg.IsEmpty() {
         .     6.51MB   1337:		if err := validatePkgName(string(pkgName)); err != nil {
         .          .   1338:			return nil, err
         .          .   1339:		}
         .          .   1340:		memPkg.Name = string(pkgName)
         .          .   1341:	}
```

Updates #3435
Fixes #3598
  • Loading branch information
odeke-em committed Jan 23, 2025
1 parent 8e1c532 commit b4ea83a
Show file tree
Hide file tree
Showing 2 changed files with 42 additions and 2 deletions.
24 changes: 24 additions & 0 deletions gnovm/pkg/gnolang/bench_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package gnolang

import (
"path/filepath"
"runtime"
"testing"
)

Expand Down Expand Up @@ -29,3 +31,25 @@ func BenchmarkPkgIDFromPkgPath(b *testing.B) {
}
sink = nil
}

func BenchmarkReadMemPackage(b *testing.B) {
_, currentFile, _, ok := runtime.Caller(0)
_ = ok // Appease golang-ci
rootOfRepo, err := filepath.Abs(filepath.Join(filepath.Dir(currentFile), "..", "..", ".."))
if err != nil {
b.Fatal(err)
}
demoDir := filepath.Join(rootOfRepo, "examples", "gno.land", "p", "demo")
ufmtDir := filepath.Join(demoDir, "ufmt")
b.ReportAllocs()
b.ResetTimer()

for i := 0; i < b.N; i++ {
sink = MustReadMemPackage(ufmtDir, "ufmt")
}

if sink == nil {
b.Fatal("Benchmark did not run!")
}
sink = nil
}
20 changes: 18 additions & 2 deletions gnovm/pkg/gnolang/nodes.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"regexp"
"strconv"
"strings"
"unsafe"

"github.com/gnolang/gno/gnovm"
"go.uber.org/multierr"
Expand Down Expand Up @@ -1317,7 +1318,10 @@ func ReadMemPackageFromList(list []string, pkgPath string) (*gnovm.MemPackage, e
}
// XXX: should check that all pkg names are the same (else package is invalid)
if pkgName == "" && strings.HasSuffix(fname, ".gno") {
pkgName, err = PackageNameFromFileBody(fname, string(bz))
// The string derived from bz is never modified inside PackageNameFromFileBody
// hence shave these unnecessary allocations by an unsafe
// byteslice->string conversion per https://github.com/gnolang/gno/issues/3598
pkgName, err = PackageNameFromFileBody(fname, unsafeBytesliceToStr(bz))
if err != nil {
return nil, err
}
Expand All @@ -1328,7 +1332,12 @@ func ReadMemPackageFromList(list []string, pkgPath string) (*gnovm.MemPackage, e
memPkg.Files = append(memPkg.Files,
&gnovm.MemFile{
Name: fname,
Body: string(bz),
// The string derived from bz is never modified, hence to shave
// unnecessary allocations, perform this unsafe byteslice->string
// conversion per https://github.com/gnolang/gno/issues/3598
// and the Go garbage collector will knows to garbage collect
// bz when no other object points to that memory.
Body: unsafeBytesliceToStr(bz),
})
}

Expand All @@ -1343,6 +1352,13 @@ func ReadMemPackageFromList(list []string, pkgPath string) (*gnovm.MemPackage, e
return memPkg, nil
}

// unsafeBytesliceToStr helps rid the unnecessary byteslice->string conversion
// in instances where we definitely would not be modifying the input byteslice
// such as in ReadMemPackageFromList.
func unsafeBytesliceToStr(bz []byte) string {
return unsafe.String(&bz[0], len(bz))
}

// MustReadMemPackageFromList is a wrapper around [ReadMemPackageFromList] that panics on error.
func MustReadMemPackageFromList(list []string, pkgPath string) *gnovm.MemPackage {
pkg, err := ReadMemPackageFromList(list, pkgPath)
Expand Down

0 comments on commit b4ea83a

Please sign in to comment.