Skip to content
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

examples(allocation): ensure message outlives host call #1436

Merged
merged 2 commits into from
May 5, 2023
Merged

examples(allocation): ensure message outlives host call #1436

merged 2 commits into from
May 5, 2023

Conversation

ncruces
Copy link
Collaborator

@ncruces ncruces commented May 4, 2023

Also, incorporate guidance from TinyGo to use unsafe.String rather than unsafe.SliceHeader.

Also, incorporate guidance from TinyGo to use unsafe.String
rather than unsafe.SliceHeader.

Signed-off-by: Nuno Cruces <[email protected]>
@ncruces ncruces marked this pull request as draft May 4, 2023 11:58
@ncruces
Copy link
Collaborator Author

ncruces commented May 4, 2023

Oh jeez. This is what I get for going off grid: managed to miss #1434.
Converted to draft and joining discussion there.

Copy link
Contributor

@codefromthecrypt codefromthecrypt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

approved, except you want to regen the wasm with make build.examples.tinygo as it seems to be a lot bigger than before, probably from different flags.

I added notes to save the days around special casing, especially why we aren't doing it and why it is complicated to explain. Having the notes here is ok, and the only other place it might make sense is a new file examples/allocation/tinygo/RATIONALE.md or in the site doc, as the explanation is extremely weedy and also we aren't special casing anyway!

ptr := &buf[0]
unsafePtr := uintptr(unsafe.Pointer(ptr))
return uint32(unsafePtr), uint32(len(buf))
ptr := unsafe.Pointer(unsafe.StringData(s))
Copy link
Contributor

@codefromthecrypt codefromthecrypt May 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

summary of a chat with @aykevl on this is that even though StringData can return nil the latter cast to uintptr is safe.

Also, special casing len(s) zero to return early with 0, 0 is both unnecessary and may slow down the code. Plus, we'd have to clarify that empty isn't always ptr 0, for example a result of a function that reslices a non-empty string to zero length. If we did early exit, we could say "the input ptr might not really be zero due to reslicing, however a read of zero bytes is noop on the host for any ptr value."

Copy link

@aykevl aykevl May 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, special casing len(s) zero to return early with 0, 0 is both unnecessary and may slow down the code.

Yes, but the slowdown will probably so small it's not even noticeable. We're talking about just a few wasm instructions.

Plus, we'd have to clarify that empty isn't always ptr 0, for example a result of a function that reslices a non-empty string to zero length. If we did early exit, we could say "the input ptr might not really be zero due to reslicing, however a read of zero bytes is noop on the host for any ptr value."

One way to not have to clarify this is to return 0, 0 when len(s) == 0. That makes the interface simpler on the other end, in case people accidentally check the pointer value instead of the length.

Anyway, while both ways are correct, I think checking for len(s) == 0 will lead to a more robust API for users. But this is your call, of course, so so what you think is best.

