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 [email protected] 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 [email protected]
> 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 [email protected]
>>>> 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 [email protected]
>>>>> 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 [email protected].
To view this discussion on the web visit
https://groups.google.com/d/msgid/golang-nuts/ebcdbdbb-d1f0-47b7-a9f5-696d9887c6d6n%40googlegroups.com.