From 6cf7b6bf15116ca8be1b3d1a73eca7625240a287 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Thu, 30 May 2024 17:32:35 -0400 Subject: [PATCH 01/23] feat[venom]: extract literals pass extract IRLiterals which are instruction arguments; this reduces pressure on the stack scheduler --- vyper/venom/__init__.py | 2 ++ vyper/venom/passes/extract_literals.py | 37 ++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 vyper/venom/passes/extract_literals.py diff --git a/vyper/venom/__init__.py b/vyper/venom/__init__.py index cd981cd462..d7a2c27ddf 100644 --- a/vyper/venom/__init__.py +++ b/vyper/venom/__init__.py @@ -13,6 +13,7 @@ from vyper.venom.passes.branch_optimization import BranchOptimizationPass from vyper.venom.passes.dft import DFTPass from vyper.venom.passes.make_ssa import MakeSSA +from vyper.venom.passes.extract_literals import ExtractLiteralsPass from vyper.venom.passes.mem2var import Mem2Var from vyper.venom.passes.remove_unused_variables import RemoveUnusedVariablesPass from vyper.venom.passes.sccp import SCCP @@ -53,6 +54,7 @@ def _run_passes(fn: IRFunction, optimize: OptimizationLevel) -> None: SimplifyCFGPass(ac, fn).run_pass() AlgebraicOptimizationPass(ac, fn).run_pass() BranchOptimizationPass(ac, fn).run_pass() + ExtractLiteralsPass(ac, fn).run_pass() RemoveUnusedVariablesPass(ac, fn).run_pass() DFTPass(ac, fn).run_pass() diff --git a/vyper/venom/passes/extract_literals.py b/vyper/venom/passes/extract_literals.py new file mode 100644 index 0000000000..3ee54af3c9 --- /dev/null +++ b/vyper/venom/passes/extract_literals.py @@ -0,0 +1,37 @@ +from vyper.utils import OrderedSet +from vyper.venom.analysis.dfg import DFGAnalysis +from vyper.venom.analysis.liveness import LivenessAnalysis +from vyper.venom.basicblock import IRInstruction, IRLiteral +from vyper.venom.passes.base_pass import IRPass + + +class ExtractLiteralsPass(IRPass): + """ + This pass extracts literals so that they can be reordered by the DFT pass + """ + def run_pass(self): + for bb in self.function.get_basic_blocks(): + self._process_bb(bb) + + self.analyses_cache.invalidate_analysis(DFGAnalysis) + self.analyses_cache.invalidate_analysis(LivenessAnalysis) + + def _process_bb(self, bb): + i = 0 + while i < len(bb.instructions): + inst = bb.instructions[i] + if inst.opcode == "store": + i += 1 + continue + + for j, op in enumerate(inst.operands): + # first operand to log is magic + if inst.opcode == "log" and j == 0: + continue + + if isinstance(op, IRLiteral): + var = self.function.get_next_variable() + to_insert = IRInstruction("store", [op], var) + bb.insert_instruction(to_insert, index=i) + inst.operands[j] = var + i += 1 From ef7c3691c5597c56104fec591b0db4ffbf076602 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Fri, 31 May 2024 07:38:45 -0400 Subject: [PATCH 02/23] feat: store expansion pass --- vyper/venom/__init__.py | 2 ++ vyper/venom/passes/store_expansion.py | 50 +++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 vyper/venom/passes/store_expansion.py diff --git a/vyper/venom/__init__.py b/vyper/venom/__init__.py index d7a2c27ddf..2da94bf98d 100644 --- a/vyper/venom/__init__.py +++ b/vyper/venom/__init__.py @@ -14,6 +14,7 @@ from vyper.venom.passes.dft import DFTPass from vyper.venom.passes.make_ssa import MakeSSA from vyper.venom.passes.extract_literals import ExtractLiteralsPass +from vyper.venom.passes.store_expansion import StoreExpansionPass from vyper.venom.passes.mem2var import Mem2Var from vyper.venom.passes.remove_unused_variables import RemoveUnusedVariablesPass from vyper.venom.passes.sccp import SCCP @@ -55,6 +56,7 @@ def _run_passes(fn: IRFunction, optimize: OptimizationLevel) -> None: AlgebraicOptimizationPass(ac, fn).run_pass() BranchOptimizationPass(ac, fn).run_pass() ExtractLiteralsPass(ac, fn).run_pass() + StoreExpansionPass(ac, fn).run_pass() RemoveUnusedVariablesPass(ac, fn).run_pass() DFTPass(ac, fn).run_pass() diff --git a/vyper/venom/passes/store_expansion.py b/vyper/venom/passes/store_expansion.py new file mode 100644 index 0000000000..91911e3854 --- /dev/null +++ b/vyper/venom/passes/store_expansion.py @@ -0,0 +1,50 @@ +from vyper.venom.analysis.cfg import CFGAnalysis +from vyper.venom.analysis.dfg import DFGAnalysis +from vyper.venom.analysis.dominators import DominatorTreeAnalysis +from vyper.venom.analysis.liveness import LivenessAnalysis +from vyper.venom.basicblock import IRVariable, IRInstruction +from vyper.venom.passes.base_pass import IRPass + + +class StoreExpansionPass(IRPass): + """ + This pass expands variables to their uses though `store` instructions, + reducing pressure on the stack scheduler + """ + + def run_pass(self): + self.analyses_cache.request_analysis(CFGAnalysis) + dfg = self.analyses_cache.request_analysis(DFGAnalysis) + + for bb in self.function.get_basic_blocks(): + for idx, inst in enumerate(bb.instructions): + if inst.opcode != "store": + continue + + self._process_inst(dfg, inst, idx) + + self.analyses_cache.invalidate_analysis(LivenessAnalysis) + self.analyses_cache.invalidate_analysis(DFGAnalysis) + + def _process_inst(self, dfg, inst, idx): + """ + Process store instruction. If the variable is only used by a load instruction, + forward the variable to the load instruction. + """ + var = inst.output + uses = dfg.get_uses(var) + + insertion_idx = idx + 1 + + for use_inst in uses: + if use_inst.parent != inst.parent: + continue + + for i, operand in enumerate(use_inst.operands): + if operand == var: + new_var = self.function.get_next_variable() + new_inst = IRInstruction("store", [var], new_var) + inst.parent.insert_instruction(new_inst, insertion_idx) + insertion_idx += 1 + use_inst.operands[i] = new_var + From ff700b49ccb121e3a51bd8c2c3f86c09d85fa4cd Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Fri, 31 May 2024 07:53:20 -0400 Subject: [PATCH 03/23] lint --- vyper/venom/__init__.py | 4 ++-- vyper/venom/passes/extract_literals.py | 1 + vyper/venom/passes/store_expansion.py | 3 +-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/vyper/venom/__init__.py b/vyper/venom/__init__.py index 2da94bf98d..9b1f67b195 100644 --- a/vyper/venom/__init__.py +++ b/vyper/venom/__init__.py @@ -12,14 +12,14 @@ from vyper.venom.passes.algebraic_optimization import AlgebraicOptimizationPass from vyper.venom.passes.branch_optimization import BranchOptimizationPass from vyper.venom.passes.dft import DFTPass -from vyper.venom.passes.make_ssa import MakeSSA from vyper.venom.passes.extract_literals import ExtractLiteralsPass -from vyper.venom.passes.store_expansion import StoreExpansionPass +from vyper.venom.passes.make_ssa import MakeSSA from vyper.venom.passes.mem2var import Mem2Var from vyper.venom.passes.remove_unused_variables import RemoveUnusedVariablesPass from vyper.venom.passes.sccp import SCCP from vyper.venom.passes.simplify_cfg import SimplifyCFGPass from vyper.venom.passes.store_elimination import StoreElimination +from vyper.venom.passes.store_expansion import StoreExpansionPass from vyper.venom.venom_to_assembly import VenomCompiler DEFAULT_OPT_LEVEL = OptimizationLevel.default() diff --git a/vyper/venom/passes/extract_literals.py b/vyper/venom/passes/extract_literals.py index 3ee54af3c9..f504481616 100644 --- a/vyper/venom/passes/extract_literals.py +++ b/vyper/venom/passes/extract_literals.py @@ -9,6 +9,7 @@ class ExtractLiteralsPass(IRPass): """ This pass extracts literals so that they can be reordered by the DFT pass """ + def run_pass(self): for bb in self.function.get_basic_blocks(): self._process_bb(bb) diff --git a/vyper/venom/passes/store_expansion.py b/vyper/venom/passes/store_expansion.py index 91911e3854..ff60e7c5ca 100644 --- a/vyper/venom/passes/store_expansion.py +++ b/vyper/venom/passes/store_expansion.py @@ -2,7 +2,7 @@ from vyper.venom.analysis.dfg import DFGAnalysis from vyper.venom.analysis.dominators import DominatorTreeAnalysis from vyper.venom.analysis.liveness import LivenessAnalysis -from vyper.venom.basicblock import IRVariable, IRInstruction +from vyper.venom.basicblock import IRInstruction, IRVariable from vyper.venom.passes.base_pass import IRPass @@ -47,4 +47,3 @@ def _process_inst(self, dfg, inst, idx): inst.parent.insert_instruction(new_inst, insertion_idx) insertion_idx += 1 use_inst.operands[i] = new_var - From ea9b1c519abe89ddb03accf0eaef7c5648e47f33 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Fri, 31 May 2024 09:57:37 -0400 Subject: [PATCH 04/23] remove inter-bb restriction --- vyper/venom/passes/store_expansion.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/vyper/venom/passes/store_expansion.py b/vyper/venom/passes/store_expansion.py index ff60e7c5ca..0f260fb224 100644 --- a/vyper/venom/passes/store_expansion.py +++ b/vyper/venom/passes/store_expansion.py @@ -38,7 +38,8 @@ def _process_inst(self, dfg, inst, idx): for use_inst in uses: if use_inst.parent != inst.parent: - continue + pass + #continue for i, operand in enumerate(use_inst.operands): if operand == var: From adbf01cf7549c08905d3120c2a5ebe5401192d18 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Fri, 31 May 2024 10:01:14 -0400 Subject: [PATCH 05/23] don't replace first use --- vyper/venom/passes/store_expansion.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vyper/venom/passes/store_expansion.py b/vyper/venom/passes/store_expansion.py index 0f260fb224..5c86a5518d 100644 --- a/vyper/venom/passes/store_expansion.py +++ b/vyper/venom/passes/store_expansion.py @@ -36,7 +36,7 @@ def _process_inst(self, dfg, inst, idx): insertion_idx = idx + 1 - for use_inst in uses: + for use_inst in uses[1:]: if use_inst.parent != inst.parent: pass #continue From aa2234c8e7981fbd05edeb508a091eeca68da49d Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Sat, 1 Jun 2024 19:31:44 -0400 Subject: [PATCH 06/23] fix bugs --- vyper/venom/passes/extract_literals.py | 1 - vyper/venom/passes/store_expansion.py | 11 +++++------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/vyper/venom/passes/extract_literals.py b/vyper/venom/passes/extract_literals.py index f504481616..b8e042b357 100644 --- a/vyper/venom/passes/extract_literals.py +++ b/vyper/venom/passes/extract_literals.py @@ -1,4 +1,3 @@ -from vyper.utils import OrderedSet from vyper.venom.analysis.dfg import DFGAnalysis from vyper.venom.analysis.liveness import LivenessAnalysis from vyper.venom.basicblock import IRInstruction, IRLiteral diff --git a/vyper/venom/passes/store_expansion.py b/vyper/venom/passes/store_expansion.py index 5c86a5518d..b857a66b9b 100644 --- a/vyper/venom/passes/store_expansion.py +++ b/vyper/venom/passes/store_expansion.py @@ -1,8 +1,7 @@ from vyper.venom.analysis.cfg import CFGAnalysis from vyper.venom.analysis.dfg import DFGAnalysis -from vyper.venom.analysis.dominators import DominatorTreeAnalysis from vyper.venom.analysis.liveness import LivenessAnalysis -from vyper.venom.basicblock import IRInstruction, IRVariable +from vyper.venom.basicblock import IRInstruction from vyper.venom.passes.base_pass import IRPass @@ -13,7 +12,6 @@ class StoreExpansionPass(IRPass): """ def run_pass(self): - self.analyses_cache.request_analysis(CFGAnalysis) dfg = self.analyses_cache.request_analysis(DFGAnalysis) for bb in self.function.get_basic_blocks(): @@ -38,13 +36,14 @@ def _process_inst(self, dfg, inst, idx): for use_inst in uses[1:]: if use_inst.parent != inst.parent: - pass - #continue + continue + prev = var for i, operand in enumerate(use_inst.operands): if operand == var: new_var = self.function.get_next_variable() - new_inst = IRInstruction("store", [var], new_var) + new_inst = IRInstruction("store", [prev], new_var) inst.parent.insert_instruction(new_inst, insertion_idx) insertion_idx += 1 use_inst.operands[i] = new_var + prev = new_var From b6b7aeda2b5d00f2a09c2337df8dfa4d6b4018e3 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Sat, 1 Jun 2024 19:33:14 -0400 Subject: [PATCH 07/23] allow inter-bb --- vyper/venom/passes/store_expansion.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/vyper/venom/passes/store_expansion.py b/vyper/venom/passes/store_expansion.py index b857a66b9b..99d011edb3 100644 --- a/vyper/venom/passes/store_expansion.py +++ b/vyper/venom/passes/store_expansion.py @@ -36,7 +36,8 @@ def _process_inst(self, dfg, inst, idx): for use_inst in uses[1:]: if use_inst.parent != inst.parent: - continue + #continue # improves codesize + pass prev = var for i, operand in enumerate(use_inst.operands): From a71cad8977b1b94960d0123a43708b6c069f4b00 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Sat, 1 Jun 2024 19:38:54 -0400 Subject: [PATCH 08/23] lint --- vyper/venom/passes/store_expansion.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/vyper/venom/passes/store_expansion.py b/vyper/venom/passes/store_expansion.py index 99d011edb3..37145464ec 100644 --- a/vyper/venom/passes/store_expansion.py +++ b/vyper/venom/passes/store_expansion.py @@ -1,4 +1,3 @@ -from vyper.venom.analysis.cfg import CFGAnalysis from vyper.venom.analysis.dfg import DFGAnalysis from vyper.venom.analysis.liveness import LivenessAnalysis from vyper.venom.basicblock import IRInstruction @@ -36,7 +35,7 @@ def _process_inst(self, dfg, inst, idx): for use_inst in uses[1:]: if use_inst.parent != inst.parent: - #continue # improves codesize + # continue # improves codesize pass prev = var From 163979ba2a75c22539d63991454ad1e463515a42 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Tue, 4 Jun 2024 15:39:56 -0400 Subject: [PATCH 09/23] fix a bug --- vyper/venom/passes/store_expansion.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/vyper/venom/passes/store_expansion.py b/vyper/venom/passes/store_expansion.py index 37145464ec..e3373d0119 100644 --- a/vyper/venom/passes/store_expansion.py +++ b/vyper/venom/passes/store_expansion.py @@ -15,7 +15,7 @@ def run_pass(self): for bb in self.function.get_basic_blocks(): for idx, inst in enumerate(bb.instructions): - if inst.opcode != "store": + if inst.output is None: continue self._process_inst(dfg, inst, idx) @@ -35,8 +35,7 @@ def _process_inst(self, dfg, inst, idx): for use_inst in uses[1:]: if use_inst.parent != inst.parent: - # continue # improves codesize - pass + continue # improves codesize prev = var for i, operand in enumerate(use_inst.operands): From 61ea5776c2ecb298ef9758573455ebdeb1ebb6d9 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Tue, 4 Jun 2024 22:04:42 -0400 Subject: [PATCH 10/23] fix algorithm a bit --- vyper/venom/__init__.py | 2 +- vyper/venom/passes/store_expansion.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/vyper/venom/__init__.py b/vyper/venom/__init__.py index 9b1f67b195..801710d8ab 100644 --- a/vyper/venom/__init__.py +++ b/vyper/venom/__init__.py @@ -55,8 +55,8 @@ def _run_passes(fn: IRFunction, optimize: OptimizationLevel) -> None: SimplifyCFGPass(ac, fn).run_pass() AlgebraicOptimizationPass(ac, fn).run_pass() BranchOptimizationPass(ac, fn).run_pass() - ExtractLiteralsPass(ac, fn).run_pass() StoreExpansionPass(ac, fn).run_pass() + ExtractLiteralsPass(ac, fn).run_pass() RemoveUnusedVariablesPass(ac, fn).run_pass() DFTPass(ac, fn).run_pass() diff --git a/vyper/venom/passes/store_expansion.py b/vyper/venom/passes/store_expansion.py index e3373d0119..0aa8fc8ff5 100644 --- a/vyper/venom/passes/store_expansion.py +++ b/vyper/venom/passes/store_expansion.py @@ -33,15 +33,15 @@ def _process_inst(self, dfg, inst, idx): insertion_idx = idx + 1 - for use_inst in uses[1:]: + prev = var + for use_inst in uses[:-1]: if use_inst.parent != inst.parent: continue # improves codesize - prev = var for i, operand in enumerate(use_inst.operands): if operand == var: new_var = self.function.get_next_variable() - new_inst = IRInstruction("store", [prev], new_var) + new_inst = IRInstruction("store", [var], new_var) inst.parent.insert_instruction(new_inst, insertion_idx) insertion_idx += 1 use_inst.operands[i] = new_var From 669c170c9491e526652fc165a4adda05426355d1 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Wed, 25 Sep 2024 11:02:27 -0400 Subject: [PATCH 11/23] fix store expansion - expand close to the use site, not the production site --- vyper/venom/passes/store_expansion.py | 48 +++++++++++---------------- 1 file changed, 19 insertions(+), 29 deletions(-) diff --git a/vyper/venom/passes/store_expansion.py b/vyper/venom/passes/store_expansion.py index 0aa8fc8ff5..c856a09e1c 100644 --- a/vyper/venom/passes/store_expansion.py +++ b/vyper/venom/passes/store_expansion.py @@ -1,6 +1,6 @@ from vyper.venom.analysis.dfg import DFGAnalysis from vyper.venom.analysis.liveness import LivenessAnalysis -from vyper.venom.basicblock import IRInstruction +from vyper.venom.basicblock import IRInstruction, IRVariable from vyper.venom.passes.base_pass import IRPass @@ -14,35 +14,25 @@ def run_pass(self): dfg = self.analyses_cache.request_analysis(DFGAnalysis) for bb in self.function.get_basic_blocks(): - for idx, inst in enumerate(bb.instructions): - if inst.output is None: - continue - - self._process_inst(dfg, inst, idx) + self._process_bb(bb) self.analyses_cache.invalidate_analysis(LivenessAnalysis) self.analyses_cache.invalidate_analysis(DFGAnalysis) - def _process_inst(self, dfg, inst, idx): - """ - Process store instruction. If the variable is only used by a load instruction, - forward the variable to the load instruction. - """ - var = inst.output - uses = dfg.get_uses(var) - - insertion_idx = idx + 1 - - prev = var - for use_inst in uses[:-1]: - if use_inst.parent != inst.parent: - continue # improves codesize - - for i, operand in enumerate(use_inst.operands): - if operand == var: - new_var = self.function.get_next_variable() - new_inst = IRInstruction("store", [var], new_var) - inst.parent.insert_instruction(new_inst, insertion_idx) - insertion_idx += 1 - use_inst.operands[i] = new_var - prev = new_var + def _process_bb(self, bb): + i = 0 + while i < len(bb.instructions): + inst = bb.instructions[i] + if inst.opcode in ("store", "offset", "phi"): + i += 1 + continue + + index = i + for j, op in enumerate(inst.operands): + if isinstance(op, IRVariable): + var = self.function.get_next_variable() + to_insert = IRInstruction("store", [op], var) + bb.insert_instruction(to_insert, index=index) + i += 1 + inst.operands[j] = var + i += 1 From 248370d6c99c493390bfd4ec226698a47d47e418 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Wed, 25 Sep 2024 12:03:24 -0400 Subject: [PATCH 12/23] fix lint --- vyper/venom/passes/store_expansion.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/vyper/venom/passes/store_expansion.py b/vyper/venom/passes/store_expansion.py index c856a09e1c..4b3eac84d7 100644 --- a/vyper/venom/passes/store_expansion.py +++ b/vyper/venom/passes/store_expansion.py @@ -11,8 +11,6 @@ class StoreExpansionPass(IRPass): """ def run_pass(self): - dfg = self.analyses_cache.request_analysis(DFGAnalysis) - for bb in self.function.get_basic_blocks(): self._process_bb(bb) @@ -23,7 +21,7 @@ def _process_bb(self, bb): i = 0 while i < len(bb.instructions): inst = bb.instructions[i] - if inst.opcode in ("store", "offset", "phi"): + if inst.opcode in ("store", "offset", "phi", "param"): i += 1 continue From 7313acf469fe83869fb6ebe6702f3e05f46173ed Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Wed, 25 Sep 2024 19:39:24 -0400 Subject: [PATCH 13/23] fuse store expansion and literal extraction passes they do pretty much the same things --- vyper/venom/__init__.py | 3 +-- vyper/venom/passes/extract_literals.py | 8 +++--- vyper/venom/passes/store_expansion.py | 36 -------------------------- 3 files changed, 6 insertions(+), 41 deletions(-) delete mode 100644 vyper/venom/passes/store_expansion.py diff --git a/vyper/venom/__init__.py b/vyper/venom/__init__.py index 801710d8ab..c96ec95207 100644 --- a/vyper/venom/__init__.py +++ b/vyper/venom/__init__.py @@ -19,7 +19,6 @@ from vyper.venom.passes.sccp import SCCP from vyper.venom.passes.simplify_cfg import SimplifyCFGPass from vyper.venom.passes.store_elimination import StoreElimination -from vyper.venom.passes.store_expansion import StoreExpansionPass from vyper.venom.venom_to_assembly import VenomCompiler DEFAULT_OPT_LEVEL = OptimizationLevel.default() @@ -55,7 +54,7 @@ def _run_passes(fn: IRFunction, optimize: OptimizationLevel) -> None: SimplifyCFGPass(ac, fn).run_pass() AlgebraicOptimizationPass(ac, fn).run_pass() BranchOptimizationPass(ac, fn).run_pass() - StoreExpansionPass(ac, fn).run_pass() + ExtractLiteralsPass(ac, fn).run_pass() RemoveUnusedVariablesPass(ac, fn).run_pass() DFTPass(ac, fn).run_pass() diff --git a/vyper/venom/passes/extract_literals.py b/vyper/venom/passes/extract_literals.py index 91c0813e67..372a7c6748 100644 --- a/vyper/venom/passes/extract_literals.py +++ b/vyper/venom/passes/extract_literals.py @@ -1,6 +1,6 @@ from vyper.venom.analysis.dfg import DFGAnalysis from vyper.venom.analysis.liveness import LivenessAnalysis -from vyper.venom.basicblock import IRInstruction, IRLiteral +from vyper.venom.basicblock import IRInstruction, IRLiteral, IRVariable from vyper.venom.passes.base_pass import IRPass @@ -20,7 +20,7 @@ def _process_bb(self, bb): i = 0 while i < len(bb.instructions): inst = bb.instructions[i] - if inst.opcode in ("store", "offset"): + if inst.opcode in ("store", "offset", "phi", "param"): i += 1 continue @@ -29,9 +29,11 @@ def _process_bb(self, bb): if inst.opcode == "log" and j == 0: continue - if isinstance(op, IRLiteral): + if isinstance(op, (IRVariable, IRLiteral)): var = self.function.get_next_variable() to_insert = IRInstruction("store", [op], var) bb.insert_instruction(to_insert, index=i) inst.operands[j] = var + i += 1 + i += 1 diff --git a/vyper/venom/passes/store_expansion.py b/vyper/venom/passes/store_expansion.py deleted file mode 100644 index 4b3eac84d7..0000000000 --- a/vyper/venom/passes/store_expansion.py +++ /dev/null @@ -1,36 +0,0 @@ -from vyper.venom.analysis.dfg import DFGAnalysis -from vyper.venom.analysis.liveness import LivenessAnalysis -from vyper.venom.basicblock import IRInstruction, IRVariable -from vyper.venom.passes.base_pass import IRPass - - -class StoreExpansionPass(IRPass): - """ - This pass expands variables to their uses though `store` instructions, - reducing pressure on the stack scheduler - """ - - def run_pass(self): - for bb in self.function.get_basic_blocks(): - self._process_bb(bb) - - self.analyses_cache.invalidate_analysis(LivenessAnalysis) - self.analyses_cache.invalidate_analysis(DFGAnalysis) - - def _process_bb(self, bb): - i = 0 - while i < len(bb.instructions): - inst = bb.instructions[i] - if inst.opcode in ("store", "offset", "phi", "param"): - i += 1 - continue - - index = i - for j, op in enumerate(inst.operands): - if isinstance(op, IRVariable): - var = self.function.get_next_variable() - to_insert = IRInstruction("store", [op], var) - bb.insert_instruction(to_insert, index=index) - i += 1 - inst.operands[j] = var - i += 1 From fe215d9741ba5352aed72338d08c1078276469bb Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Wed, 25 Sep 2024 19:43:49 -0400 Subject: [PATCH 14/23] cleanup - rename extract literals to store_expansion --- vyper/venom/__init__.py | 4 ++-- .../venom/passes/{extract_literals.py => store_expansion.py} | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) rename vyper/venom/passes/{extract_literals.py => store_expansion.py} (90%) diff --git a/vyper/venom/__init__.py b/vyper/venom/__init__.py index c96ec95207..4c49b18088 100644 --- a/vyper/venom/__init__.py +++ b/vyper/venom/__init__.py @@ -12,13 +12,13 @@ from vyper.venom.passes.algebraic_optimization import AlgebraicOptimizationPass from vyper.venom.passes.branch_optimization import BranchOptimizationPass from vyper.venom.passes.dft import DFTPass -from vyper.venom.passes.extract_literals import ExtractLiteralsPass from vyper.venom.passes.make_ssa import MakeSSA from vyper.venom.passes.mem2var import Mem2Var from vyper.venom.passes.remove_unused_variables import RemoveUnusedVariablesPass from vyper.venom.passes.sccp import SCCP from vyper.venom.passes.simplify_cfg import SimplifyCFGPass from vyper.venom.passes.store_elimination import StoreElimination +from vyper.venom.passes.store_expansion import StoreExpansionPass from vyper.venom.venom_to_assembly import VenomCompiler DEFAULT_OPT_LEVEL = OptimizationLevel.default() @@ -55,7 +55,7 @@ def _run_passes(fn: IRFunction, optimize: OptimizationLevel) -> None: AlgebraicOptimizationPass(ac, fn).run_pass() BranchOptimizationPass(ac, fn).run_pass() - ExtractLiteralsPass(ac, fn).run_pass() + StoreExpansionPass(ac, fn).run_pass() RemoveUnusedVariablesPass(ac, fn).run_pass() DFTPass(ac, fn).run_pass() diff --git a/vyper/venom/passes/extract_literals.py b/vyper/venom/passes/store_expansion.py similarity index 90% rename from vyper/venom/passes/extract_literals.py rename to vyper/venom/passes/store_expansion.py index 372a7c6748..7718e67d33 100644 --- a/vyper/venom/passes/extract_literals.py +++ b/vyper/venom/passes/store_expansion.py @@ -4,9 +4,10 @@ from vyper.venom.passes.base_pass import IRPass -class ExtractLiteralsPass(IRPass): +class StoreExpansionPass(IRPass): """ - This pass extracts literals so that they can be reordered by the DFT pass + This pass extracts literals and variables so that they can be + reordered by the DFT pass """ def run_pass(self): From a22f5c92eaa6d3d2ce33d3d4f1252241c18baa0c Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Wed, 25 Sep 2024 19:45:42 -0400 Subject: [PATCH 15/23] reorder store expansion and unused var elimination --- vyper/venom/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vyper/venom/__init__.py b/vyper/venom/__init__.py index 4c49b18088..a5f51b787d 100644 --- a/vyper/venom/__init__.py +++ b/vyper/venom/__init__.py @@ -54,9 +54,9 @@ def _run_passes(fn: IRFunction, optimize: OptimizationLevel) -> None: SimplifyCFGPass(ac, fn).run_pass() AlgebraicOptimizationPass(ac, fn).run_pass() BranchOptimizationPass(ac, fn).run_pass() + RemoveUnusedVariablesPass(ac, fn).run_pass() StoreExpansionPass(ac, fn).run_pass() - RemoveUnusedVariablesPass(ac, fn).run_pass() DFTPass(ac, fn).run_pass() From f843eca45ca5999407a2defe7f3685a4006609a1 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Sat, 28 Sep 2024 11:13:20 -0400 Subject: [PATCH 16/23] simplify emit_input_operands we no longer need to keep track of emitted ops --- vyper/venom/venom_to_assembly.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/vyper/venom/venom_to_assembly.py b/vyper/venom/venom_to_assembly.py index 390fab8e7c..eeef9affd9 100644 --- a/vyper/venom/venom_to_assembly.py +++ b/vyper/venom/venom_to_assembly.py @@ -248,11 +248,14 @@ def _emit_input_operands( self.swap_op(assembly, stack, op) break - emitted_ops = OrderedSet[IROperand]() + # to validate store expansion invariant - + # each op is emitted at most once. + seen = set() + for op in ops: if isinstance(op, IRLabel): - # invoke emits the actual instruction itself so we don't need to emit it here - # but we need to add it to the stack map + # invoke emits the actual instruction itself so we don't need + # to emit it here but we need to add it to the stack map if inst.opcode != "invoke": assembly.append(f"_sym_{op.value}") stack.push(op) @@ -267,13 +270,12 @@ def _emit_input_operands( stack.push(op) continue - if op in next_liveness and op not in emitted_ops: - self.dup_op(assembly, stack, op) - - if op in emitted_ops: + if op in next_liveness: self.dup_op(assembly, stack, op) - emitted_ops.add(op) + # guaranteed by store expansion + assert op not in seen, (op, seen) + seen.add(op) def _generate_evm_for_basicblock_r( self, asm: list, basicblock: IRBasicBlock, stack: StackModel From 1bf017381b671f37387dbabb06c5773851c8addb Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Sat, 28 Sep 2024 11:15:14 -0400 Subject: [PATCH 17/23] remove a heuristic it no longer changes bytecode --- vyper/venom/venom_to_assembly.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/vyper/venom/venom_to_assembly.py b/vyper/venom/venom_to_assembly.py index eeef9affd9..749bdd5395 100644 --- a/vyper/venom/venom_to_assembly.py +++ b/vyper/venom/venom_to_assembly.py @@ -240,14 +240,6 @@ def _emit_input_operands( # been scheduled to be killed. now it's just a matter of emitting # SWAPs, DUPs and PUSHes until we match the `ops` argument - # dumb heuristic: if the top of stack is not wanted here, swap - # it with something that is wanted - if ops and stack.height > 0 and stack.peek(0) not in ops: - for op in ops: - if isinstance(op, IRVariable) and op not in next_liveness: - self.swap_op(assembly, stack, op) - break - # to validate store expansion invariant - # each op is emitted at most once. seen = set() From 54d7e9752c836143e657448c15fce29d82e58763 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Fri, 20 Sep 2024 21:20:21 -0400 Subject: [PATCH 18/23] add equivalence analysis use equivalence analysis to reduce swaps --- vyper/venom/analysis/equivalent_vars.py | 37 ++++++++++++++++++ vyper/venom/venom_to_assembly.py | 51 ++++++++++++++++++++++--- 2 files changed, 82 insertions(+), 6 deletions(-) create mode 100644 vyper/venom/analysis/equivalent_vars.py diff --git a/vyper/venom/analysis/equivalent_vars.py b/vyper/venom/analysis/equivalent_vars.py new file mode 100644 index 0000000000..e820fcfe47 --- /dev/null +++ b/vyper/venom/analysis/equivalent_vars.py @@ -0,0 +1,37 @@ +from vyper.utils import OrderedSet +from vyper.venom.analysis.analysis import IRAnalysis +from vyper.venom.basicblock import IRVariable +from vyper.venom.analysis.dfg import DFGAnalysis + + +class VarEquivalenceAnalysis(IRAnalysis): + """ + Generate equivalence sets of variables + """ + def analyze(self): + dfg = self.analyses_cache.request_analysis(DFGAnalysis) + + equivalence_set: dict[IRVariable, int] = {} + + for bag, (var, inst) in enumerate(dfg._dfg_outputs.items()): + if inst.opcode != "store": + continue + + source = inst.operands[0] + + if source in equivalence_set: + equivalence_set[var] = equivalence_set[source] + continue + else: + assert var not in equivalence_set + equivalence_set[var] = bag + equivalence_set[source] = bag + + self._equivalence_set = equivalence_set + + def equivalent(self, var1, var2): + if var1 not in self._equivalence_set: + return False + if var2 not in self._equivalence_set: + return False + return self._equivalence_set[var1] == self._equivalence_set[var2] diff --git a/vyper/venom/venom_to_assembly.py b/vyper/venom/venom_to_assembly.py index 749bdd5395..934c10d1e3 100644 --- a/vyper/venom/venom_to_assembly.py +++ b/vyper/venom/venom_to_assembly.py @@ -13,6 +13,7 @@ from vyper.utils import MemoryPositions, OrderedSet from vyper.venom.analysis.analysis import IRAnalysesCache from vyper.venom.analysis.liveness import LivenessAnalysis +from vyper.venom.analysis.equivalent_vars import VarEquivalenceAnalysis from vyper.venom.basicblock import ( IRBasicBlock, IRInstruction, @@ -25,6 +26,10 @@ from vyper.venom.passes.normalization import NormalizationPass from vyper.venom.stack_model import StackModel +DEBUG_SHOW_COST = True +if DEBUG_SHOW_COST: + import sys + # instructions which map one-to-one from venom to EVM _ONE_TO_ONE_INSTRUCTIONS = frozenset( [ @@ -152,6 +157,7 @@ def generate_evm(self, no_optimize: bool = False) -> list[str]: NormalizationPass(ac, fn).run_pass() self.liveness_analysis = ac.request_analysis(LivenessAnalysis) + self.equivalence = ac.request_analysis(VarEquivalenceAnalysis) assert fn.normalized, "Non-normalized CFG!" @@ -220,7 +226,10 @@ def _stack_reorder( if depth == final_stack_depth: continue - if op == stack.peek(final_stack_depth): + to_swap = stack.peek(final_stack_depth) + if self.equivalence.equivalent(op, to_swap): + stack.poke(final_stack_depth, op) + stack.poke(depth, to_swap) continue cost += self.swap(assembly, stack, depth) @@ -276,6 +285,12 @@ def _generate_evm_for_basicblock_r( return self.visited_basicblocks.add(basicblock) + if DEBUG_SHOW_COST: + print(basicblock, file=sys.stderr) + + ref = asm + asm = [] + # assembly entry point into the block asm.append(f"_sym_{basicblock.label}") asm.append("JUMPDEST") @@ -291,8 +306,14 @@ def _generate_evm_for_basicblock_r( asm.extend(self._generate_evm_for_instruction(inst, stack, next_liveness)) + if DEBUG_SHOW_COST: + print(" ".join(map(str, asm)), file=sys.stderr) + print("\n", file=sys.stderr) + + ref.extend(asm) + for bb in basicblock.reachable: - self._generate_evm_for_basicblock_r(asm, bb, stack.copy()) + self._generate_evm_for_basicblock_r(ref, bb, stack.copy()) # pop values from stack at entry to bb # note this produces the same result(!) no matter which basic block @@ -415,6 +436,13 @@ def _generate_evm_for_instruction( if cost_with_swap > cost_no_swap: operands[-1], operands[-2] = operands[-2], operands[-1] + cost = self._stack_reorder([], stack, operands, dry_run=True) + if DEBUG_SHOW_COST and cost: + print("ENTER", inst, file=sys.stderr) + print(" HAVE", stack, file=sys.stderr) + print(" WANT", operands, file=sys.stderr) + print(" COST", cost, file=sys.stderr) + # final step to get the inputs to this instruction ordered # correctly on the stack self._stack_reorder(assembly, stack, operands) @@ -531,10 +559,21 @@ def _generate_evm_for_instruction( if inst.output not in next_liveness: self.pop(assembly, stack) else: - # peek at next_liveness to find the next scheduled item, - # and optimistically swap with it + # heuristic: peek at next_liveness to find the next scheduled + # item, and optimistically swap with it + if DEBUG_SHOW_COST: + stack0 = stack.copy() + next_scheduled = next_liveness.last() - self.swap_op(assembly, stack, next_scheduled) + cost = 0 + if not self.equivalence.equivalent(inst.output, next_scheduled): + cost = self.swap_op(assembly, stack, next_scheduled) + + if DEBUG_SHOW_COST and cost != 0: + print("ENTER", inst, file=sys.stderr) + print(" HAVE", stack0, file=sys.stderr) + print(" NEXT LIVENESS", next_liveness, file=sys.stderr) + print(" NEW_STACK", stack, file=sys.stderr) return apply_line_numbers(inst, assembly) @@ -556,7 +595,7 @@ def dup(self, assembly, stack, depth): assembly.append(_evm_dup_for(depth)) def swap_op(self, assembly, stack, op): - self.swap(assembly, stack, stack.get_depth(op)) + return self.swap(assembly, stack, stack.get_depth(op)) def dup_op(self, assembly, stack, op): self.dup(assembly, stack, stack.get_depth(op)) From 42785ed4446280132a69b0365e33370855d6c15d Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Sat, 28 Sep 2024 19:02:40 -0400 Subject: [PATCH 19/23] fix lint --- vyper/venom/analysis/equivalent_vars.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vyper/venom/analysis/equivalent_vars.py b/vyper/venom/analysis/equivalent_vars.py index e820fcfe47..09cf88c1ea 100644 --- a/vyper/venom/analysis/equivalent_vars.py +++ b/vyper/venom/analysis/equivalent_vars.py @@ -1,13 +1,13 @@ -from vyper.utils import OrderedSet from vyper.venom.analysis.analysis import IRAnalysis -from vyper.venom.basicblock import IRVariable from vyper.venom.analysis.dfg import DFGAnalysis +from vyper.venom.basicblock import IRVariable class VarEquivalenceAnalysis(IRAnalysis): """ Generate equivalence sets of variables """ + def analyze(self): dfg = self.analyses_cache.request_analysis(DFGAnalysis) From a367b1d23c2589dcd0b18a55e78199819cf528a9 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Sat, 28 Sep 2024 19:04:41 -0400 Subject: [PATCH 20/23] fix lint, tests --- tests/unit/compiler/venom/test_duplicate_operands.py | 12 +++++++++--- tests/unit/compiler/venom/test_stack_cleanup.py | 3 ++- vyper/venom/venom_to_assembly.py | 6 +++--- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/tests/unit/compiler/venom/test_duplicate_operands.py b/tests/unit/compiler/venom/test_duplicate_operands.py index fbff0835d2..ab55649dae 100644 --- a/tests/unit/compiler/venom/test_duplicate_operands.py +++ b/tests/unit/compiler/venom/test_duplicate_operands.py @@ -1,6 +1,8 @@ from vyper.compiler.settings import OptimizationLevel from vyper.venom import generate_assembly_experimental +from vyper.venom.analysis.analysis import IRAnalysesCache from vyper.venom.context import IRContext +from vyper.venom.passes.store_expansion import StoreExpansionPass def test_duplicate_operands(): @@ -13,7 +15,7 @@ def test_duplicate_operands(): %3 = mul %1, %2 stop - Should compile to: [PUSH1, 10, DUP1, DUP1, DUP1, ADD, MUL, POP, STOP] + Should compile to: [PUSH1, 10, DUP1, DUP2, ADD, MUL, POP, STOP] """ ctx = IRContext() fn = ctx.create_function("test") @@ -23,5 +25,9 @@ def test_duplicate_operands(): bb.append_instruction("mul", sum_, op) bb.append_instruction("stop") - asm = generate_assembly_experimental(ctx, optimize=OptimizationLevel.GAS) - assert asm == ["PUSH1", 10, "DUP1", "DUP1", "ADD", "MUL", "POP", "STOP"] + ac = IRAnalysesCache(fn) + StoreExpansionPass(ac, fn).run_pass() + + optimize = OptimizationLevel.GAS + asm = generate_assembly_experimental(ctx, optimize=optimize) + assert asm == ["PUSH1", 10, "DUP1", "DUP2", "ADD", "MUL", "POP", "STOP"] diff --git a/tests/unit/compiler/venom/test_stack_cleanup.py b/tests/unit/compiler/venom/test_stack_cleanup.py index 6015cf1c41..7198861771 100644 --- a/tests/unit/compiler/venom/test_stack_cleanup.py +++ b/tests/unit/compiler/venom/test_stack_cleanup.py @@ -9,7 +9,8 @@ def test_cleanup_stack(): bb = fn.get_basic_block() ret_val = bb.append_instruction("param") op = bb.append_instruction("store", 10) - bb.append_instruction("add", op, op) + op2 = bb.append_instruction("store", op) + bb.append_instruction("add", op, op2) bb.append_instruction("ret", ret_val) asm = generate_assembly_experimental(ctx, optimize=OptimizationLevel.GAS) diff --git a/vyper/venom/venom_to_assembly.py b/vyper/venom/venom_to_assembly.py index 934c10d1e3..d024cd8475 100644 --- a/vyper/venom/venom_to_assembly.py +++ b/vyper/venom/venom_to_assembly.py @@ -12,8 +12,8 @@ ) from vyper.utils import MemoryPositions, OrderedSet from vyper.venom.analysis.analysis import IRAnalysesCache -from vyper.venom.analysis.liveness import LivenessAnalysis from vyper.venom.analysis.equivalent_vars import VarEquivalenceAnalysis +from vyper.venom.analysis.liveness import LivenessAnalysis from vyper.venom.basicblock import ( IRBasicBlock, IRInstruction, @@ -26,7 +26,7 @@ from vyper.venom.passes.normalization import NormalizationPass from vyper.venom.stack_model import StackModel -DEBUG_SHOW_COST = True +DEBUG_SHOW_COST = False if DEBUG_SHOW_COST: import sys @@ -251,7 +251,7 @@ def _emit_input_operands( # to validate store expansion invariant - # each op is emitted at most once. - seen = set() + seen: set[IROperand] = set() for op in ops: if isinstance(op, IRLabel): From b8ae0d1f2b72905fed1f360a6a82c44272f711a1 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Sat, 28 Sep 2024 20:11:29 -0400 Subject: [PATCH 21/23] add a note --- vyper/venom/venom_to_assembly.py | 1 + 1 file changed, 1 insertion(+) diff --git a/vyper/venom/venom_to_assembly.py b/vyper/venom/venom_to_assembly.py index d024cd8475..5bd6366bf6 100644 --- a/vyper/venom/venom_to_assembly.py +++ b/vyper/venom/venom_to_assembly.py @@ -228,6 +228,7 @@ def _stack_reorder( to_swap = stack.peek(final_stack_depth) if self.equivalence.equivalent(op, to_swap): + # perform a "virtual" swap stack.poke(final_stack_depth, op) stack.poke(depth, to_swap) continue From df469c6510712ee34b5cad583e63b323fd434d98 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Sat, 28 Sep 2024 20:25:44 -0400 Subject: [PATCH 22/23] strength an assertion --- vyper/venom/analysis/equivalent_vars.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vyper/venom/analysis/equivalent_vars.py b/vyper/venom/analysis/equivalent_vars.py index 09cf88c1ea..b15a850557 100644 --- a/vyper/venom/analysis/equivalent_vars.py +++ b/vyper/venom/analysis/equivalent_vars.py @@ -19,11 +19,11 @@ def analyze(self): source = inst.operands[0] + assert var not in equivalence_set # invariant if source in equivalence_set: equivalence_set[var] = equivalence_set[source] continue else: - assert var not in equivalence_set equivalence_set[var] = bag equivalence_set[source] = bag From 2ae3ea9f15c6755b13669cceca55fad8f7c5aada Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Sat, 28 Sep 2024 20:39:22 -0400 Subject: [PATCH 23/23] add some comments --- vyper/venom/analysis/equivalent_vars.py | 6 +++++- vyper/venom/venom_to_assembly.py | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/vyper/venom/analysis/equivalent_vars.py b/vyper/venom/analysis/equivalent_vars.py index b15a850557..9b0c03e3d1 100644 --- a/vyper/venom/analysis/equivalent_vars.py +++ b/vyper/venom/analysis/equivalent_vars.py @@ -5,7 +5,11 @@ class VarEquivalenceAnalysis(IRAnalysis): """ - Generate equivalence sets of variables + Generate equivalence sets of variables. This is used to avoid swapping + variables which are the same during venom_to_assembly. Theoretically, + the DFTPass should order variable declarations optimally, but, it is + not aware of the "pickaxe" heuristic in venom_to_assembly, so they can + interfere. """ def analyze(self): diff --git a/vyper/venom/venom_to_assembly.py b/vyper/venom/venom_to_assembly.py index 5bd6366bf6..45b307d7b3 100644 --- a/vyper/venom/venom_to_assembly.py +++ b/vyper/venom/venom_to_assembly.py @@ -396,6 +396,7 @@ def _generate_evm_for_instruction( # find an instance of %13 *or* %14 in the stack and replace it with %56. to_be_replaced = stack.peek(depth) if to_be_replaced in next_liveness: + # this branch seems unreachable (maybe due to make_ssa) # %13/%14 is still live(!), so we make a copy of it self.dup(assembly, stack, depth) stack.poke(0, ret)