Skip to content

Commit

Permalink
chore: polishing for codegen (#2794)
Browse files Browse the repository at this point in the history
add some notes, and a small conversion optimization:
skip `unwrap_location` if the conversion does not do anything. this
triggers downstream zero-copy optimizations.

also skip a copy if the length is known to be 0 at compile-time.
  • Loading branch information
charles-cooper authored Jun 15, 2022
1 parent f46649e commit 58a5ae5
Show file tree
Hide file tree
Showing 4 changed files with 35 additions and 19 deletions.
12 changes: 9 additions & 3 deletions vyper/builtin_functions/convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,6 @@ def to_bool(expr, arg, out_typ):

@_input_types("int", "bytes_m", "decimal", "bytes", "address", "bool")
def to_int(expr, arg, out_typ):

int_info = out_typ._int_info

assert int_info.bits % 8 == 0
Expand Down Expand Up @@ -462,6 +461,7 @@ def convert(expr, context):

arg_ast = expr.args[0]
arg = Expr(arg_ast, context).ir_node
original_arg = arg
out_typ = context.parse_type(expr.args[1])

if isinstance(arg.typ, BaseType):
Expand All @@ -484,6 +484,12 @@ def convert(expr, context):
else:
raise StructureException(f"Conversion to {out_typ} is invalid.", arg_ast)

ret = b.resolve(ret)
# test if arg actually changed. if not, we do not need to use
# unwrap_location (this can reduce memory traffic for downstream
# operations which are in-place, like the returndata routine)
test_arg = IRnode.from_list(arg, typ=out_typ)
if test_arg == ret:
original_arg.typ = out_typ
return original_arg

return IRnode.from_list(ret)
return IRnode.from_list(b.resolve(ret))
2 changes: 2 additions & 0 deletions vyper/codegen/abi_encoder.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,8 @@ def abi_encoding_matches_vyper(typ):
# the abi_encode routine will push the output len onto the stack,
# otherwise it will return 0 items to the stack.
def abi_encode(dst, ir_node, context, bufsz, returns_len=False):
# TODO change dst to be an IRnode so it has type info to begin with.
# setting the typ of dst to ir_node.typ is a footgun.
dst = IRnode.from_list(dst, typ=ir_node.typ, location=MEMORY)
abi_t = dst.typ.abi_type
size_bound = abi_t.size_bound()
Expand Down
36 changes: 21 additions & 15 deletions vyper/codegen/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ def make_byte_array_copier(dst, src):

_check_assign_bytes(dst, src)

# TODO: remove this branch, copy_bytes and get_bytearray_length should handle
if src.value == "~empty":
# set length word to 0.
return STORE(dst, 0)
Expand Down Expand Up @@ -120,16 +121,18 @@ def _dynarray_make_setter(dst, src):
# the layout does not match our memory layout
should_loop = src.encoding == Encoding.ABI and src.typ.subtype.abi_type.is_dynamic()

# if the subtype is dynamic, there might be a lot of
# unused space inside of each element. for instance
# if the data is not validated, we must loop to unpack
should_loop |= needs_clamp(src.typ.subtype, src.encoding)

# performance: if the subtype is dynamic, there might be a lot
# of unused space inside of each element. for instance
# DynArray[DynArray[uint256, 100], 5] where all the child
# arrays are empty - for this case, we recursively call
# into make_setter instead of straight bytes copy
# TODO we can make this heuristic more precise, e.g.
# loop when subtype.is_dynamic AND location == storage
# OR array_size <= /bound where loop is cheaper than memcpy/
should_loop |= src.typ.subtype.abi_type.is_dynamic()
should_loop |= needs_clamp(src.typ.subtype, src.encoding)

with get_dyn_array_count(src).cache_when_complex("darray_count") as (b2, count):
ret = ["seq"]
Expand Down Expand Up @@ -180,10 +183,14 @@ def copy_bytes(dst, src, length, length_bound):
"copy_bytes_count"
) as (b2, length), dst.cache_when_complex("dst") as (b3, dst):

