Skip to content

Commit

Permalink
feat: improve batch copy performance (#3483)
Browse files Browse the repository at this point in the history
per cancun, eip-5656, this commit adds the use of mcopy for memory
copies. it also
- adds heuristics to use loops vs unrolled loops for batch copies.
- adds helper functions `vyper.codegen.core._opt_[gas,codesize,none]()`
  to detect optimization mode during codegen
- adds `--optimize none` to CLI options, with the intent of phasing out
  `--no-optimize` if the ergonomics are better.
  • Loading branch information
charles-cooper authored Jul 15, 2023
1 parent 593c9b8 commit 5dc3ac7
Show file tree
Hide file tree
Showing 13 changed files with 221 additions and 99 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/era-tester.yml
Original file line number Diff line number Diff line change
Expand Up @@ -101,11 +101,11 @@ jobs:
if: ${{ github.ref != 'refs/heads/master' }}
run: |
cd era-compiler-tester
cargo run --release --bin compiler-tester -- -v --path=tests/vyper/ --mode="M0B0 ${{ env.VYPER_VERSION }}"
cargo run --release --bin compiler-tester -- --path=tests/vyper/ --mode="M0B0 ${{ env.VYPER_VERSION }}"
- name: Run tester (slow)
# Run era tester across the LLVM optimization matrix
if: ${{ github.ref == 'refs/heads/master' }}
run: |
cd era-compiler-tester
cargo run --release --bin compiler-tester -- -v --path=tests/vyper/ --mode="M*B* ${{ env.VYPER_VERSION }}"
cargo run --release --bin compiler-tester -- --path=tests/vyper/ --mode="M*B* ${{ env.VYPER_VERSION }}"
1 change: 0 additions & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ addopts = -n auto
--cov-report html
--cov-report xml
--cov=vyper
--hypothesis-show-statistics
python_files = test_*.py
testpaths = tests
markers =
Expand Down
7 changes: 5 additions & 2 deletions tests/compiler/test_opcodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,5 +59,8 @@ def test_get_opcodes(evm_version):
assert "PUSH0" in ops

if evm_version in ("cancun",):
assert "TLOAD" in ops
assert "TSTORE" in ops
for op in ("TLOAD", "TSTORE", "MCOPY"):
assert op in ops
else:
for op in ("TLOAD", "TSTORE", "MCOPY"):
assert op not in ops
89 changes: 48 additions & 41 deletions tests/parser/functions/test_slice.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import hypothesis.strategies as st
import pytest
from hypothesis import given, settings

from vyper.exceptions import ArgumentException

Expand All @@ -9,14 +11,6 @@ def _generate_bytes(length):
return bytes(list(range(length)))


# good numbers to try
_fun_numbers = [0, 1, 5, 31, 32, 33, 64, 99, 100, 101]


# [b"", b"\x01", b"\x02"...]
_bytes_examples = [_generate_bytes(i) for i in _fun_numbers if i <= 100]


def test_basic_slice(get_contract_with_gas_estimation):
code = """
@external
Expand All @@ -31,12 +25,16 @@ def slice_tower_test(inp1: Bytes[50]) -> Bytes[50]:
assert x == b"klmnopqrst", x


@pytest.mark.parametrize("bytesdata", _bytes_examples)
@pytest.mark.parametrize("start", _fun_numbers)
# note: optimization boundaries at 32, 64 and 320 depending on mode
_draw_1024 = st.integers(min_value=0, max_value=1024)
_draw_1024_1 = st.integers(min_value=1, max_value=1024)
_bytes_1024 = st.binary(min_size=0, max_size=1024)


@pytest.mark.parametrize("literal_start", (True, False))
@pytest.mark.parametrize("length", _fun_numbers)
@pytest.mark.parametrize("literal_length", (True, False))
@pytest.mark.fuzzing
@given(start=_draw_1024, length=_draw_1024, length_bound=_draw_1024_1, bytesdata=_bytes_1024)
@settings(max_examples=25, deadline=None)
def test_slice_immutable(
get_contract,
assert_compile_failed,
Expand All @@ -46,47 +44,48 @@ def test_slice_immutable(
literal_start,
length,
literal_length,
length_bound,
):
_start = start if literal_start else "start"
_length = length if literal_length else "length"

code = f"""
IMMUTABLE_BYTES: immutable(Bytes[100])
IMMUTABLE_SLICE: immutable(Bytes[100])
IMMUTABLE_BYTES: immutable(Bytes[{length_bound}])
IMMUTABLE_SLICE: immutable(Bytes[{length_bound}])
@external
def __init__(inp: Bytes[100], start: uint256, length: uint256):
def __init__(inp: Bytes[{length_bound}], start: uint256, length: uint256):
IMMUTABLE_BYTES = inp
IMMUTABLE_SLICE = slice(IMMUTABLE_BYTES, {_start}, {_length})
@external
def do_splice() -> Bytes[100]:
def do_splice() -> Bytes[{length_bound}]:
return IMMUTABLE_SLICE
"""

def _get_contract():
return get_contract(code, bytesdata, start, length)

if (
(start + length > 100 and literal_start and literal_length)
or (literal_length and length > 100)
or (literal_start and start > 100)
(start + length > length_bound and literal_start and literal_length)
or (literal_length and length > length_bound)
or (literal_start and start > length_bound)
or (literal_length and length < 1)
):
assert_compile_failed(
lambda: get_contract(code, bytesdata, start, length), ArgumentException
)
elif start + length > len(bytesdata):
assert_tx_failed(lambda: get_contract(code, bytesdata, start, length))
assert_compile_failed(lambda: _get_contract(), ArgumentException)
elif start + length > len(bytesdata) or (len(bytesdata) > length_bound):
# deploy fail
assert_tx_failed(lambda: _get_contract())
else:
c = get_contract(code, bytesdata, start, length)
c = _get_contract()
assert c.do_splice() == bytesdata[start : start + length]


@pytest.mark.parametrize("location", ("storage", "calldata", "memory", "literal", "code"))
@pytest.mark.parametrize("bytesdata", _bytes_examples)
@pytest.mark.parametrize("start", _fun_numbers)
@pytest.mark.parametrize("literal_start", (True, False))
@pytest.mark.parametrize("length", _fun_numbers)
@pytest.mark.parametrize("literal_length", (True, False))
@pytest.mark.fuzzing
@given(start=_draw_1024, length=_draw_1024, length_bound=_draw_1024_1, bytesdata=_bytes_1024)
@settings(max_examples=25, deadline=None)
def test_slice_bytes(
get_contract,
assert_compile_failed,
Expand All @@ -97,9 +96,10 @@ def test_slice_bytes(
literal_start,
length,
literal_length,
length_bound,
):
if location == "memory":
spliced_code = "foo: Bytes[100] = inp"
spliced_code = f"foo: Bytes[{length_bound}] = inp"
foo = "foo"
elif location == "storage":
spliced_code = "self.foo = inp"
Expand All @@ -120,31 +120,38 @@ def test_slice_bytes(
_length = length if literal_length else "length"

code = f"""
foo: Bytes[100]
IMMUTABLE_BYTES: immutable(Bytes[100])
foo: Bytes[{length_bound}]
IMMUTABLE_BYTES: immutable(Bytes[{length_bound}])
@external
def __init__(foo: Bytes[100]):
def __init__(foo: Bytes[{length_bound}]):
IMMUTABLE_BYTES = foo
@external
def do_slice(inp: Bytes[100], start: uint256, length: uint256) -> Bytes[100]:
def do_slice(inp: Bytes[{length_bound}], start: uint256, length: uint256) -> Bytes[{length_bound}]:
{spliced_code}
return slice({foo}, {_start}, {_length})
"""

length_bound = len(bytesdata) if location == "literal" else 100
def _get_contract():
return get_contract(code, bytesdata)

data_length = len(bytesdata) if location == "literal" else length_bound
if (
(start + length > length_bound and literal_start and literal_length)
or (literal_length and length > length_bound)
or (literal_start and start > length_bound)
(start + length > data_length and literal_start and literal_length)
or (literal_length and length > data_length)
or (location == "literal" and len(bytesdata) > length_bound)
or (literal_start and start > data_length)
or (literal_length and length < 1)
):
assert_compile_failed(lambda: get_contract(code, bytesdata), ArgumentException)
assert_compile_failed(lambda: _get_contract(), ArgumentException)
elif len(bytesdata) > data_length:
# deploy fail
assert_tx_failed(lambda: _get_contract())
elif start + length > len(bytesdata):
c = get_contract(code, bytesdata)
c = _get_contract()
assert_tx_failed(lambda: c.do_slice(bytesdata, start, length))
else:
c = get_contract(code, bytesdata)
c = _get_contract()
assert c.do_slice(bytesdata, start, length) == bytesdata[start : start + length], code


Expand Down
12 changes: 3 additions & 9 deletions tests/parser/types/test_dynamic_array.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import pytest

from vyper.compiler.settings import OptimizationLevel
from vyper.exceptions import (
ArgumentException,
ArrayIndexException,
Expand Down Expand Up @@ -1585,14 +1584,9 @@ def bar2() -> uint256:
newFoo.b1[1][0][0].a1[0][1][1] + \\
newFoo.b1[0][1][0].a1[0][0][0]
"""

if optimize == OptimizationLevel.NONE:
# fails at assembly stage with too many stack variables
assert_compile_failed(lambda: get_contract(code), Exception)
else:
c = get_contract(code)
assert c.bar() == [[[3, 7], [7, 3]], [[7, 3], [0, 0]]]
assert c.bar2() == 0
c = get_contract(code)
assert c.bar() == [[[3, 7], [7, 3]], [[7, 3], [0, 0]]]
assert c.bar2() == 0


def test_tuple_of_lists(get_contract):
Expand Down
2 changes: 1 addition & 1 deletion vyper/cli/vyper_compile.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ def _parse_args(argv):
dest="evm_version",
)
parser.add_argument("--no-optimize", help="Do not optimize", action="store_true")
parser.add_argument("--optimize", help="Optimization flag", choices=["gas", "codesize"])
parser.add_argument("--optimize", help="Optimization flag", choices=["gas", "codesize", "none"])
parser.add_argument(
"--no-bytecode-metadata", help="Do not add metadata to bytecode", action="store_true"
)
Expand Down
Loading

0 comments on commit 5dc3ac7

Please sign in to comment.