gameboy superoptimizer
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

142 lines
4.1 KiB

2 years ago
2 years ago
  1. from dataclasses import dataclass, replace
  2. from math import log
  3. from random import random
  4. from typing import Callable, List, Optional, Tuple
  5. from gbso.program.test_case import Output, TestCase, eq_on_testcase
  6. from gbso.program.mutate import create_random_program, mutate_program
  7. from gbso.program.program import Program
  8. EPSILON = 0.00001
  9. DEFAULT_ANNEALING_CONSTANT = 0.5
  10. DEFAULT_SYNTHESIS_ITERS = 0
  11. DEFAULT_OPTIMIZE_ITERS = 5_000_000
  12. DEFAULT_NUM_CANDIDATES = 1
  13. DEFAULT_PROB_OPCODE = 0.25
  14. DEFAULT_PROB_OPERAND = 0.25
  15. DEFAULT_PROB_SWAP = 0.25
  16. DEFAULT_PROB_INSN = 0.25
  17. DEFAULT_PROB_INSN_UNUSED = 0.1
  18. def cost(
  19. orig_prgm: Program, test_cases: List[TestCase], outputs: List[Output], prgm: Program
  20. ) -> Tuple[float, bool]:
  21. # Since each instruction executes in 4*k cycles (for some k), this can have
  22. # the undesirable effect of performance improvements being weighted much
  23. # higher than correctness. This hurts convergence pretty badly, so we scale
  24. # by 1/4 to compensate.
  25. perf = (prgm.perf() - orig_prgm.perf()) / 4.0
  26. eq = 0
  27. for test_case in test_cases:
  28. eq += eq_on_testcase(orig_prgm, prgm, test_case, outputs)
  29. return perf + eq, eq == 0
  30. def cost_noperf(
  31. orig_prgm: Program, test_cases: List[TestCase], outputs: List[Output], prgm: Program
  32. ) -> Tuple[float, bool]:
  33. eq = 0
  34. for test_case in test_cases:
  35. eq += eq_on_testcase(orig_prgm, prgm, test_case, outputs)
  36. return eq, eq == 0
  37. @dataclass
  38. class OptimizationParameters:
  39. max_size: int
  40. beta: float = DEFAULT_ANNEALING_CONSTANT
  41. synthesis_iters: int = DEFAULT_SYNTHESIS_ITERS
  42. optimize_iters: int = DEFAULT_OPTIMIZE_ITERS
  43. num_candidates: int = DEFAULT_NUM_CANDIDATES
  44. prob_opcode: float = DEFAULT_PROB_OPCODE
  45. prob_operand: float = DEFAULT_PROB_OPERAND
  46. prob_swap: float = DEFAULT_PROB_SWAP
  47. prob_insn: float = DEFAULT_PROB_INSN
  48. prob_insn_unused: float = DEFAULT_PROB_INSN_UNUSED
  49. cost_fn: Callable[
  50. [Program, List[TestCase], List[Output], Program], Tuple[float, bool]
  51. ] = cost
  52. # Perform one round of optimization
  53. def _optimize(
  54. target_prgm: Program,
  55. test_cases: List[TestCase],
  56. outputs: List[Output],
  57. params: OptimizationParameters,
  58. num_iters: int = DEFAULT_OPTIMIZE_ITERS,
  59. init_prgm: Optional[Program] = None,
  60. ) -> Program:
  61. padded_prgm = target_prgm.pad(params.max_size)
  62. if init_prgm is not None:
  63. padded_prgm = init_prgm.pad(params.max_size)
  64. last_prgm = padded_prgm
  65. last_cost, _last_eq = params.cost_fn(target_prgm, test_cases, outputs, last_prgm)
  66. best_prgm = target_prgm.pad(params.max_size)
  67. best_cost = 0.0
  68. num_candidates = 0
  69. for _ in range(num_iters):
  70. candidate_prgm = mutate_program(
  71. last_prgm,
  72. params.prob_opcode,
  73. params.prob_operand,
  74. params.prob_swap,
  75. params.prob_insn,
  76. params.prob_insn_unused,
  77. )
  78. candidate_cost, candidate_eq = params.cost_fn(
  79. target_prgm, test_cases, outputs, candidate_prgm
  80. )
  81. if candidate_cost < best_cost and candidate_eq:
  82. best_prgm = candidate_prgm
  83. best_cost = candidate_cost
  84. num_candidates += 1
  85. if candidate_cost < last_cost - log(random()) / params.beta:
  86. last_prgm = candidate_prgm
  87. last_cost = candidate_cost
  88. return best_prgm
  89. def optimize(
  90. target_prgm: Program,
  91. test_cases: List[TestCase],
  92. outputs: List[Output],
  93. params: OptimizationParameters,
  94. ) -> Program:
  95. print("Synthesizing candidates...")
  96. candidates = [
  97. _optimize(
  98. target_prgm,
  99. test_cases,
  100. outputs,
  101. replace(params, cost_fn=cost_noperf),
  102. num_iters=params.synthesis_iters,
  103. init_prgm=create_random_program(params.max_size),
  104. )
  105. for _ in range(params.num_candidates)
  106. ]
  107. best_candidate = min(
  108. candidates, key=lambda p: cost(target_prgm, test_cases, outputs, p)[0]
  109. )
  110. print("Optimizing...")
  111. return _optimize(
  112. target_prgm,
  113. test_cases,
  114. outputs,
  115. params,
  116. num_iters=params.optimize_iters,
  117. init_prgm=best_candidate,
  118. )