tarantool / go-tarantool

Tarantool 1.10+ client for Go language
https://pkg.go.dev/github.com/tarantool/go-tarantool/v2
BSD 2-Clause "Simplified" License
180 stars 58 forks source link

v3: minimize allocations #283

Open oleg-jukovec opened 1 year ago

oleg-jukovec commented 1 year ago

Now we do a lot of allocations per a request:

$ go test -bench BenchmarkClientSerialTyped -benchmem
BenchmarkClientSerialTyped-12          12250         94545 ns/op         898 B/op         16 allocs/op

There is no need for 0, but with 80-20 rule we could have a soft goal 8 allocs per a request.

DerekBum commented 1 year ago

Right now output is different (with 15 allocs):

$ go test -bench BenchmarkClientSerialTyped -benchmem
BenchmarkClientSerialTyped-8       13958         77821 ns/op         920 B/op         15 allocs/op

Here are all allocations: 1) In https://github.com/tarantool/go-tarantool/blob/d8df65dcd0f29a6a5d07472115cbcf2753b12609/future.go#L120-L127

./future.go:122:8: &Future{} escapes to heap
./future.go:125:19: make([]*Response, 0) escapes to heap

We create pointers to the Future and an array of pointers. They are stored in heap.

2) In https://github.com/tarantool/go-tarantool/blob/d8df65dcd0f29a6a5d07472115cbcf2753b12609/request.go#L862-L874

./request.go:864:12: new(SelectRequest) escapes to heap
./request.go:870:25: []interface {}{} escapes to heap

We allocate memory for the SelectRequest. We actually can just not initialize key field and reduce total allocations by 1.

3) In https://github.com/tarantool/go-tarantool/blob/d8df65dcd0f29a6a5d07472115cbcf2753b12609/connection.go#L1244-L1267

./connection.go:1263:17: make([]byte, length) escapes to heap

4) In https://github.com/tarantool/go-tarantool/blob/d8df65dcd0f29a6a5d07472115cbcf2753b12609/connection.go#L878-L932

./connection.go:894:11: &Response{...} escapes to heap

Creating a pointer to a Response. Later, this pointer might be used in the Future-s AppendPush and SetResponse methods. They accept a pointer and change the value of the corresponding structure.

5) In https://github.com/tarantool/go-tarantool/blob/d8df65dcd0f29a6a5d07472115cbcf2753b12609/response.go#L278-L337

./response.go:288:19: func literal escapes to heap

Some other allocations happens inside of msgpack package:

6) In https://github.com/vmihailenco/msgpack/blob/0ac2a568aee92ef815c8d3e06b3fb3e2f79c18f6/decode.go#L84-L88

./decode.go:85:10: new(Decoder) escapes to heap

7) In https://github.com/vmihailenco/msgpack/blob/0ac2a568aee92ef815c8d3e06b3fb3e2f79c18f6/decode.go#L617-L624

./decode.go:624:12: make([]byte, 0, 64) escapes to heap

8) In https://github.com/vmihailenco/msgpack/blob/0ac2a568aee92ef815c8d3e06b3fb3e2f79c18f6/decode_string.go#L54-L60

./decode_string.go:59:16: string(b) escapes to heap

9) In https://github.com/vmihailenco/msgpack/blob/0ac2a568aee92ef815c8d3e06b3fb3e2f79c18f6/decode_map.go#L72-L88

./decode_map.go:87:31: unexpectedCodeError{...} escapes to heap

Rest of the allocations (4) I could not find, perhaps they are somewhere deep inside of the msgpack calls. Or there might be some allocations tied to use of interface{} as function arguments (Values stored in interfaces might be arbitrarily large, but only one word is dedicated to holding the value in the interface structure, so the assignment allocates a chunk of memory on the heap and records the pointer in the one-word slot. from https://research.swtch.com/interfaces). And we use interfaces a lot in the functions. But this is not detected by the go build -gcflags='-m=3' output.

oleg-jukovec commented 1 year ago

You could also try to run test with memory profiler. See -memprofile option to ensure that rest of allocation belongs to msgpack:

https://habr.com/en/companies/badoo/articles/301990/

DerekBum commented 1 year ago

Updated list of all 15 allocations:

1) 2 allocations for creating a channel here https://github.com/tarantool/go-tarantool/blob/d8df65dcd0f29a6a5d07472115cbcf2753b12609/connection.go#L879

2) 1 allocation while passing an argument to a goroutine function https://github.com/tarantool/go-tarantool/blob/d8df65dcd0f29a6a5d07472115cbcf2753b12609/connection.go#L882

3) 1 allocation when creating a pointer to a Response struct https://github.com/tarantool/go-tarantool/blob/d8df65dcd0f29a6a5d07472115cbcf2753b12609/connection.go#L894

4) 2 allocations while calling NewFuture https://github.com/tarantool/go-tarantool/blob/d8df65dcd0f29a6a5d07472115cbcf2753b12609/connection.go#L1284 https://github.com/tarantool/go-tarantool/blob/d8df65dcd0f29a6a5d07472115cbcf2753b12609/future.go#L120-L127

./future.go:122:8: &Future{} escapes to heap
./future.go:125:19: make([]*Response, 0) escapes to heap

5) 2 allocations while calling NewSelectRequest https://github.com/tarantool/go-tarantool/blob/d8df65dcd0f29a6a5d07472115cbcf2753b12609/request.go#L862-L874

./request.go:864:12: new(SelectRequest) escapes to heap
./request.go:870:25: []interface {}{} escapes to heap

6) 1 allocation in In https://github.com/tarantool/go-tarantool/blob/d8df65dcd0f29a6a5d07472115cbcf2753b12609/connection.go#L1244-L1267

./connection.go:1263:17: make([]byte, length) escapes to heap

7) 2 allocations: 1 for creating a []byte array and 1 for creating a pointer to Response structure: https://github.com/tarantool/go-tarantool/blob/d8df65dcd0f29a6a5d07472115cbcf2753b12609/connection.go#L1102 https://github.com/tarantool/go-tarantool/blob/d8df65dcd0f29a6a5d07472115cbcf2753b12609/connection.go#L1139

./connection.go:1102:21: make([]byte, 0, 128) escapes to heap
./connection.go:1139:12: &Response{...} escapes to heap:

Some other allocations happens inside of msgpack package:

8) 1 allocation in https://github.com/vmihailenco/msgpack/blob/0ac2a568aee92ef815c8d3e06b3fb3e2f79c18f6/decode.go#L84-L88

./decode.go:85:10: new(Decoder) escapes to heap

9) 1 allocation in https://github.com/vmihailenco/msgpack/blob/0ac2a568aee92ef815c8d3e06b3fb3e2f79c18f6/decode.go#L617-L624

./decode.go:624:12: make([]byte, 0, 64) escapes to heap

10) 1 allocation in https://github.com/vmihailenco/msgpack/blob/0ac2a568aee92ef815c8d3e06b3fb3e2f79c18f6/decode_string.go#L54-L60

./decode_string.go:59:16: string(b) escapes to heap

11) 1 allocation in https://github.com/vmihailenco/msgpack/blob/0ac2a568aee92ef815c8d3e06b3fb3e2f79c18f6/decode_map.go#L72-L88

./decode_map.go:87:31: unexpectedCodeError{...} escapes to heap

15 allocations total.

oleg-jukovec commented 1 year ago

Updated list of all 15 allocations:

1. 2 allocations for creating a channel here https://github.com/tarantool/go-tarantool/blob/d8df65dcd0f29a6a5d07472115cbcf2753b12609/connection.go#L879

2. 1 allocation while passing an argument to a goroutine function https://github.com/tarantool/go-tarantool/blob/d8df65dcd0f29a6a5d07472115cbcf2753b12609/connection.go#L882

It does not look like a per request allocations. But let stop here. Thank you!

KaymeKaydex commented 3 weeks ago

I also want to note that msgpack supports zero resource allocation with sync.Pool, I think you can try using this implementation for decoding

code from msgpack

var decPool = sync.Pool{
    New: func() interface{} {
        return NewDecoder(nil)
    },
}

func GetDecoder() *Decoder {
    return decPool.Get().(*Decoder)
}

func PutDecoder(dec *Decoder) {
    dec.r = nil
    dec.s = nil
    decPool.Put(dec)
}