assert length_bound >= 0
assert isinstance(length_bound, int) and length_bound >= 0

# correctness: do not clobber dst
if length_bound == 0:
return IRnode.from_list(["seq"], annotation=annotation)
# performance: if we know that length is 0, do not copy anything
if length.value == 0:
return IRnode.from_list(["seq"], annotation=annotation)

assert src.is_pointer and dst.is_pointer

Expand Down Expand Up @@ -241,6 +248,9 @@ def copy_bytes(dst, src, length, length_bound):
# get the number of bytes at runtime
def get_bytearray_length(arg):
typ = BaseType("uint256")

# TODO add "~empty" case to mirror get_dyn_array_count

return IRnode.from_list(LOAD(arg), typ=typ)


Expand All @@ -254,7 +264,7 @@ def get_dyn_array_count(arg):
return IRnode.from_list(len(arg.args), typ=typ)

if arg.value == "~empty":
# empty(DynArray[])
# empty(DynArray[...])
return IRnode.from_list(0, typ=typ)

return IRnode.from_list(LOAD(arg), typ=typ)
Expand All @@ -281,6 +291,7 @@ def append_dyn_array(darray_node, elem_node):

def pop_dyn_array(darray_node, return_popped_item):
assert isinstance(darray_node.typ, DArrayType)
assert darray_node.encoding == Encoding.VYPER
ret = ["seq"]
with darray_node.cache_when_complex("darray") as (b1, darray_node):
old_len = clamp("gt", get_dyn_array_count(darray_node), 0)
Expand All @@ -295,12 +306,9 @@ def pop_dyn_array(darray_node, return_popped_item):
ret.append(popped_item)
typ = popped_item.typ
location = popped_item.location
encoding = popped_item.encoding
else:
typ, location, encoding = None, None, None
return IRnode.from_list(
b1.resolve(b2.resolve(ret)), typ=typ, location=location, encoding=encoding
)
typ, location = None, None
return IRnode.from_list(b1.resolve(b2.resolve(ret)), typ=typ, location=location)


def getpos(node):
Expand Down Expand Up @@ -534,6 +542,7 @@ def unwrap_location(orig):
else:
# CMC 2022-03-24 TODO refactor so this branch can be removed
if orig.value == "~empty":
# must be word type
return IRnode.from_list(0, typ=orig.typ)
return orig

Expand Down Expand Up @@ -821,12 +830,9 @@ def eval_seq(ir_node):
def is_return_from_function(node):
if isinstance(node, vy_ast.Expr) and node.get("value.func.id") == "selfdestruct":
return True
if isinstance(node, vy_ast.Return):
return True
elif isinstance(node, vy_ast.Raise):
if isinstance(node, (vy_ast.Return, vy_ast.Raise)):
return True
else:
return False
return False


def check_single_exit(fn_node):
Expand Down
4 changes: 3 additions & 1 deletion vyper/codegen/expr.py
Original file line number Diff line number Diff line change
Expand Up @@ -475,7 +475,7 @@ def parse_BinOp(self):

# Sanity check - ensure that we aren't dealing with different types
# This should be unreachable due to the type check pass
assert ltyp == rtyp, "unreachable"
assert ltyp == rtyp, f"unreachable, {ltyp}!={rtyp}, {self.expr}"

arith = None
if isinstance(self.expr.op, (vy_ast.Add, vy_ast.Sub)):
Expand Down Expand Up @@ -963,6 +963,8 @@ def struct_literals(expr, name, context):
sub = Expr(value, context).ir_node
member_subs[key.id] = sub
member_typs[key.id] = sub.typ

# TODO: get struct type from context.global_ctx.parse_type(name)
return IRnode.from_list(
["multi"] + [member_subs[key] for key in member_subs.keys()],
typ=StructType(member_typs, name, is_literal=True),
Expand Down

0 comments on commit 58a5ae5

Please sign in to comment.