Skip to content

Commit 114eaf5

Browse files
marioevzfselmo
andauthored
feat(benchmarking): Optimize IteratingBytecode (ethereum#2184)
* feat(testing/vm): Cache bytecode's keccak * feat(testing/vm): Cache bytecode gas cost/refund * feat(tests/benchmark): Cache created contract address * feat(tests/benchmarking): Cache access list * refactor: refactor caching with module-level ``@cache`` --------- Co-authored-by: fselmo <fselmo2@gmail.com>
1 parent 658e388 commit 114eaf5

4 files changed

Lines changed: 72 additions & 29 deletions

File tree

packages/testing/src/execution_testing/test_types/helpers.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
FixedSizeBytesConvertible,
1212
)
1313
from execution_testing.forks import Fork
14-
from execution_testing.vm import Op
14+
from execution_testing.vm import Bytecode, Op
1515

1616
from .account_types import EOA
1717
from .utils import int_to_bytes
@@ -88,14 +88,18 @@ def compute_create_address(
8888
def compute_create2_address(
8989
address: FixedSizeBytesConvertible,
9090
salt: FixedSizeBytesConvertible,
91-
initcode: BytesConvertible,
91+
initcode: Bytecode | BytesConvertible,
9292
) -> Address:
9393
"""
9494
Compute address of the resulting contract created using the `CREATE2`
9595
opcode.
9696
"""
97+
if isinstance(initcode, Bytecode):
98+
initcode_hash = initcode.keccak256()
99+
else:
100+
initcode_hash = Bytes(initcode).keccak256()
97101
hash_bytes = Bytes(
98-
b"\xff" + Address(address) + Hash(salt) + Bytes(initcode).keccak256()
102+
b"\xff" + Address(address) + Hash(salt) + initcode_hash
99103
).keccak256()
100104
return Address(hash_bytes[-20:])
101105

packages/testing/src/execution_testing/vm/bytecode.py

Lines changed: 41 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Ethereum Virtual Machine bytecode primitives and utilities."""
22

3+
from functools import cache
34
from typing import Any, List, Self, SupportsBytes, Type
45

56
from pydantic import GetCoreSchemaHandler
@@ -33,6 +34,7 @@ class Bytecode:
3334

3435
_name_: str = ""
3536
_bytes_: bytes
37+
_keccak_256_: Hash | None = None
3638

3739
popped_stack_items: int
3840
pushed_stack_items: int
@@ -261,7 +263,9 @@ def hex(self) -> str:
261263

262264
def keccak256(self) -> Hash:
263265
"""Return the keccak256 hash of the opcode byte representation."""
264-
return Bytes(self._bytes_).keccak256()
266+
if self._keccak_256_ is None:
267+
self._keccak_256_ = Bytes(self._bytes_).keccak256()
268+
return self._keccak_256_
265269

266270
def gas_cost(
267271
self,
@@ -271,13 +275,7 @@ def gas_cost(
271275
timestamp: int = 0,
272276
) -> int:
273277
"""Use a fork object to calculate the gas used by this bytecode."""
274-
opcode_gas_calculator = fork.opcode_gas_calculator(
275-
block_number=block_number, timestamp=timestamp
276-
)
277-
total_gas = 0
278-
for opcode in self.opcode_list:
279-
total_gas += opcode_gas_calculator(opcode)
280-
return total_gas
278+
return _bytecode_gas_cost(self, fork, block_number, timestamp)
281279

282280
def refund(
283281
self,
@@ -287,13 +285,7 @@ def refund(
287285
timestamp: int = 0,
288286
) -> int:
289287
"""Use a fork object to calculate the gas refund from this bytecode."""
290-
opcode_refund_calculator = fork.opcode_refund_calculator(
291-
block_number=block_number, timestamp=timestamp
292-
)
293-
total_refund = 0
294-
for opcode in self.opcode_list:
295-
total_refund += opcode_refund_calculator(opcode)
296-
return total_refund
288+
return _bytecode_refund(self, fork, block_number, timestamp)
297289

298290
@classmethod
299291
def __get_pydantic_core_schema__(
@@ -310,3 +302,37 @@ def __get_pydantic_core_schema__(
310302
info_arg=False,
311303
),
312304
)
305+
306+
307+
@cache
308+
def _bytecode_gas_cost(
309+
bytecode: Bytecode,
310+
fork: Type[ForkOpcodeInterface],
311+
block_number: int,
312+
timestamp: int,
313+
) -> int:
314+
"""Return cached gas cost for the given bytecode and fork parameters."""
315+
opcode_gas_calculator = fork.opcode_gas_calculator(
316+
block_number=block_number, timestamp=timestamp
317+
)
318+
total_gas = 0
319+
for opcode in bytecode.opcode_list:
320+
total_gas += opcode_gas_calculator(opcode)
321+
return total_gas
322+
323+
324+
@cache
325+
def _bytecode_refund(
326+
bytecode: Bytecode,
327+
fork: Type[ForkOpcodeInterface],
328+
block_number: int,
329+
timestamp: int,
330+
) -> int:
331+
"""Return cached gas refund for the given bytecode and fork parameters."""
332+
opcode_refund_calculator = fork.opcode_refund_calculator(
333+
block_number=block_number, timestamp=timestamp
334+
)
335+
total_refund = 0
336+
for opcode in bytecode.opcode_list:
337+
total_refund += opcode_refund_calculator(opcode)
338+
return total_refund

tests/benchmark/compute/helpers.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import math
44
from enum import Enum, auto
5-
from typing import Generator, Self, Sequence, cast
5+
from typing import Dict, Generator, Self, Sequence, cast
66

77
from execution_testing import (
88
EOA,
@@ -349,6 +349,8 @@ class CustomSizedContractFactory(IteratingBytecode):
349349

350350
_cached_address: Address
351351
"""Cached address to avoid expensive recomputation."""
352+
_cached_created_contracts: Dict[int, Address]
353+
"""Cached created contract addresses to avoid expensive recomputation."""
352354
contract_size: int
353355
"""The size of the contracts to deploy."""
354356

@@ -418,6 +420,7 @@ def __new__(
418420
initcode=Initcode(deploy_code=instance),
419421
fork=fork,
420422
)
423+
instance._cached_created_contracts = {}
421424
instance.contract_size = initcode.contract_size
422425
deployed_address = pre.deterministic_deploy_contract(
423426
deploy_code=instance
@@ -494,8 +497,10 @@ def address(self) -> Address:
494497

495498
def created_contract_address(self, *, salt: int) -> Address:
496499
"""Get the deterministic address of the created contract."""
497-
return compute_create2_address(
498-
address=self.address(),
499-
salt=salt,
500-
initcode=self.initcode,
501-
)
500+
if salt not in self._cached_created_contracts:
501+
self._cached_created_contracts[salt] = compute_create2_address(
502+
address=self.address(),
503+
salt=salt,
504+
initcode=self.initcode,
505+
)
506+
return self._cached_created_contracts[salt]

tests/benchmark/compute/instruction/test_account_query.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
"""
1414

1515
import math
16-
from typing import Any
16+
from typing import Any, Dict
1717

1818
import pytest
1919
from execution_testing import (
@@ -539,17 +539,25 @@ def calldata(iteration_count: int, start_iteration: int) -> bytes:
539539
# Access list generator for warm access tests.
540540
# When access_warm=True, include all contract addresses that will be
541541
# accessed in each transaction to warm them up via access list.
542+
# Note: This access list generation is very expensive due to the binary
543+
# search, which builds different access lists using the same elements
544+
# over and over. Caching the elements helps a bit.
545+
access_list_cache: Dict[int, AccessList] = {}
546+
542547
def access_list_generator(
543548
iteration_count: int, start_iteration: int
544549
) -> list[AccessList] | None:
545550
if not access_warm:
546551
return None
547552
return [
548-
AccessList(
549-
address=custom_sized_contract_factory.created_contract_address(
550-
salt=i
553+
access_list_cache.setdefault(
554+
i,
555+
AccessList(
556+
address=custom_sized_contract_factory.created_contract_address(
557+
salt=i
558+
),
559+
storage_keys=[],
551560
),
552-
storage_keys=[],
553561
)
554562
for i in range(start_iteration, start_iteration + iteration_count)
555563
]

0 commit comments

Comments
 (0)