Skip to content
This repository has been archived by the owner on Apr 4, 2024. It is now read-only.

x/evm/keeper: invoke tx.To() once and reuse that value instead of incurring a 24B allocation for a [20]byte #826

Closed
odeke-em opened this issue Dec 12, 2021 · 0 comments · Fixed by #828

Comments

@odeke-em
Copy link
Contributor

Noticed while auditing the EVM module that we've got this code pattern https://github.com/tharsis/ethermint/blob/54fa882ab8e56e3de65a1a730b02e73884c26bed/x/evm/keeper/msg_server.go#L47-L48

and it looks innocent until we actually examine what it does, which is https://github.com/ethereum/go-ethereum/blob/7a0c19f813e285516f4b525305fd73b625d2dec8/core/types/transaction.go#L286-L288

of which the code for copyAddressPtr is https://github.com/ethereum/go-ethereum/blob/7a0c19f813e285516f4b525305fd73b625d2dec8/core/types/transaction.go#L631-L637

which for an array which uses a reflect.incurs each time this expense:

  • 20 bytes copied
  • Round up to of len(array's slice) rounded up to character size class and pointer aligned so 24 bytes on 64-bit machines

Remedy

We need to adopt the elegant Go idiom of in-condition variables with the fix being

-       if tx.To() != nil {
-               attrs = append(attrs, sdk.NewAttribute(types.AttributeKeyRecipie
nt, tx.To().Hex()))
+       if to := tx.To(); to != nil {
+               attrs = append(attrs, sdk.NewAttribute(types.AttributeKeyRecipie
nt, to.Hex()))
        }

Exhibits

Here is a demo of how we can see how much is incurred in allocations https://go.dev/play/p/9d_F2SyixjK with results of the benchmarks before and after the change

$ benchstat before.txt after.txt 
name        old time/op    new time/op    delta
CopyAddr-8    38.4ns ± 3%    19.3ns ± 3%  -49.66%  (p=0.000 n=10+10)

name        old alloc/op   new alloc/op   delta
CopyAddr-8     48.0B ± 0%     24.0B ± 0%  -50.00%  (p=0.000 n=10+10)

name        old allocs/op  new allocs/op  delta
CopyAddr-8      2.00 ± 0%      1.00 ± 0%  -50.00%  (p=0.000 n=10+10)

then an exhibit of the benchmarks,

type Addr [20]byte

var exampleAddr = &Addr{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o'}

func copyAddr(a *Addr) *Addr {
        if a == nil {
                return nil
        }
        c := *a
        return &c
}

func after() *Addr {
        return copyAddr(exampleAddr)
}

func before() *Addr {
        a := copyAddr(exampleAddr)
        if a == nil {
                return a
        }
        return copyAddr(exampleAddr)
}

var sink interface{}

func BenchmarkCopyAddrBefore(b *testing.B) {
        for i := 0; i < b.N; i++ {
                sink = before()
        }
        if sink == nil {
                b.Fatal("Benchmark did not run")
        }
        sink = (interface{})(nil)
}

func BenchmarkCopyAddrAfter(b *testing.B) {
        for i := 0; i < b.N; i++ {
                sink = after()
        }
        if sink == nil {
                b.Fatal("Benchmark did not run")
        }
        sink = (interface{})(nil)
}
odeke-em added a commit that referenced this issue Dec 13, 2021
The prior code doubly invoked (*ethereum/go-ethereum/core/types.Transaction).To()
which is quite expensive, and firstly copies 20 bytes each time, then
that gets rounded up to the proper size class/pointer alignment so on
64-bit machines 20B -> 24B.

Isolating a benchmark for this code per issue #826 shows this saves
quite a bit of bytes and some nano seconds which all count up towards
the transactions per seconds being processed:

```shell
$ benchstat before.txt after.txt
name        old time/op    new time/op    delta
CopyAddr-8    38.4ns ± 3%    19.3ns ± 3%  -49.66%  (p=0.000 n=10+10)

name        old alloc/op   new alloc/op   delta
CopyAddr-8     48.0B ± 0%     24.0B ± 0%  -50.00%  (p=0.000 n=10+10)

name        old allocs/op  new allocs/op  delta
CopyAddr-8      2.00 ± 0%      1.00 ± 0%  -50.00%  (p=0.000 n=10+10)
```

Fixes #826
odeke-em added a commit that referenced this issue Dec 13, 2021
The prior code doubly invoked (*ethereum/go-ethereum/core/types.Transaction).To()
which is quite expensive, and firstly copies 20 bytes each time, then
that gets rounded up to the proper size class/pointer alignment so on
64-bit machines 20B -> 24B.

Isolating a benchmark for this code per issue #826 shows this saves
quite a bit of bytes and some nano seconds which all count up towards
the transactions per seconds being processed:

```shell
$ benchstat before.txt after.txt
name        old time/op    new time/op    delta
CopyAddr-8    38.4ns ± 3%    19.3ns ± 3%  -49.66%  (p=0.000 n=10+10)

name        old alloc/op   new alloc/op   delta
CopyAddr-8     48.0B ± 0%     24.0B ± 0%  -50.00%  (p=0.000 n=10+10)

name        old allocs/op  new allocs/op  delta
CopyAddr-8      2.00 ± 0%      1.00 ± 0%  -50.00%  (p=0.000 n=10+10)
```

Fixes #826
fedekunze added a commit that referenced this issue Dec 13, 2021
* x/evm/keeper: save 24B with Go in-condition variable idiom

The prior code doubly invoked (*ethereum/go-ethereum/core/types.Transaction).To()
which is quite expensive, and firstly copies 20 bytes each time, then
that gets rounded up to the proper size class/pointer alignment so on
64-bit machines 20B -> 24B.

Isolating a benchmark for this code per issue #826 shows this saves
quite a bit of bytes and some nano seconds which all count up towards
the transactions per seconds being processed:

```shell
$ benchstat before.txt after.txt
name        old time/op    new time/op    delta
CopyAddr-8    38.4ns ± 3%    19.3ns ± 3%  -49.66%  (p=0.000 n=10+10)

name        old alloc/op   new alloc/op   delta
CopyAddr-8     48.0B ± 0%     24.0B ± 0%  -50.00%  (p=0.000 n=10+10)

name        old allocs/op  new allocs/op  delta
CopyAddr-8      2.00 ± 0%      1.00 ± 0%  -50.00%  (p=0.000 n=10+10)
```

Fixes #826

* changelog

* lint

* Revert stray changes that were used in testing

* rm log

Co-authored-by: Federico Kunze Küllmer <federico.kunze94@gmail.com>
odeke-em added a commit that referenced this issue Dec 14, 2021
Following suit with PR #828, this change cuts down the expenses
from using .To doubly; yet using the Go in-condition variable idiom.

Updates #826
odeke-em added a commit that referenced this issue Dec 14, 2021
Following suit with PR #828, this change cuts down the expenses
from using .To doubly; yet using the Go in-condition variable idiom.

Updates #826
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

1 participant