// compatible with WebAssembly numeric types. The pointer is not automatically
// managed by TinyGo hence it must be freed by the host.
// compatible with WebAssembly numeric types.
// The pointer is not automatically managed by TinyGo hence it must be freed by the host.
func stringToLeakedPtr(s string) (uint32, uint32) {
size := C.ulong(len(s))
Copy link
Contributor

@codefromthecrypt codefromthecrypt May 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we aren't special casing len(s) == 0 here (to return ptr=0, size=0), and I don't recommend doing so as it is so complicated to explain. So fine as-is.

I'll explain anyway just to save the research on the special casing which was hard to track down.

Unlike stringToPtr, the host must call free on the ptr result of stringToLeakedPtr. If we returned ptr=0 manually, instead of proceeding to the logic below on len(s)==0, it would be ok even though it might actually slow the code.

When the host calls the "free" function exported by the guest with wasm i32(0), it ends up calling TinyGo's libc_free function.

TinyGo's compiler converts zero to unsafe.Pointer(nil) on all architectures, including wasm32, so this means libc_free is called with nil on i32(0), same as any other function export that has an unsafe.Pointer param. An unrelated side effect is, you can't represent memory offset 0 with an unsafe.Pointer, as it cannot be differentiated from nil.

Anyway, TinyGo's libc_free is following the C specification for free which says "ptr is NULL, no operation is performed". This means the passing zero will work not just with the default libc_free, but any other "free" export someone might have wired in.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ncruces mentioned that being unable to access offset 0 is a feature not a bug, and it could be a good practice to actually enforce code never tries to use offset zero.

If anyone ends up writing a reference based on these notes in TinyGo, our TinyGo site doc, tinymem RATIONALE.md, or just a personal site, this seems solid advice to consider.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I should point out this quirk of malloc:

If size is 0, then malloc() returns either NULL, or a unique pointer value that can later be successfully passed to free().

TinyGo will return nil for malloc(0) and I don't see a reason to change this, but there is the possibility that other heap implementations will do something different. (In fact, the internal TinyGo memory allocator runtime.alloc returns a non-nil pointer when allocating a zero-length object).

Also, unrelated:

@ncruces mentioned that being unable to access offset 0 is a feature not a bug, and it could be a good practice to actually enforce code never tries to use offset zero.

There is actually a more subtle reason to enforce this:

  • Dereferencing a nil pointer will lead to a panic in TinyGo, as the Go language specification says it should.
  • If you work around this (using unsafe or whatever), you may find that LLVM also assumes nil pointers are invalid. If it sees you are dereferencing a nil pointer, it will consider this undefined behavior and act accordingly (probably by deleting code as it assumes undefined behavior is unreachable).

@evacchi
Copy link
Contributor

evacchi commented May 5, 2023

rebuilt with make build.examples.tinygo (after rebasing)

❯ tinygo version
tinygo version 0.27.0 darwin/arm64 (using go version go1.20.4 and LLVM version 15.0.7)

results

# without this commit
$ go test -run='^$' -bench '^BenchmarkAllocation.*' ./internal/integration_test/vs/compiler ./internal/integration_test/vs/interpreter -count=6 > old.txt
# with this commit
$ go test -run='^$' -bench '^BenchmarkAllocation.*' ./internal/integration_test/vs/compiler ./internal/integration_test/vs/interpreter -count=6 > new.txt
$ benchstat old.txt new.txt 
goos: darwin
goarch: arm64
pkg: github.com/tetratelabs/wazero/internal/integration_test/vs/compiler
                          │   old.txt   │              new.txt               │
                          │   sec/op    │   sec/op     vs base               │
Allocation/Compile-10       3.664m ± 0%   3.647m ± 1%        ~ (p=0.065 n=6)
Allocation/Instantiate-10   149.5µ ± 4%   151.5µ ± 3%        ~ (p=0.937 n=6)
Allocation/Call-10          1.562µ ± 0%   1.403µ ± 0%  -10.15% (p=0.002 n=6)
geomean                     94.91µ        91.87µ        -3.21%

                          │   old.txt    │               new.txt                │
                          │     B/op     │     B/op      vs base                │
Allocation/Compile-10       2.404Mi ± 0%   2.400Mi ± 0%  -0.16% (p=0.002 n=6)
Allocation/Instantiate-10   318.3Ki ± 0%   318.3Ki ± 0%       ~ (p=0.937 n=6)
Allocation/Call-10            48.00 ± 0%     48.00 ± 0%       ~ (p=1.000 n=6) ¹
geomean                     33.24Ki        33.22Ki       -0.05%
¹ all samples are equal

                          │   old.txt   │               new.txt               │
                          │  allocs/op  │  allocs/op   vs base                │
Allocation/Compile-10       1.824k ± 0%   1.830k ± 0%  +0.33% (p=0.002 n=6)
Allocation/Instantiate-10    812.0 ± 0%    812.0 ± 0%       ~ (p=1.000 n=6) ¹
Allocation/Call-10           5.000 ± 0%    5.000 ± 0%       ~ (p=1.000 n=6) ¹
geomean                      194.9         195.1       +0.11%
¹ all samples are equal

pkg: github.com/tetratelabs/wazero/internal/integration_test/vs/interpreter
                          │   old.txt   │              new.txt              │
                          │   sec/op    │   sec/op     vs base              │
Allocation/Compile-10       1.266m ± 3%   1.264m ± 0%       ~ (p=0.310 n=6)
Allocation/Instantiate-10   99.10µ ± 6%   99.35µ ± 4%       ~ (p=0.699 n=6)
Allocation/Call-10          32.92µ ± 0%   29.89µ ± 1%  -9.21% (p=0.002 n=6)
geomean                     160.4µ        155.4µ       -3.14%

                          │   old.txt    │               new.txt               │
                          │     B/op     │     B/op      vs base               │
Allocation/Compile-10       1.448Mi ± 0%   1.445Mi ± 0%   -0.21% (p=0.002 n=6)
Allocation/Instantiate-10   219.7Ki ± 0%   219.7Ki ± 0%        ~ (p=0.463 n=6)
Allocation/Call-10          2.223Ki ± 0%   1.944Ki ± 0%  -12.54% (p=0.002 n=6)
geomean                     89.79Ki        85.81Ki        -4.44%

                          │   old.txt   │               new.txt                │
                          │  allocs/op  │  allocs/op   vs base                 │
Allocation/Compile-10       1.065k ± 0%   1.070k ± 0%   +0.47% (p=0.002 n=6)
Allocation/Instantiate-10    756.0 ± 0%    756.0 ± 0%        ~ (p=1.000 n=6) ¹
Allocation/Call-10           97.00 ± 0%    85.00 ± 1%  -12.37% (p=0.002 n=6)
geomean                      427.4         409.7        -4.16%
¹ all samples are equal

Copy link
Contributor

@codefromthecrypt codefromthecrypt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great. Someone can try to weave this into tinymem, trivy and go-plugin if needs it (not sure any other major projects that use malloc/free manually with tinymem)

@evacchi
Copy link
Contributor

evacchi commented May 5, 2023

pushing the updated wasm binary. @lburgazzoli since you don't mind we are merging this PR because the discussion is more linear. Thanks for your help 🫡

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants