2 Commits

Author SHA1 Message Date
  Forest Belton 71d80d26ab Finish optimization algorithm 2 years ago
  Forest Belton d4907eb204 Implement operand mutation 2 years ago
8 changed files with 149 additions and 53 deletions
Split View
  1. +57
    -33
      ex.py
  2. +3
    -0
      gbso/cpu/insn.py
  3. +8
    -0
      gbso/cpu/state.py
  4. +36
    -3
      gbso/optimize.py
  5. +26
    -9
      gbso/program/mutate.py
  6. +11
    -3
      gbso/program/program.py
  7. +5
    -5
      gbso/program/test_case.py
  8. +3
    -0
      tests/program/test_mutate.py

+ 57
- 33
ex.py View File

@ -1,43 +1,67 @@
from gbso.cpu.insn import *
from gbso.cpu.regs import R8, R16
from gbso.optimize import (
DEFAULT_PROB_INSN,
DEFAULT_PROB_INSN_UNUSED,
DEFAULT_PROB_OPCODE,
DEFAULT_PROB_OPERAND,
DEFAULT_PROB_SWAP,
optimize,
)
from gbso.program.program import Program
from gbso.program.mutate import mutate_program
from time import time
rLCDC = 0xFF40
rIE = 0xFFFF
from gbso.cpu.state import CPUState
from gbso.program.test_case import TestCase
from gbso.cpu.insn import *
from gbso.cpu.regs import R8
IEF_VBLANK = 1 << 0
from gbso.optimize import cost, optimize
from gbso.program.program import Program
prgm = Program(
insns=[
LD_R_N8(R8.A, 12),
LD_RR_NN(R16.HL, rLCDC),
RES_N_HL(7),
RES_N_HL(2),
LD_RR_NN(R16.HL, rIE),
LD_HL_N(IEF_VBLANK),
INC_R(R8.A),
INC_R(R8.A),
INC_R(R8.A),
INC_R(R8.A),
],
)
optimized_prgm = optimize(prgm)
outputs = [R8.A]
test_cases = [TestCase()]
max_size = 4
num_iters = 1_000_000 # 10_000_000
initial_cost = cost(prgm, test_cases, outputs, prgm)
initial_cycles = prgm.perf()
prgm.display()
print(f"Cost: {initial_cost}")
print(f"Cycles: {initial_cycles}")
start_time = time()
optimized_prgm = optimize(
prgm,
max_size=max_size,
test_cases=test_cases,
outputs=outputs,
num_iters=num_iters,
)
end_time = time()
optimized_prgm.display()
for _ in range(10):
prgm = mutate_program(
prgm,
prob_opcode=DEFAULT_PROB_OPCODE,
prob_operand=DEFAULT_PROB_OPERAND,
prob_swap=DEFAULT_PROB_SWAP,
prob_insn=DEFAULT_PROB_INSN,
prob_insn_unused=DEFAULT_PROB_INSN_UNUSED,
)
prgm.display()
optimized_cost = cost(prgm, test_cases, outputs, optimized_prgm)
optimized_cycles = optimized_prgm.perf()
print(f"Cost: {optimized_cost}")
print(f"Cycles: {optimized_cycles}")
optimized_cost = cost(prgm, test_cases, outputs, optimized_prgm)
optimized_cycles = optimized_prgm.perf()
print(f"Cost: {optimized_cost}")
print(f"Cycles: {optimized_cycles}")
optimized_cost = cost(prgm, test_cases, outputs, optimized_prgm)
optimized_cycles = optimized_prgm.perf()
print(f"Cost: {optimized_cost}")
print(f"Cycles: {optimized_cycles}")
print(f"Took {round(end_time - start_time, 3)} seconds")

+ 3
- 0
gbso/cpu/insn.py View File

@ -1047,6 +1047,9 @@ class DEC_HL(Insn):
return "DEC (HL)"
# TODO: Implement DAA
@dataclass
class CPL(Insn):
@staticmethod

+ 8
- 0
gbso/cpu/state.py View File

