cfg_ssa_construction.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. """Converts 'declare-local', 'load' and 'store' instructions into SSA form."""
  2. from collections import defaultdict
  3. import modelverse_jit.cfg_ir as cfg_ir
  4. def get_local_id(def_or_value):
  5. """Gets the node of the local resolved or declared by the given definition or value.
  6. If the given definition or value does not refer to a 'resolve-local' or
  7. 'declare-local' node, then None is returned."""
  8. value = cfg_ir.get_def_value(def_or_value)
  9. if isinstance(value, (cfg_ir.ResolveLocal, cfg_ir.DeclareLocal)):
  10. return value.variable.node_id
  11. else:
  12. return None
  13. def get_ineligible_local_ids(entry_point):
  14. """Finds the ids of all local variables which are not eligible for conversion to SSA form."""
  15. # Local variables are eligible for conversion to SSA form if their pointer node is never
  16. # leaked to the outside world. So we know that we can safely convert a local to SSA form
  17. # if 'resolve-local' values are only used by 'load' and 'store' values.
  18. ineligible_local_ids = set()
  19. def __maybe_mark_ineligible(def_or_value):
  20. local_id = get_local_id(def_or_value)
  21. if local_id is not None:
  22. ineligible_local_ids.add(local_id)
  23. for block in cfg_ir.get_all_blocks(entry_point):
  24. for definition in block.definitions + [block.flow]:
  25. value = cfg_ir.get_def_value(definition)
  26. if isinstance(value, cfg_ir.LoadPointer):
  27. # Loading a pointer to a local is fine.
  28. pass
  29. elif isinstance(value, cfg_ir.StoreAtPointer):
  30. # Storing a value in a local is fine, too.
  31. # But be careful not to ignore a store where the stored value is a local pointer.
  32. __maybe_mark_ineligible(value.value)
  33. else:
  34. # Walk over the dependencies, and mark them all as ineligible for
  35. # local-to-SSA conversion.
  36. for dependency in value.get_all_dependencies():
  37. __maybe_mark_ineligible(dependency)
  38. return ineligible_local_ids
  39. def construct_ssa_form(entry_point):
  40. """Converts local variables into SSA form in the graph defined by the given entry point.
  41. A potentially new entry block is returned."""
  42. # Build some helper data structures.
  43. all_blocks = list(cfg_ir.get_all_blocks(entry_point))
  44. ineligible_locals = get_ineligible_local_ids(entry_point)
  45. predecessor_map = cfg_ir.get_all_predecessor_blocks(entry_point)
  46. # Create the SSA construction state.
  47. state = SSAConstructionState(all_blocks, ineligible_locals, predecessor_map)
  48. # Fill all blocks in the graph.
  49. for block in all_blocks:
  50. state.fill_block(block)
  51. # Update branches.
  52. for block in all_blocks:
  53. state.update_block_branches(block)
  54. # Nullify entry point parameters.
  55. return nullify_entry_block_parameters(entry_point)
  56. def nullify_entry_block_parameters(entry_point):
  57. """Creates or returns an entry block that takes no parameters.
  58. The (new) entry block is returned."""
  59. # The SSA construction algorithm has the nasty habit of replacing potentially
  60. # undefined variables by entry block parameters. Codegen doesn't like that one
  61. # bit: it assumes that all block parameters are defined by the branches to those
  62. # parameters -- but there usually is no branch to the entry point!
  63. #
  64. # We can fix this by either replacing the block-parameters in the entry block by
  65. # definitions, or by creating a new entry point that jumps to the old entry
  66. # point with an argument list. The former construction is more efficient, but it is only
  67. # correct if there are no pre-existing branches to the entry point.
  68. #
  69. # We could build the predecessor map block and use the first option if possible,
  70. # but we won't; trivial phi elimination followed by block merging will reduce
  71. # the second construction to the first one anyway.
  72. if len(entry_point.parameters) == 0:
  73. return entry_point
  74. pre_entry_point = cfg_ir.BasicBlock(entry_point.counter)
  75. arg_list = []
  76. for _ in entry_point.parameters:
  77. literal_def = pre_entry_point.append_definition(cfg_ir.Literal(None))
  78. arg_list.append(literal_def)
  79. pre_entry_point.flow = cfg_ir.create_jump(entry_point, arg_list)
  80. return pre_entry_point
  81. # The algorithms below are based on
  82. # Simple and Efficient Construction of Static Single Assignment Form by M. Braun et al
  83. # (https://pp.info.uni-karlsruhe.de/uploads/publikationen/braun13cc.pdf).
  84. class SSAConstructionState(object):
  85. """Encapsulates state related to SSA construction."""
  86. def __init__(self, all_blocks, ineligible_locals, predecessor_map):
  87. self.all_blocks = all_blocks
  88. self.ineligible_locals = ineligible_locals
  89. self.predecessor_map = predecessor_map
  90. # `current_defs` is a local node id -> basic block -> definition map.
  91. self.current_defs = defaultdict(dict)
  92. # `incomplete_phis` is a basic block -> local node id -> block parameter def map.
  93. self.incomplete_phis = defaultdict(dict)
  94. # `extra_phi_operands` is a basic block -> block parameter def -> def map.
  95. self.extra_phi_operands = defaultdict(dict)
  96. self.processed_blocks = set()
  97. self.filled_blocks = set()
  98. self.sealed_blocks = set()
  99. def read_variable(self, block, node_id):
  100. """Reads the latest definition of the local variable with the
  101. given node id for the specified block."""
  102. if block in self.current_defs[node_id]:
  103. return self.current_defs[node_id][block]
  104. else:
  105. return self.read_variable_recursive(block, node_id)
  106. def write_variable(self, block, node_id, value):
  107. """Writes the given value to the local with the specified id in the
  108. specified block."""
  109. self.current_defs[node_id][block] = value
  110. def read_variable_recursive(self, block, node_id):
  111. """Reads the latest definition of the local variable with the
  112. given node id from one of the given block's predecessor blocks."""
  113. if block not in self.sealed_blocks:
  114. # Create an incomplete phi.
  115. val = block.append_parameter(cfg_ir.BlockParameter())
  116. self.incomplete_phis[block][node_id] = val
  117. elif len(self.predecessor_map[block]) == 1:
  118. # Optimize the common case of one predecessor: no phi needed.
  119. pred = next(iter(self.predecessor_map[block]))
  120. val = self.read_variable(pred, node_id)
  121. else:
  122. # Break potential cycles with an operandless phi.
  123. val = block.append_parameter(cfg_ir.BlockParameter())
  124. self.write_variable(block, node_id, val)
  125. val = self.add_phi_operands(node_id, val)
  126. self.write_variable(block, node_id, val)
  127. return val
  128. def add_phi_operands(self, node_id, phi_def):
  129. """Finds out which arguments branches should provide for the given block
  130. parameter definition."""
  131. # Determine operands from predecessors
  132. all_values = []
  133. for pred in self.predecessor_map[phi_def.block]:
  134. arg = self.read_variable(pred, node_id)
  135. self.extra_phi_operands[pred][phi_def] = arg
  136. all_values.append(arg)
  137. return self.try_remove_trivial_phi(phi_def, all_values)
  138. def try_remove_trivial_phi(self, phi_def, values):
  139. """Tries to remove a trivial block parameter definition."""
  140. # This is a somewhat simplified (and less powerful) version of the
  141. # algorithm in the SSA construction paper. That's kind of okay, though;
  142. # trivial phi elimination is also implemented as a separate pass in the
  143. # optimization pipeline.
  144. trivial_phi_val = cfg_ir.get_trivial_phi_value(phi_def, values)
  145. if trivial_phi_val is None:
  146. return phi_def
  147. else:
  148. for pred in self.predecessor_map[phi_def.block]:
  149. del self.extra_phi_operands[pred][phi_def]
  150. phi_def.block.remove_parameter(phi_def)
  151. phi_def.redefine(trivial_phi_val)
  152. phi_def.block.prepend_definition(phi_def)
  153. return trivial_phi_val
  154. def has_sealed(self, block):
  155. """Tells if the given block has been sealed yet."""
  156. return block in self.sealed_blocks
  157. def can_seal(self, block):
  158. """Tells if the given block can be sealed right away."""
  159. # A block can be sealed if all if its predecessors have been filled.
  160. return all(
  161. [predecessor in self.filled_blocks for predecessor in self.predecessor_map[block]])
  162. def seal_all_sealable_blocks(self):
  163. """Seals all sealable blocks."""
  164. for block in self.all_blocks:
  165. if self.can_seal(block):
  166. self.seal_block(block)
  167. def seal_block(self, block):
  168. """Seals the given block."""
  169. if self.has_sealed(block):
  170. return
  171. for node_id, phi_def in self.incomplete_phis[block].items():
  172. self.add_phi_operands(node_id, phi_def)
  173. self.sealed_blocks.add(block)
  174. def has_filled(self, block):
  175. """Tells if the given block has been filled yet."""
  176. return block in self.filled_blocks
  177. def fill_block(self, block):
  178. """Visits all definitions in the given block. Locals are converted into SSA form."""
  179. if block in self.processed_blocks:
  180. return
  181. self.processed_blocks.add(block)
  182. # Try to seal the block right away if at all possible.
  183. if self.can_seal(block):
  184. self.seal_block(block)
  185. block_definitions = list(block.definitions)
  186. for definition in block_definitions:
  187. value = definition.value
  188. if cfg_ir.is_value_def(value, cfg_ir.LoadPointer):
  189. # Read the variable from the definitions dictionary.
  190. node_id = get_local_id(value.pointer)
  191. if node_id is not None and node_id not in self.ineligible_locals:
  192. definition.redefine(self.read_variable(block, node_id))
  193. elif isinstance(value, cfg_ir.StoreAtPointer):
  194. node_id = get_local_id(value.pointer)
  195. if node_id is not None and node_id not in self.ineligible_locals:
  196. # Write to the variable, and replace the definition by a 'None' literal.
  197. self.write_variable(block, node_id, value.value)
  198. definition.redefine(cfg_ir.Literal(None))
  199. elif isinstance(value, cfg_ir.DeclareLocal):
  200. node_id = value.variable.node_id
  201. if node_id not in self.ineligible_locals:
  202. definition.redefine(cfg_ir.Literal(None))
  203. # Mark the block as filled.
  204. self.filled_blocks.add(block)
  205. # Seal all sealable blocks.
  206. self.seal_all_sealable_blocks()
  207. # Fill successor blocks.
  208. for branch in block.flow.branches():
  209. self.fill_block(branch.block)
  210. def update_block_branches(self, block):
  211. """Appends arguments to the given block's flow instruction's branches, if necessary."""
  212. for branch in block.flow.branches():
  213. # Find all pairs phis which are defined in the branch target block.
  214. applicable_pairs = [
  215. (phi_def, operand_def)
  216. for phi_def, operand_def in self.extra_phi_operands[block].items()
  217. if phi_def.block == branch.block]
  218. if len(applicable_pairs) == 0:
  219. # We might as well early-out here.
  220. continue
  221. # Sort the pairs by block parameter index.
  222. sorted_pairs = sorted(
  223. applicable_pairs,
  224. key=lambda (phi_def, _): phi_def.block.parameters.index(phi_def))
  225. # Append arguments to the branch.
  226. for _, arg in sorted_pairs:
  227. branch.arguments.append(arg)