-
Notifications
You must be signed in to change notification settings - Fork 389
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
gnovm/pkg/gnolang: ReadMemPackage should reuse byteslices from reading files as those byteslices are not modified; without incurring expensive byteslice->string conversion #3598
Labels
Comments
odeke-em
added a commit
to odeke-em/gno
that referenced
this issue
Jan 23, 2025
…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 gnolang#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 gnolang#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 gnolang#3435 Fixes gnolang#3598
odeke-em
added a commit
to odeke-em/gno
that referenced
this issue
Jan 23, 2025
…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 gnolang#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 gnolang#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 gnolang#3435 Fixes gnolang#3598
odeke-em
added a commit
to odeke-em/gno
that referenced
this issue
Jan 24, 2025
…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 gnolang#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 gnolang#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 gnolang#3435 Fixes gnolang#3598
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
While studying ReadMemPackage* I examined some profiles and noticed that we've got this RAM hog
if you squint close enough you'll notice a few things:
2.16GB 2.16GB 1331: Body: string(bz),
is another heavy handed allocation that is incurred from byteslice->string conversion yet in 1. we already paid the penalty but even more at no point do we mutate the body of each byteslice that was allocation previously.and the above comes from this benchmark addition
Remedy
We should be very conservative and try to reuse memory as much in the VM, and in the case above where we don't modify that memory we should hook into the runtime unsafe conversion of byteslice header to string header then to string which will reuse the byteslices allocated by
Results
The result is a hefty reduction in all dimensions
The text was updated successfully, but these errors were encountered: