wit-bindgen icon indicating copy to clipboard operation
wit-bindgen copied to clipboard

Go: ergonomics improvement for Variants type

Open Mossaka opened this issue 2 years ago • 7 comments

This issue is created for a discussion on improving the variants type in the Go bindgen.

To see how the variants type is generated, please read the head of this https://github.com/bytecodealliance/wit-bindgen/issues/612#issue-1801773199.

A summary of what's being proposed: https://github.com/bytecodealliance/wit-bindgen/issues/612#issuecomment-1644691976

Mossaka avatar Jul 20 '23 22:07 Mossaka

I think a sealed interface as suggested by @lann in https://github.com/bytecodealliance/wit-bindgen/issues/612#issuecomment-1633404434 is my preferred solution to variants. If possible, I'd suggest avoiding setters in favor of concrete assignment (this is similar to how the Go protobuf generation has getters but no setters on oneof types).

johanbrandhorst avatar Jul 24 '23 21:07 johanbrandhorst

https://github.com/bytecodealliance/wit-bindgen/issues/612#issuecomment-1639655039 by @qmuntal points out a potential performance optimization allowed by struct wrappers. It would be worthwhile to (micro-)benchmark the two approaches to get an idea of the potential performance deltas.

lann avatar Jul 24 '23 22:07 lann

For reference, the new log/slog package code using the struct wrapper kind approach is here: https://cs.opensource.google/go/go/+/refs/tags/go1.21rc3:src/log/slog/value.go;l=21.

johanbrandhorst avatar Jul 24 '23 22:07 johanbrandhorst

It would be worthwhile to (micro-)benchmark the two approaches to get an idea of the potential performance deltas.

Disclaimer: please do your own benchmarks, I'm not a users of this project and I can be biased towards a niche use-case.

I did run some benchmarks:

  • Code: https://play.golang.com/p/uD0J_f5bLJZ
  • Benchmark code C1: https://play.golang.com/p/TE0SZepwuIR
  • Benchmark code C2: https://play.golang.com/p/gpKHjQqXVDa

Results: (c1 == sealed struct, c2 == sealed interface)

goos: windows
goarch: amd64
pkg: gotest
cpu: Intel(R) Core(TM) i7-10850H CPU @ 2.70GHz
                │    c2.txt      │                c1.txt                 │
                │     sec/op     │    sec/op     vs base                 │
AppendInt32-12     213.50n ±  2%   20.64n ±  4%   -90.33% (p=0.000 n=10)
AppendInt64-12     261.00n ±  1%   19.14n ±  1%   -92.66% (p=0.000 n=10)
AppendString-12    606.15n ± 13%   42.88n ±  8%   -92.93% (p=0.000 n=10)
AppendMix-12      1163.50n ±  7%   65.78n ± 13%   -94.35% (p=0.000 n=10)
Access-12           1.292n ± 11%   2.623n ±  9%  +102.98% (p=0.000 n=10)

                │   c2.txt     │                 c1.txt                  │
                │     B/op     │    B/op     vs base                     │
AppendInt32-12    80.00 ± 0%      0.00 ± 0%  -100.00% (p=0.000 n=10)
AppendInt64-12    160.0 ± 0%       0.0 ± 0%  -100.00% (p=0.000 n=10)
AppendString-12   320.0 ± 0%       0.0 ± 0%  -100.00% (p=0.000 n=10)
AppendMix-12      640.0 ± 0%       0.0 ± 0%  -100.00% (p=0.000 n=10)
Access-12         0.000 ± 0%     0.000 ± 0%         ~ (p=1.000 n=10)


                │   c2.txt     │                 c1.txt                  │
                │  allocs/op   │ allocs/op   vs base                     │
AppendInt32-12    20.00 ± 0%      0.00 ± 0%  -100.00% (p=0.000 n=10)
AppendInt64-12    20.00 ± 0%      0.00 ± 0%  -100.00% (p=0.000 n=10)
AppendString-12   20.00 ± 0%      0.00 ± 0%  -100.00% (p=0.000 n=10)
AppendMix-12      60.00 ± 0%      0.00 ± 0%  -100.00% (p=0.000 n=10)
Access-12         0.000 ± 0%     0.000 ± 0%         ~ (p=1.000 n=10) 

Highlights:

  • Sealed struct wins:
    • No allocations
    • 10 times faster to append to an slice
  • Sealed interface wins:
    • 2 times faster to access a value via type-switch

Although the sealed interface approach is faster in terms of type-switching, the absolute difference is so small (1n vs 2n) that I don't expect it to compensate the increased GC pressure.

qmuntal avatar Jul 25 '23 07:07 qmuntal

In fairness, we probably should be doing these benchmarks on WebAssembly, not amd64 😅. I also want to caution against being guided too much by benchmarks in designing an ergonomic interface. I am still a fan of the sealed interface, perhaps we could implement both and then perform benchmarks? Ideally with real world examples, too.

johanbrandhorst avatar Jul 25 '23 21:07 johanbrandhorst

Okay so to summarize the discussion so far. We will want to implement both the sealed interface and sealed struct + integer optaimization methods and comapre their real world benchmarks on Wasm.

Mossaka avatar Aug 02 '23 02:08 Mossaka

Turns out that using a sealed struct works fine, and is ABI compatible (at least on TinyGo, and eventually on GOARCH=wasm32) with the Canonical ABI.

Here is an example of various variant and result types in-progress targeting WASI Preview 2: https://github.com/ydnar/wasm-tools-go/tree/main/wasi

I modeled the API after the Go enum pattern, with constructors and getters, but not setters (per @johanbrandhorst’s observation). Variant values are pass-by-value (for ABI compatibility), but can implement a common interface.

Ignoring that the type (new-timestamp) starts with New when translated to Go…

// NewTimestamp represents the variant "wasi:filesystem/types.new-timestamp".
//
// When setting a timestamp, this gives the value to set it to.
type NewTimestamp struct {
        v cm.Variant[uint8, DateTime, struct{}]
}


// NewTimestampNoChange returns a NewTimestamp with variant case "no-change".
func NewTimestampNoChange() NewTimestamp {
        var result NewTimestamp
        cm.Set(&result.v, 0, struct{}{})
        return result
}


// NoChange represents variant case "no-change".
//
// Leave the timestamp set to its previous value.
func (self *NewTimestamp) NoChange() bool {
        return self.v.Is(0)
}


// NewTimestampNow returns a NewTimestamp with variant case "now".
func NewTimestampNow() NewTimestamp {
        var result NewTimestamp
        cm.Set(&result.v, 1, struct{}{})
        return result
}


// Now represents variant case "now".
//
// Leave the timestamp set to its previous value.
func (self *NewTimestamp) Now() bool {
        return self.v.Is(1)
}

// NewTimestampTimestamp returns a NewTimestamp with variant case "timestamp(datetime)".
func NewTimestampTimestamp(v DateTime) NewTimestamp {
        var result NewTimestamp
        cm.Set(&result.v, 2, v)
        return result
}

// Timestamp represents variant case "timestamp(datetime)".
//
// Set the timestamp to the given value.
func (self *NewTimestamp) Timestamp() (DateTime, bool) {
        return cm.Get[DateTime](&self.v, 2)
}

ydnar avatar Jan 25 '24 01:01 ydnar