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.

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