Luckily, I have the "no scalar" version with a build tag. Here is a simple benchmark:
func BenchmarkValue(b *testing.B) { for n := 0; n < b.N; n++ { sv := IntValue(0) for i := 0; i < 1000; i++ { iv := IntValue(int64(i)) sv, _ = add(nil, sv, iv) // add is the "real" lua runtime function that adds two numeric values. } } } Results with the "scalar" version $ go test -benchmem -run=^$ -bench '^(BenchmarkValue)$' ./runtime goos: darwin goarch: amd64 pkg: github.com/arnodel/golua/runtime cpu: Intel(R) Core(TM) i7-7820HQ CPU @ 2.90GHz BenchmarkValue-8 122995 9494 ns/op 0 B/op 0 allocs/op PASS ok github.com/arnodel/golua/runtime 1.415s Results without the "scalar" version (noscalar build tag) $ go test -benchmem -run=^$ -tags noscalar -bench '^(BenchmarkValue)$' ./runtime goos: darwin goarch: amd64 pkg: github.com/arnodel/golua/runtime cpu: Intel(R) Core(TM) i7-7820HQ CPU @ 2.90GHz BenchmarkValue-8 37407 32357 ns/op 13768 B/op 1721 allocs/op PASS ok github.com/arnodel/golua/runtime 1.629s That looks like a pretty big improvement :) The improvement is also significant in real workloads but not.so dramatic (given they don't spend all their time manipulating scalar values!) On Monday, 21 December 2020 at 21:02:26 UTC ben...@gmail.com wrote: > Nice! Do you have any benchmarks on how much faster the "scalar" version > is than the non-scalar? > > On Tuesday, December 22, 2020 at 12:58:19 AM UTC+13 arn...@gmail.com > wrote: > >> Just an update (in case anyone is interested!). I went for the approach >> described below of having a Value type holding a scalar for quick access to >> values that fit in 64 bits (ints, floats, bools) and an interface fo for >> the rest. >> >> type Value struct { >> scalar uint64 >> iface interface{} >> } >> >> That significantly decreased memory management pressure on the program >> for many workloads, without having to manage a pool of say integer values. >> It also had the consequence of speeding up many arithmetic operations. >> Thanks all for your explanations and suggestions! >> >> -- >> Arnaud >> >> On Wednesday, 16 December 2020 at 11:15:32 UTC Arnaud Delobelle wrote: >> >>> Ah interesting, I guess that could mean I would need to switch to using >>> reflect.Value as the "value" type in the Lua runtime. I am unclear about >>> the performance consequences, but I guess I could try to measure that. >>> >>> Also, looking at the implementation of reflect, its seems like the Value >>> type I suggested in my reply to Ben [1] is a "special purpose" version of >>> reflect.Value - if you squint at it from the right angle! >>> >>> -- >>> Arnaud >>> >>> [1] >>> type Value struct { >>> scalar uint64 >>> iface interface{} >>> } >>> On Wednesday, 16 December 2020 at 00:56:52 UTC Keith Randall wrote: >>> >>>> Unfortunately for you, interfaces are immutable. We can't provide a >>>> means to create an interface from a pointer, because then the user can >>>> modify the interface using the pointer they constructed it with (as you >>>> were planning to do). >>>> >>>> You could use a modifiable reflect.Value for this. >>>> >>>> var i int64 = 77 >>>> v := reflect.ValueOf(&i).Elem() >>>> >>>> At this point, v now has .Type() of int64, and is settable. >>>> >>>> Note that to get the value you can't do v.Interface().(int64), as that >>>> allocates. You need to use v.Int(). >>>> Of course, reflection has its own performance gotchas. It will solve >>>> this problem but may surface others. >>>> On Tuesday, December 15, 2020 at 12:04:54 PM UTC-8 ben...@gmail.com >>>> wrote: >>>> >>>>> Nice project! >>>>> >>>>> It's a pity Go doesn't have C-like unions for cases like this (though >>>>> I understand why). In my implementation of AWK in Go, I modelled the >>>>> value >>>>> type as a pseudo-union struct, passed by value: >>>>> >>>>> type value struct { >>>>> typ valueType // Type of value (Null, Str, Num, NumStr) >>>>> s string // String value (for typeStr) >>>>> n float64 // Numeric value (for typeNum and typeNumStr) >>>>> } >>>>> >>>>> Code here: >>>>> https://github.com/benhoyt/goawk/blob/22bd82c92461cedfd02aa7b8fe1fbebd697d59b5/interp/value.go#L22-L27 >>>>> >>>>> Initially I actually used "type Value interface{}" as well, but I >>>>> switched to the above primarily to model the funky AWK "numeric string" >>>>> concept. However, I seem to recall that it had a significant performance >>>>> benefit too, as passing everything by value avoided a number of >>>>> allocations. >>>>> >>>>> Lua has more types to deal with, but you could try something similar. >>>>> Or maybe include int64 (for bool as well) and string fields, and >>>>> everything >>>>> else falls back to interface{}? It'd be a fairly large struct, so not >>>>> sure >>>>> it would help ... you'd have to benchmark it. But I'm thinking something >>>>> like this: >>>>> >>>>> type Value struct { >>>>> typ valueType >>>>> i int64 // for typ = bool, integer >>>>> s string // for typ = string >>>>> v interface{} // for typ = float, other >>>>> } >>>>> >>>>> -Ben >>>>> >>>>> On Wednesday, December 16, 2020 at 6:50:05 AM UTC+13 arn...@gmail.com >>>>> wrote: >>>>> >>>>>> Hi >>>>>> >>>>>> The context for this question is that I am working on a pure Go >>>>>> implementation of Lua [1] (as a personal project). Now that it is more >>>>>> or >>>>>> less functionally complete, I am using pprof to see what the main CPU >>>>>> bottlenecks are, and it turns out that they are around memory >>>>>> management. >>>>>> The first one was to do with allocating and collecting Lua "stack frame" >>>>>> data, which I improved by having add-hoc pools for such objects. >>>>>> >>>>>> The second one is the one that is giving me some trouble. Lua is a >>>>>> so-called "dynamically typed" language, i.e. values are typed but >>>>>> variables >>>>>> are not. So for easy interoperability with Go I implemented Lua values >>>>>> with the type >>>>>> >>>>>> // Go code >>>>>> type Value interface{} >>>>>> >>>>>> The scalar Lua types are simply implemented as int64, float64, bool, >>>>>> string with their type "erased" by putting them in a Value interface. >>>>>> The >>>>>> problem is that the Lua runtime creates a great number of short lived >>>>>> Value >>>>>> instances. E.g. >>>>>> >>>>>> -- Lua code >>>>>> for i = 0, 1000000000 do >>>>>> n = n + i >>>>>> end >>>>>> >>>>>> When executing this code, the Lua runtime will put the values 0 to 1 >>>>>> billion into the register associated with the variable "i" (say, r_i). >>>>>> But >>>>>> because r_i contains a Value, each integer is converted to an interface >>>>>> which triggers a memory allocation. The critical functions in the Go >>>>>> runtime seem to be convT64 and mallocgc. >>>>>> >>>>>> I am not sure how to deal with this issue. I cannot easily create a >>>>>> pool of available values because Go presents say Value(int64(1000)) as >>>>>> an >>>>>> immutable object to me, so I cannot keep it around for later use to hold >>>>>> the integer 1001. To be more explicit >>>>>> >>>>>> // Go code >>>>>> i := int64(1000) >>>>>> v := Value(i) // This triggers an allocation (because the >>>>>> interface needs a pointer) >>>>>> // Here the Lua runtime can work with v (containing 1000) >>>>>> j := i + 1 >>>>>> // Even though v contains a pointer to a heap location, I cannot >>>>>> modify it >>>>>> v := Value(j) // This triggers another allocation >>>>>> // Here the Lua runtime can work with v (containing 1001) >>>>>> >>>>>> >>>>>> I could perhaps use a pointer to an integer to make a Value out of. >>>>>> This would allow reuse of the heap location. >>>>>> >>>>>> // Go code >>>>>> p :=new(int64) // Explicit allocation >>>>>> vp := Value(p) >>>>>> i :=int64(1000) >>>>>> *p = i // No allocation >>>>>> // Here the Lua runtime can work with vp (contaning 1000) >>>>>> j := i + 1 >>>>>> *p = j // No allocation >>>>>> // Here the Lua runtime can work with vp (containing 1001) >>>>>> >>>>>> But the issue with this is that Go interoperability is not so good, >>>>>> as Go int64 now map to (interfaces holding) *int64 in the Lua runtime. >>>>>> >>>>>> However, as I understand it, in reality interfaces holding an int64 >>>>>> and an *int64 both contain the same thing (with a different type >>>>>> annotation): a pointer to an int64. >>>>>> >>>>>> Imagine that if somehow I had a function that can turn an *int64 to a >>>>>> Value holding an int64 (and vice-versa): >>>>>> >>>>>> func Int64PointerToInt64Iface(p *int16) interface{} { >>>>>> // returns an interface that has concrete type int64, and >>>>>> points at p >>>>>> } >>>>>> >>>>>> func int64IfaceToInt64Pointer(v interface{}) *int64 { >>>>>> // returns the pointer that v holds >>>>>> } >>>>>> >>>>>> then I would be able to "pool" the allocations as follows: >>>>>> >>>>>> func NewIntValue(n int64) Value { >>>>>> v = getFromPool() >>>>>> if p == nil { >>>>>> return Value(n) >>>>>> } >>>>>> *p = n >>>>>> return Int64PointerToint64Iface(p) >>>>>> } >>>>>> >>>>>> func ReleaseIntValue(v Value) { >>>>>> addToPool(Int64IPointerFromInt64Iface(v)) >>>>>> } >>>>>> >>>>>> func getFromPool() *int64 { >>>>>> // returns nil if there is no available pointer in the pool >>>>>> } >>>>>> >>>>>> func addToPool(p *int64) { >>>>>> // May add p to the pool if there is spare capacity. >>>>>> } >>>>>> >>>>>> I am sure that this must leak an abstraction and that there are good >>>>>> reasons why this may be dangerous or impossible, but I don't know what >>>>>> the >>>>>> specific issues are. Could someone enlighten me? >>>>>> >>>>>> Or even better, would there be a different way of modelling Lua >>>>>> values that would allow good Go interoperability and allow controlling >>>>>> heap >>>>>> allocations? >>>>>> >>>>>> If you got to this point, thank you for reading! >>>>>> >>>>>> Arnaud Delobelle >>>>>> >>>>>> [1] https://github.com/arnodel/golua >>>>>> >>>>> -- You received this message because you are subscribed to the Google Groups "golang-nuts" group. To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts+unsubscr...@googlegroups.com. To view this discussion on the web visit https://groups.google.com/d/msgid/golang-nuts/ebcdbdbb-d1f0-47b7-a9f5-696d9887c6d6n%40googlegroups.com.