Browse Source

Implement SSA form construction

jonathanvdc 8 years ago
parent
commit
59076f6d8a
2 changed files with 249 additions and 0 deletions
  1. 13 0
      kernel/modelverse_jit/cfg_ir.py
  2. 236 0
      kernel/modelverse_jit/cfg_ssa_construction.py

+ 13 - 0
kernel/modelverse_jit/cfg_ir.py

@@ -720,3 +720,16 @@ def get_all_blocks(entry_point):
     yield entry_point
     yield entry_point
     for block in get_reachable_blocks(entry_point):
     for block in get_reachable_blocks(entry_point):
         yield block
         yield block
+
+def get_trivial_phi_value(parameter_def, values):
+    """Tests if the given parameter definition is an alias for another definition.
+       If so, then the other definition is returned; otherwise, None."""
+    result = None
+    for elem in values:
+        if elem is not parameter_def:
+            if result is None:
+                result = elem
+            else:
+                return None
+
+    return result

+ 236 - 0
kernel/modelverse_jit/cfg_ssa_construction.py

@@ -0,0 +1,236 @@
+"""Converts 'declare-local', 'load' and 'store' instructions into SSA form."""
+
+from collections import defaultdict
+import modelverse_jit.cfg_ir as cfg_ir
+
+def get_local_id(def_or_value):
+    """Gets the node of the local resolved or declared by the given definition or value.
+       If the given definition or value does not refer to a 'resolve-local' or
+       'declare-local' node, then None is returned."""
+    value = cfg_ir.get_def_value(def_or_value)
+    if isinstance(value, (cfg_ir.ResolveLocal, cfg_ir.DeclareLocal)):
+        return value.variable.node_id
+    else:
+        return None
+
+def get_ineligible_local_ids(entry_point):
+    """Finds the ids of all local variables which are not eligible for conversion to SSA form."""
+    # Local variables are eligible for conversion to SSA form if their pointer node is never
+    # leaked to the outside world. So we know that we can safely convert a local to SSA form
+    # if 'resolve-local' values are only used by 'load' and 'store' values.
+    ineligible_local_ids = set()
+    def __maybe_mark_ineligible(def_or_value):
+        local_id = get_local_id(def_or_value)
+        if local_id is not None:
+            ineligible_local_ids.add(value.variable.node_id)
+
+    for block in cfg_ir.get_all_blocks(entry_point):
+        for definition in block.definitions + [block.flow]:
+            value = cfg_ir.get_def_value(definition)
+            if isinstance(value, cfg_ir.LoadPointer):
+                # Loading a pointer to a local is fine.
+                pass
+            elif isinstance(value, cfg_ir.StoreAtPointer):
+                # Storing a value in a local is fine, too.
+                # But be careful not to ignore a store where the stored value is a local pointer.
+                __maybe_mark_ineligible(value.value)
+            else:
+                # Walk over the dependencies, and mark them all as ineligible for
+                # local-to-SSA conversion.
+                for dependency in value.get_all_dependencies():
+                    __maybe_mark_ineligible(dependency)
+
+    return ineligible_local_ids
+
+def construct_ssa_form(entry_point):
+    """Converts local variables into SSA form in the graph defined by the given entry point."""
+    # Build some helper data structures.
+    all_blocks = cfg_ir.get_all_blocks(entry_point)
+    ineligible_locals = get_ineligible_local_ids(entry_point)
+    predecessor_map = cfg_ir.get_all_predecessor_blocks(entry_point)
+
+    # Create the SSA construction state.
+    state = SSAConstructionState(all_blocks, ineligible_locals, predecessor_map)
+
+    # Fill all blocks in the graph.
+    for block in all_blocks:
+        state.fill_block(block)
+
+    # Update branches.
+    for block in all_blocks:
+        state.update_block_branches(block)
+
+# The algorithms below are based on
+# Simple and Efficient Construction of Static Single Assignment Form by M. Braun et al
+# (https://pp.info.uni-karlsruhe.de/uploads/publikationen/braun13cc.pdf).
+
+class SSAConstructionState(object):
+    """Encapsulates state related to SSA construction."""
+    def __init__(self, all_blocks, ineligible_locals, predecessor_map):
+        self.all_blocks = all_blocks
+        self.ineligible_locals = ineligible_locals
+        self.predecessor_map = predecessor_map
+        # `current_defs` is a local node id -> basic block -> definition map.
+        self.current_defs = defaultdict(dict)
+        # `incomplete_phis` is a basic block -> local node id -> block parameter def map.
+        self.incomplete_phis = defaultdict(dict)
+        # `extra_phi_operands` is a basic block -> block parameter def -> def map.
+        self.extra_phi_operands = defaultdict(dict)
+        self.processed_blocks = set()
+        self.filled_blocks = set()
+        self.sealed_blocks = set()
+
+    def read_variable(self, block, node_id):
+        """Reads the latest definition of the local variable with the
+           given node id for the specified block."""
+        if block in self.current_defs[node_id]:
+            return self.current_defs[node_id][block]
+        else:
+            return self.read_variable_recursive(block, node_id)
+
+    def write_variable(self, block, node_id, value):
+        """Writes the given value to the local with the specified id in the
+           specified block."""
+        self.current_defs[node_id][block] = value
+
+    def read_variable_recursive(self, block, node_id):
+        """Reads the latest definition of the local variable with the
+           given node id from one of the given block's predecessor blocks."""
+        if block not in self.sealed_blocks:
+            # Create an incomplete phi.
+            val = block.append_parameter(cfg_ir.BlockParameter())
+            self.incomplete_phis[block][node_id] = val
+        elif len(self.predecessor_map[block]) == 1:
+            # Optimize the common case of one predecessor: no phi needed.
+            pred = next(iter(self.predecessor_map[block]))
+            val = self.read_variable(pred, node_id)
+        else:
+            # Break potential cycles with an operandless phi.
+            val = block.append_parameter(cfg_ir.BlockParameter())
+            self.write_variable(block, node_id, val)
+            val = self.add_phi_operands(node_id, val)
+
+        self.write_variable(block, node_id, val)
+        return val
+
+    def add_phi_operands(self, node_id, phi_def):
+        """Finds out which arguments branches should provide for the given block
+           parameter definition."""
+        # Determine operands from predecessors
+        all_values = []
+        for pred in self.predecessor_map[phi_def.block]:
+            arg = self.read_variable(pred, node_id)
+            self.extra_phi_operands[pred][phi_def] = arg
+            all_values.append(arg)
+        return self.try_remove_trivial_phi(phi_def, all_values)
+
+    def try_remove_trivial_phi(self, phi_def, values):
+        """Tries to remove a trivial block parameter definition."""
+        # This is a somewhat simplified (and less powerful) version of the
+        # algorithm in the SSA construction paper. That's kind of okay, though;
+        # trivial phi elimination is also implemented as a separate pass in the
+        # optimization pipeline.
+        trivial_phi_val = cfg_ir.get_trivial_phi_value(phi_def, values)
+        if trivial_phi_val is None:
+            return phi_def
+        else:
+            phi_def.block.remove_parameter(phi_def)
+            phi_def.redefine(trivial_phi_val)
+            phi_def.block.prepend_definition(phi_def)
+            return trivial_phi_val
+
+    def has_sealed(self, block):
+        """Tells if the given block has been sealed yet."""
+        return block in self.sealed_blocks
+
+    def can_seal(self, block):
+        """Tells if the given block can be sealed right away."""
+        # A block can be sealed if all if its predecessors have been filled.
+        return all(
+            [predecessor in self.filled_blocks for predecessor in self.predecessor_map[block]])
+
+    def seal_all_sealable_blocks(self):
+        """Seals all sealable blocks."""
+        for block in self.all_blocks:
+            if self.can_seal(block):
+                self.seal_block(block)
+
+    def seal_block(self, block):
+        """Seals the given block."""
+        if self.has_sealed(block):
+            return
+
+        for node_id, phi_def in self.incomplete_phis[block].items():
+            self.add_phi_operands(node_id, phi_def)
+
+        self.sealed_blocks.add(block)
+
+    def has_filled(self, block):
+        """Tells if the given block has been filled yet."""
+        return block in self.filled_blocks
+
+    def fill_block(self, block):
+        """Visits all definitions in the given block. Locals are converted into SSA form."""
+        if block in self.processed_blocks:
+            return
+
+        self.processed_blocks.add(block)
+
+        # Try to seal the block right away if at all possible.
+        if self.can_seal(block):
+            self.seal_block(block)
+
+        block_definitions = list(block.definitions)
+        for definition in block_definitions:
+            value = definition.value
+            if cfg_ir.is_value_def(value, cfg_ir.LoadPointer):
+                # Read the variable from the definitions dictionary.
+                node_id = get_local_id(value.pointer)
+                if node_id is not None and node_id not in self.ineligible_locals:
+                    definition.redefine(self.read_variable(block, node_id))
+            elif isinstance(value, cfg_ir.StoreAtPointer):
+                node_id = get_local_id(value.pointer)
+                if node_id is not None and node_id not in self.ineligible_locals:
+                    # Write to the variable, and replace the definition by a 'None' literal.
+                    self.write_variable(block, node_id, value.value)
+                    definition.redefine(cfg_ir.Literal(None))
+            elif isinstance(value, cfg_ir.DeclareLocal):
+                node_id = value.variable.node_id
+                if node_id not in self.ineligible_locals:
+                    definition.redefine(cfg_ir.Literal(None))
+
+
+        # Mark the block as filled.
+        self.filled_blocks.add(block)
+
+        # Seal all sealable blocks.
+        self.seal_all_sealable_blocks()
+
+        # Fill successor blocks.
+        for branch in block.flow.branches():
+            self.fill_block(branch.block)
+
+    def update_block_branches(self, block):
+        """Appends arguments to the given block's flow instruction's branches, if necessary."""
+        for branch in block.flow.branches():
+            # Find all pairs phis which are defined in the branch target block.
+            applicable_pairs = [
+                (phi_def, operand_def)
+                for phi_def, operand_def in self.extra_phi_operands[block]
+                if phi_def.block == branch.block]
+
+            if len(applicable_pairs) == 0:
+                # We might as well early-out here.
+                continue
+
+            # Sort the pairs by block parameter index.
+            sorted_pairs = sorted(
+                applicable_pairs,
+                key=lambda (phi_def, _): phi_def.block.parameters.index(phi_def))
+
+            # Append arguments to the branch.
+            for _, arg in sorted_pairs:
+                branch.arguments.append(arg)
+
+
+