Skip to content

Commit

Permalink
OptimizeOptions Takes Over frame_pointers Option (#613)
Browse files Browse the repository at this point in the history
  • Loading branch information
tzaffi committed Dec 13, 2022
1 parent ce23e15 commit a240ec0
Show file tree
Hide file tree
Showing 9 changed files with 190 additions and 52 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@

# Added
* Added frame pointer support for subroutine arguments, replacing the previous usage of scratch. ([#562](https://github.com/algorand/pyteal/pull/562))
* Added `frame_pointers` property in `OptimizeOptions` to optimize away scratch slots during subroutine calls. This defaults to frame pointer usage when not specified. ([#613](https://github.com/algorand/pyteal/pull/613))

# Fixed
* Allowing the `MethodCall` and `ExecuteMethodCall` to be passed `None` as app_id argument in the case of an app create transaction ([#592](https://github.com/algorand/pyteal/pull/592))

# Changed
* Introducing `AbstractVar` to abstract value access: store, load, and stack type. ([#584](https://github.com/algorand/pyteal/pull/584))
* NOTE: a backwards incompatable change was imposed in this PR: previous ABI value's public member `stored_value` with type `ScratchVar`, is now changed to protected member `_stored_value` with type `AbstractVar`.
* Starting with program version 9, when `scratch_slots` flag isn't provided to `OptimizeOptions`, default to optimizing. For versions 8 and earlier the default is and remains to _not_ optimize. ([#613](https://github.com/algorand/pyteal/pull/613))

# 0.20.1

Expand Down
1 change: 0 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,6 @@ local-gh-job:
local-gh-simulate:
act


# ---- Extras ---- #

coverage:
Expand Down
95 changes: 87 additions & 8 deletions docs/compiler_optimization.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

Compiler Optimization
========================
**The optimizer is at an early stage and is disabled by default. Backwards compatability cannot be
guaranteed at this point.**

The optimizer is a tool for improving performance and reducing resource consumption. In this context,
the terms *performance* and *resource* can apply across multiple dimensions, including but not limited
Expand All @@ -13,18 +11,99 @@ Optimizer Usage
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

The compiler determines which optimizations to apply based on the provided :any:`OptimizeOptions` object as
shown in the code block below. The :any:`OptimizeOptions` constructor receives a set of keyword arguments
representing flags corresponding to particular optimizations. If arguments are not provided to the
constructor or no :any:`OptimizeOptions` object is passed to :any:`compileTeal` then the default behavior is
that no optimizations are applied.
shown in the code block below. Both :any:`compileTeal` as well as the :any:`Router.compile_program` method
can receive an :code:`optimize` parameter of type :any:`OptimizeOptions`.


.. code-block:: python
# optimize scratch slots for all program versions (shown is version 4)
optimize_options = OptimizeOptions(scratch_slots=True)
compileTeal(approval_program(), mode=Mode.Application, version=4, optimize=optimize_options)
============================== ================================================================================ ===========================
Optimization Flag Description Default
============================== ================================================================================ ===========================
:code:`scratch_slots` A boolean describing whether or not scratch slot optimization should be applied. :code:`False`
============================== ================================================================================ ===========================

Default Behavior
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

The :any:`OptimizeOptions` constructor receives keyword arguments representing flags for particular optimizations.
If an argument is not provided to the constructor of :any:`OptimizeOptions`, a default program version dependent
optimization behavior is used in its place according to the table below.


.. list-table::
:widths: 25 25 25 25 25
:header-rows: 1

* - Optimization Flag
- Value
- Interpretation
- Program Version
- Behavior
* - :code:`scratch_slots`
- :code:`None`
- Default
- ≥ 9
- Scratch slot optimization is applied
* -
- :code:`None`
- Default
- ≤ 8
- Scratch slot optimization is *not* applied
* -
- :code:`True`
- Enable
- *any*
- Scratch slot optimization is applied
* -
- :code:`False`
- Disable
- *any*
- Scratch slot optimization is *not* applied
* - :code:`frame_pointers`
- :code:`None`
- Default
- ≥ 8
- Frame pointers available and are therefore applied
* -
- :code:`None`
- Default
- ≤ 7
- Frame pointers not available and not applied
* -
- :code:`True`
- Enable
- ≥ 8
- Frame pointers available and applied
* -
- :code:`True`
- *attempt*
- ≤ 7
- An error occurs when attempting to compile as frame pointers are not available
* -
- :code:`False`
- Disable
- *any*
- Frame pointers not applied


When the :code:`optimize` parameter is omitted in :any:`compileTeal`
or :any:`Router.compile_program`, all parameters conform to program version dependent defaults
as defined in the above table. For example:

.. code-block:: python
optimize_options = OptimizeOptions(scratch_slots=True)
compileTeal(approval_program(), mode=Mode.Application, version=4, optimize=optimize_options)
# apply default optimization behavior by NOT providing `OptimizeOptions`
# for version 9 as shown next, this is equivalent to passing in
# optimize=OptimizeOptions(scratch_slots=True, frame_pointers=True)
compileTeal(approval_program(), mode=Mode.Application, version=9)
# for version 8 as shown next, this is equivalent to passing in
# optimize=OptimizeOptions(scratch_slots=False, frame_pointers=True)
compileTeal(approval_program(), mode=Mode.Application, version=8)
5 changes: 1 addition & 4 deletions pyteal/ast/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -723,8 +723,7 @@ def compile_program(
*,
version: int = DEFAULT_TEAL_VERSION,
assemble_constants: bool = False,
optimize: OptimizeOptions = None,
frame_pointers: Optional[bool] = None,
optimize: Optional[OptimizeOptions] = None,
) -> tuple[str, str, sdk_abi.Contract]:
"""
Constructs and compiles approval and clear-state programs from the registered methods and
Expand All @@ -750,15 +749,13 @@ def compile_program(
version=version,
assembleConstants=assemble_constants,
optimize=optimize,
frame_pointers=frame_pointers,
)
csp_compiled = compileTeal(
csp,
Mode.Application,
version=version,
assembleConstants=assemble_constants,
optimize=optimize,
frame_pointers=frame_pointers,
)
return ap_compiled, csp_compiled, contract

Expand Down
4 changes: 2 additions & 2 deletions pyteal/ast/subroutine_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import pyteal as pt
from pyteal.ast.frame import Proto
from pyteal.ast.subroutine import ABIReturnSubroutine, SubroutineEval
from pyteal.compiler.compiler import FRAME_POINTER_VERSION
from pyteal.compiler.compiler import FRAME_POINTERS_VERSION

options = pt.CompileOptions(version=5)
options_v8 = pt.CompileOptions(version=8)
Expand Down Expand Up @@ -1549,5 +1549,5 @@ def abi_meth_2(a: pt.abi.Uint64, b: pt.abi.Uint64, *, output: pt.abi.Uint64):

def test_frame_option_version_range_well_formed():
assert (
pt.Op.callsub.min_version < FRAME_POINTER_VERSION < pt.MAX_PROGRAM_VERSION + 1
pt.Op.callsub.min_version < FRAME_POINTERS_VERSION < pt.MAX_PROGRAM_VERSION + 1
)
45 changes: 17 additions & 28 deletions pyteal/compiler/compiler.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import List, Tuple, Set, Dict, Optional, cast
from typing import Final, List, Tuple, Set, Dict, Optional, cast

from pyteal.compiler.optimizer import OptimizeOptions, apply_global_optimizations

Expand Down Expand Up @@ -26,7 +26,8 @@
from pyteal.compiler.constants import createConstantBlocks

MAX_PROGRAM_VERSION = 8
FRAME_POINTER_VERSION = 8
FRAME_POINTERS_VERSION = 8
DEFAULT_SCRATCH_SLOT_OPTIMIZE_VERSION = 9
MIN_PROGRAM_VERSION = 2
DEFAULT_PROGRAM_VERSION = MIN_PROGRAM_VERSION

Expand All @@ -45,23 +46,14 @@ def __init__(
*,
mode: Mode = Mode.Signature,
version: int = DEFAULT_PROGRAM_VERSION,
optimize: OptimizeOptions = None,
frame_pointers: Optional[bool] = None,
optimize: Optional[OptimizeOptions] = None,
) -> None:
self.mode = mode
self.version = version
self.optimize = optimize if optimize is not None else OptimizeOptions()
self.use_frame_pointer: bool

if frame_pointers is None:
self.use_frame_pointer = self.version >= FRAME_POINTER_VERSION
else:
if frame_pointers and self.version < FRAME_POINTER_VERSION:
raise TealInputError(
f"Try to use frame pointer with an insufficient version {self.version}."
)
else:
self.use_frame_pointer = frame_pointers
self.mode: Final[Mode] = mode
self.version: Final[int] = version
self.optimize: Final[OptimizeOptions] = optimize or OptimizeOptions()
self.use_frame_pointers: Final[bool] = self.optimize.use_frame_pointers(
self.version
)

self.currentSubroutine: Optional[SubroutineDefinition] = None

Expand Down Expand Up @@ -168,14 +160,14 @@ def compileSubroutine(
if (
currentSubroutine
and currentSubroutine.get_declaration_by_option(
options.use_frame_pointer
options.use_frame_pointers
).deferred_expr
):
# this represents code that should be inserted before each retsub op
deferred_expr = cast(
Expr,
currentSubroutine.get_declaration_by_option(
options.use_frame_pointer
options.use_frame_pointers
).deferred_expr,
)

Expand Down Expand Up @@ -228,7 +220,7 @@ def compileSubroutine(
newSubroutines = referencedSubroutines - subroutine_start_blocks.keys()
for subroutine in sorted(newSubroutines, key=lambda subroutine: subroutine.id):
compileSubroutine(
subroutine.get_declaration_by_option(options.use_frame_pointer),
subroutine.get_declaration_by_option(options.use_frame_pointers),
options,
subroutineGraph,
subroutine_start_blocks,
Expand Down Expand Up @@ -256,8 +248,7 @@ def compileTeal(
*,
version: int = DEFAULT_PROGRAM_VERSION,
assembleConstants: bool = False,
optimize: OptimizeOptions = None,
frame_pointers: Optional[bool] = None,
optimize: Optional[OptimizeOptions] = None,
) -> str:
"""Compile a PyTeal expression into TEAL assembly.
Expand Down Expand Up @@ -291,9 +282,7 @@ def compileTeal(
)
)

options = CompileOptions(
mode=mode, version=version, optimize=optimize, frame_pointers=frame_pointers
)
options = CompileOptions(mode=mode, version=version, optimize=optimize)

subroutineGraph: Dict[SubroutineDefinition, Set[SubroutineDefinition]] = dict()
subroutine_start_blocks: Dict[Optional[SubroutineDefinition], TealBlock] = dict()
Expand All @@ -307,12 +296,12 @@ def compileTeal(
# control flow graph, the optimizer requires context across block boundaries. This
# is necessary for the dependency checking of local slots. Global slots, slots
# used by DynamicScratchVar, and reserved slots are not optimized.
if options.optimize.scratch_slots:
if options.optimize.optimize_scratch_slots(version):
options.optimize._skip_slots = collect_unoptimized_slots(
subroutine_start_blocks
)
for start in subroutine_start_blocks.values():
apply_global_optimizations(start, options.optimize)
apply_global_optimizations(start, options.optimize, version)

localSlotAssignments = assignScratchSlotsToSubroutines(subroutine_start_blocks)

Expand Down
4 changes: 2 additions & 2 deletions pyteal/compiler/compiler_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -2349,10 +2349,10 @@ def approve_if_odd(condition_encoding: pt.abi.Uint32) -> pt.Expr:

with pytest.raises(pt.TealInputError) as e:
pt.Router("will-error", on_completion_actions).compile_program(
version=6, frame_pointers=True
version=6, optimize=pt.OptimizeOptions(frame_pointers=True)
)

assert "Try to use frame pointer with an insufficient version 6" in str(e)
assert "Frame pointers aren't available" in str(e.value)

_router_with_oc = pt.Router(
"ASimpleQuestionablyRobustContract", on_completion_actions
Expand Down
49 changes: 42 additions & 7 deletions pyteal/compiler/optimizer/optimizer.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from typing import Set
from typing import Final, Optional, Set

from pyteal.ast import ScratchSlot
from pyteal.ir import TealBlock, TealOp, Op
from pyteal.errors import TealInternalError
from pyteal.errors import TealInternalError, verifyProgramVersion


class OptimizeOptions:
Expand All @@ -21,13 +22,45 @@ class OptimizeOptions:
Args:
scratch_slots (optional): cancel contiguous store/load operations
that have no load dependencies elsewhere.
that have no load dependencies elsewhere. Starting with program version 9, defaults to optimizing.
frame_pointers (optional): employ frame pointers instead of scratch slots during compilation.
Available only starting in program version 8. Defaults to optimizing starting in program version 8.
"""

def __init__(self, *, scratch_slots: bool = False):
self.scratch_slots = scratch_slots
def __init__(
self,
*,
scratch_slots: Optional[bool] = None,
frame_pointers: Optional[bool] = None,
):
self._scratch_slots: Final[Optional[bool]] = scratch_slots
self._frame_pointers: Final[Optional[bool]] = frame_pointers

self._skip_slots: Set[ScratchSlot] = set()

def optimize_scratch_slots(self, version: int) -> bool:
from pyteal.compiler.compiler import DEFAULT_SCRATCH_SLOT_OPTIMIZE_VERSION

if self._scratch_slots is None:
return version >= DEFAULT_SCRATCH_SLOT_OPTIMIZE_VERSION

return self._scratch_slots

def use_frame_pointers(self, version: int) -> bool:
from pyteal.compiler.compiler import FRAME_POINTERS_VERSION

if self._frame_pointers is None:
return version >= FRAME_POINTERS_VERSION

if self._frame_pointers:
verifyProgramVersion(
FRAME_POINTERS_VERSION,
version,
f"Frame pointers aren't available when compiling to program version {version}",
)

return self._frame_pointers


def _remove_extraneous_slot_access(start: TealBlock, remove: Set[ScratchSlot]):
def keep_op(op: TealOp) -> bool:
Expand Down Expand Up @@ -87,13 +120,15 @@ def _apply_slot_to_stack(
_remove_extraneous_slot_access(start, slots_to_remove)


def apply_global_optimizations(start: TealBlock, options: OptimizeOptions) -> TealBlock:
def apply_global_optimizations(
start: TealBlock, options: OptimizeOptions, version: int
) -> TealBlock:
# limit number of iterations to length of teal program to avoid potential
# infinite loops.
for block in TealBlock.Iterate(start):
for _ in range(len(block.ops)):
prev_ops = block.ops.copy()
if options.scratch_slots:
if options.optimize_scratch_slots(version):
_apply_slot_to_stack(block, start, options._skip_slots)

if prev_ops == block.ops:
Expand Down

0 comments on commit a240ec0

Please sign in to comment.