diff --git a/gnovm/pkg/gnolang/bench_test.go b/gnovm/pkg/gnolang/bench_test.go index b638ab66cd0..2bc266ab9d3 100644 --- a/gnovm/pkg/gnolang/bench_test.go +++ b/gnovm/pkg/gnolang/bench_test.go @@ -1,6 +1,8 @@ package gnolang import ( + "path/filepath" + "runtime" "testing" ) @@ -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 +} diff --git a/gnovm/pkg/gnolang/nodes.go b/gnovm/pkg/gnolang/nodes.go index 445968a2c9c..048af665ecf 100644 --- a/gnovm/pkg/gnolang/nodes.go +++ b/gnovm/pkg/gnolang/nodes.go @@ -11,6 +11,7 @@ import ( "regexp" "strconv" "strings" + "unsafe" "github.com/gnolang/gno/gnovm" "go.uber.org/multierr" @@ -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 } @@ -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), }) } @@ -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)