@ -11,3 +11,11 @@ class CPUState:
sp: int = 0
reg8: Dict[R8, int] = field(default_factory=lambda: defaultdict(lambda: 0))
memory: Dict[int, int] = field(default_factory=lambda: defaultdict(lambda: 0))
def copy(self) -> "CPUState":
return CPUState(
carry=self.carry,
sp=self.sp,
reg8=self.reg8.copy(),
memory=self.memory.copy(),
)

+ 36
- 3
gbso/optimize.py View File

@ -1,3 +1,9 @@
from math import exp, log
from random import random
from typing import List
from gbso.program.test_case import Output, TestCase, eq_on_testcase
from gbso.program.mutate import mutate_program
from gbso.program.program import Program
EPSILON = 0.00001
@ -12,8 +18,23 @@ DEFAULT_PROB_INSN = 0.25
DEFAULT_PROB_INSN_UNUSED = 0.1
def cost(orig_prgm, test_cases, outputs, prgm) -> int:
c = prgm.perf() - orig_prgm.perf()
# print(f"init cost: {c}")
for test_case in test_cases:
c += eq_on_testcase(orig_prgm, prgm, test_case, outputs)
# print(f"cost after testcase: {c}")
return c
def optimize(
prgm: Program,
max_size: int,
test_cases: List[TestCase],
outputs: List[Output],
beta: int = 0.75, # How far away in cost you are allowed to search
num_iters: int = DEFAULT_NUM_ITERS,
prob_opcode: float = DEFAULT_PROB_OPCODE,
prob_operand: float = DEFAULT_PROB_OPERAND,
@ -21,7 +42,19 @@ def optimize(
prob_insn: float = DEFAULT_PROB_INSN,
prob_insn_unused: float = DEFAULT_PROB_INSN_UNUSED,
) -> Program:
prob_sum = sum([prob_opcode, prob_operand, prob_swap, prob_insn])
assert abs(1 - prob_sum) < EPSILON
padded_prgm = prgm.pad(max_size)
last_prgm = padded_prgm
last_cost = cost(padded_prgm, test_cases, outputs, last_prgm)
for _ in range(num_iters):
candidate_prgm = mutate_program(
last_prgm, prob_opcode, prob_operand, prob_swap, prob_insn, prob_insn_unused
)
candidate_cost = cost(padded_prgm, test_cases, outputs, candidate_prgm)
if candidate_cost < last_cost - log(random()) / beta:
last_prgm = candidate_prgm
last_cost = candidate_cost
return prgm
return last_prgm

+ 26
- 9
gbso/program/mutate.py View File

@ -1,9 +1,10 @@
from dataclasses import replace
from enum import Enum
from gbso.cpu.regs import R16, R16_WITHOUT_SP, R8
from random import choice, randint, random, randrange
from typing import List, Tuple, Type, TypeVar, Union
from gbso.cpu.insn import ALL_INSN_CLASSES, Insn, Operand, UNUSED, get_signature_class
from gbso.cpu.regs import R16, R16_WITHOUT_SP, R8
from gbso.program.program import Program
@ -12,6 +13,7 @@ class Mutation(Enum):
OPERAND = "OPERAND"
SWAP = "SWAP"
INSTRUCTION = "INSTRUCTION"
NONE = "NONE"
def mutate_program(
@ -22,28 +24,27 @@ def mutate_program(
prob_insn: float,
prob_insn_unused: float,
) -> Program:
prob_none = max(0, 1 - prob_opcode - prob_operand - prob_swap - prob_insn)
mutation = sample_probs(
[
(Mutation.OPCODE, prob_opcode),
(Mutation.OPERAND, prob_operand),
(Mutation.SWAP, prob_swap),
(Mutation.INSTRUCTION, prob_insn),
(Mutation.NONE, prob_none),
]
)
new_prgm = prgm
print("======")
if mutation == Mutation.OPCODE:
print("OPCODE")
new_prgm = mutate_opcode(prgm)
elif mutation == Mutation.OPERAND:
print("OPERAND")
new_prgm = mutate_operand(prgm)
elif mutation == Mutation.SWAP:
print("SWAP")
new_prgm = mutate_swap(prgm)
elif mutation == Mutation.INSTRUCTION:
print("INSN")
new_prgm = mutate_insn(prgm, prob_insn_unused)
return new_prgm
@ -57,9 +58,25 @@ def mutate_opcode(prgm: Program) -> Program:
return Program(insns=insns)
# TODO: Implement
# God, this is ugly... a better approach would be to store operands as a tuple
# instead of individual named fields.
def mutate_operand(prgm: Program) -> Program:
return prgm
insn_index = randrange(len(prgm.insns))
insn = prgm.insns[insn_index]
if len(insn.signature()) == 0:
return prgm
operand_index = randrange(len(insn.signature()))
operand_type = insn.signature()[operand_index]
operand_name = list(vars(insn).keys())[operand_index]
insns = prgm.insns.copy()
insns[insn_index] = replace(
insn, **{operand_name: create_random_operand(operand_type)}
)
return Program(insns=insns)
def mutate_swap(prgm: Program) -> Program:
@ -124,7 +141,7 @@ def create_random_operand(op: Operand) -> Union[int, R8, R16]:
elif op == Operand.IMM8:
value = randrange(256)
elif op == Operand.IMM16:
value = randrange(65536)
value = randrange(256)
elif op == Operand.SIMM8:
value = randint(-128, 127)

+ 11
- 3
gbso/program/program.py View File

@ -1,15 +1,15 @@
from dataclasses import dataclass
from dataclasses import dataclass, field
from typing import List, Optional
from gbso.cpu.cpu import CPU
from gbso.cpu.state import CPUState
from gbso.cpu.insn import Insn
from gbso.cpu.insn import Insn, UNUSED
@dataclass
class Program:
insns: List[Insn]
insns: List[Insn] = field(default_factory=list)
def execute(self, init_state: Optional[CPUState] = None) -> CPU:
cpu = CPU(state=init_state)
@ -24,3 +24,11 @@ class Program:
print("=" * 10)
for insn in self.insns:
print(insn.pretty())
print("=" * 10)
def pad(self, max_bytes: int) -> "Program":
if len(self.insns) > max_bytes:
raise ValueError("larger than amount to pad to")
insns = self.insns.copy() + [UNUSED()] * (max_bytes - len(self.insns))
return Program(insns=insns)

+ 5
- 5
gbso/program/test_case.py View File

@ -1,4 +1,4 @@
from dataclasses import dataclass
from dataclasses import dataclass, field, replace
from typing import List, Union
from gbso.cpu.cpu import CPU
@ -14,7 +14,7 @@ Output = Union[R8, int]
@dataclass
class TestCase:
__test__ = False
state: CPUState
state: CPUState = field(default_factory=CPUState)
# TODO: Allow p_cpu to be computed AOT
@ -22,8 +22,8 @@ class TestCase:
def eq_on_testcase(
p: Program, q: Program, case: TestCase, outputs: List[Output]
) -> int:
p_cpu = p.execute(case.state)
q_cpu = q.execute(case.state)
p_cpu = p.execute(case.state.copy())
q_cpu = q.execute(case.state.copy())
return sum([eq_on_output(o, p_cpu, q_cpu) for o in outputs])
@ -44,4 +44,4 @@ def eq_8bit(x: int, y: int) -> int:
mask = 1 << i
if x & mask != y & mask:
delta += 1
return 0
return delta

+ 3
- 0
tests/program/test_mutate.py View File

@ -1,6 +1,9 @@
from gbso.cpu.insn import ADD_A_N, ADD_A_R, DEC_HL, LD_RR_NN
from gbso.program.mutate import *
# NOTE: All of these trial counts and probabilities could be a lot more precise,
# but it is easier to just throw extra trials at the problem.
def test_mutate_swap():
trials = 1000

Loading…
Cancel
Save