mirror of
https://github.com/KarolS/millfork.git
synced 2025-01-21 01:32:00 +00:00
Code deduplication
This commit is contained in:
parent
f045f7f4c2
commit
ff16854a11
@ -31,6 +31,8 @@ Code that uses a custom platform definitions will cause extra warnings until fix
|
||||
|
||||
* Optimizations for stack variables.
|
||||
|
||||
* Various code deduplication optimizations.
|
||||
|
||||
* Fixed emitting constant decimal expressions.
|
||||
|
||||
* Fixed decimal subtraction.
|
||||
|
@ -58,4 +58,5 @@ where `11111` is a sequential number and `xx` is the type:
|
||||
|
||||
* `wh` – beginning of a `while` statement
|
||||
|
||||
* `xc` – automatically extracted subroutine of commonly repeating code
|
||||
|
||||
|
@ -153,6 +153,8 @@ abstract class AbstractAssembler[T <: AbstractCode](private val program: Program
|
||||
|
||||
def bytePseudoopcode: String
|
||||
|
||||
def deduplicate(options: CompilationOptions, compiledFunctions: mutable.Map[String, CompiledFunction[T]]): Unit
|
||||
|
||||
def assemble(callGraph: CallGraph, optimizations: Seq[AssemblyOptimization[T]], options: CompilationOptions): AssemblerOutput = {
|
||||
mem.programName = options.outputFileName.getOrElse("MILLFORK")
|
||||
val platform = options.platform
|
||||
@ -184,7 +186,7 @@ abstract class AbstractAssembler[T <: AbstractCode](private val program: Program
|
||||
env.allocateVariables(None, mem, callGraph, variableAllocators, options, labelMap.put, 3, forZpOnly = true)
|
||||
|
||||
var inlinedFunctions = Map[String, List[T]]()
|
||||
val compiledFunctions = mutable.Map[String, List[T]]()
|
||||
val compiledFunctions = mutable.Map[String, CompiledFunction[T]]()
|
||||
val recommendedCompilationOrder = callGraph.recommendedCompilationOrder
|
||||
val niceFunctionProperties = mutable.Set[(NiceFunctionProperty, String)]()
|
||||
recommendedCompilationOrder.foreach { f =>
|
||||
@ -199,10 +201,10 @@ abstract class AbstractAssembler[T <: AbstractCode](private val program: Program
|
||||
case Some(c) =>
|
||||
log.debug("Inlining " + f, function.position)
|
||||
inlinedFunctions += f -> c
|
||||
compiledFunctions(f) = Nil
|
||||
compiledFunctions(f) = NonexistentFunction()
|
||||
case None =>
|
||||
nonInlineableFunctions += function.name
|
||||
compiledFunctions(f) = code
|
||||
compiledFunctions(f) = NormalCompiledFunction(function.declaredBank.getOrElse(platform.defaultCodeBank), code, function.address.isDefined)
|
||||
optimizedCodeSize += code.map(_.sizeInBytes).sum
|
||||
if (options.flag(CompilationFlag.InterproceduralOptimization)) {
|
||||
gatherNiceFunctionProperties(niceFunctionProperties, f, code)
|
||||
@ -211,6 +213,7 @@ abstract class AbstractAssembler[T <: AbstractCode](private val program: Program
|
||||
function.environment.removedThings.foreach(env.removeVariable)
|
||||
}
|
||||
}
|
||||
// deduplicate(options, compiledFunctions)
|
||||
if (log.traceEnabled) {
|
||||
niceFunctionProperties.toList.groupBy(_._2).mapValues(_.map(_._1).sortBy(_.toString)).toList.sortBy(_._1).foreach{ case (fname, properties) =>
|
||||
log.trace(fname.padTo(30, ' ') + properties.mkString(" "))
|
||||
@ -253,51 +256,44 @@ abstract class AbstractAssembler[T <: AbstractCode](private val program: Program
|
||||
val bank = f.bank(options)
|
||||
val bank0 = mem.banks(bank)
|
||||
val index = f.address.get.asInstanceOf[NumericConstant].value.toInt
|
||||
val code = compiledFunctions(f.name)
|
||||
if (code.nonEmpty) {
|
||||
labelMap(f.name) = index
|
||||
val end = outputFunction(bank, code, index, assembly, options)
|
||||
for(i <- index until end) {
|
||||
bank0.occupied(index) = true
|
||||
bank0.initialized(index) = true
|
||||
bank0.readable(index) = true
|
||||
}
|
||||
compiledFunctions(f.name) match {
|
||||
case NormalCompiledFunction(_, code, _) =>
|
||||
labelMap(f.name) = index
|
||||
val end = outputFunction(bank, code, index, assembly, options)
|
||||
for (i <- index until end) {
|
||||
bank0.occupied(index) = true
|
||||
bank0.initialized(index) = true
|
||||
bank0.readable(index) = true
|
||||
}
|
||||
case NonexistentFunction() => throw new IllegalStateException()
|
||||
case RedirectedFunction(_, _, _) => throw new IllegalStateException()
|
||||
}
|
||||
case _ =>
|
||||
}
|
||||
|
||||
val codeAllocators = platform.codeAllocators.mapValues(new VariableAllocator(Nil, _))
|
||||
var justAfterCode = platform.codeAllocators.mapValues(a => a.startAt)
|
||||
env.allPreallocatables.foreach {
|
||||
case f: NormalFunction if f.address.isEmpty && f.name == "main" =>
|
||||
val bank = f.bank(options)
|
||||
|
||||
val code = compiledFunctions(f.name)
|
||||
if (code.nonEmpty) {
|
||||
val size = code.map(_.sizeInBytes).sum
|
||||
val index = codeAllocators(bank).allocateBytes(mem.banks(bank), options, size, initialized = true, writeable = false, location = AllocationLocation.High)
|
||||
labelMap(f.name) = index
|
||||
justAfterCode += bank -> outputFunction(bank, code, index, assembly, options)
|
||||
}
|
||||
case _ =>
|
||||
}
|
||||
env.allPreallocatables.foreach {
|
||||
case f: NormalFunction if f.address.isEmpty && f.name != "main" =>
|
||||
val bank = f.bank(options)
|
||||
val bank0 = mem.banks(bank)
|
||||
val code = compiledFunctions(f.name)
|
||||
if (code.nonEmpty) {
|
||||
val size = code.map(_.sizeInBytes).sum
|
||||
val index = codeAllocators(bank).allocateBytes(bank0, options, size, initialized = true, writeable = false, location = AllocationLocation.High)
|
||||
labelMap(f.name) = index
|
||||
justAfterCode += bank -> outputFunction(bank, code, index, assembly, options)
|
||||
}
|
||||
case _ =>
|
||||
compiledFunctions.toList.sortBy{case (name, cf) => if (name == "main") 0 -> "" else cf.orderKey}.foreach {
|
||||
case (_, NormalCompiledFunction(_, _, true)) =>
|
||||
// already done before
|
||||
case (name, NormalCompiledFunction(bank, code, false)) =>
|
||||
val size = code.map(_.sizeInBytes).sum
|
||||
val index = codeAllocators(bank).allocateBytes(mem.banks(bank), options, size, initialized = true, writeable = false, location = AllocationLocation.High)
|
||||
labelMap(name) = index
|
||||
justAfterCode += bank -> outputFunction(bank, code, index, assembly, options)
|
||||
case (_, NonexistentFunction()) =>
|
||||
case (name, RedirectedFunction(_, target, offset)) =>
|
||||
labelMap(name) = labelMap(target) + offset
|
||||
}
|
||||
|
||||
if (options.flag(CompilationFlag.LUnixRelocatableCode)) {
|
||||
env.allThings.things.foreach {
|
||||
case (_, m@UninitializedMemoryVariable(name, typ, _, _)) if name.endsWith(".addr") || env.maybeGet[Thing](name + ".array").isDefined =>
|
||||
val isUsed = compiledFunctions.values.exists(_.exists(_.parameter.isRelatedTo(m)))
|
||||
val isUsed = compiledFunctions.values.exists{
|
||||
case NormalCompiledFunction(_, code, _) => code.exists(_.parameter.isRelatedTo(m))
|
||||
case _ => false
|
||||
}
|
||||
// println(m.name -> isUsed)
|
||||
if (isUsed) {
|
||||
val bank = m.bank(options)
|
||||
|
22
src/main/scala/millfork/output/CompiledFunction.scala
Normal file
22
src/main/scala/millfork/output/CompiledFunction.scala
Normal file
@ -0,0 +1,22 @@
|
||||
package millfork.output
|
||||
|
||||
import millfork.assembly.AbstractCode
|
||||
|
||||
/**
|
||||
* @author Karol Stasiak
|
||||
*/
|
||||
sealed trait CompiledFunction[T <: AbstractCode] {
|
||||
def orderKey : (Int, String)
|
||||
}
|
||||
|
||||
case class NormalCompiledFunction[T <: AbstractCode](segment: String, code: List[T], hasFixedAddress: Boolean) extends CompiledFunction[T] {
|
||||
override def orderKey: (Int, String) = (if (hasFixedAddress) 1 else 2) -> ""
|
||||
}
|
||||
|
||||
case class RedirectedFunction[T <: AbstractCode](segment: String, redirect: String, offset: Int) extends CompiledFunction[T] {
|
||||
override def orderKey: (Int, String) = 3 -> redirect
|
||||
}
|
||||
|
||||
case class NonexistentFunction[T <: AbstractCode]() extends CompiledFunction[T] {
|
||||
override def orderKey: (Int, String) = 4 -> ""
|
||||
}
|
264
src/main/scala/millfork/output/Deduplicate.scala
Normal file
264
src/main/scala/millfork/output/Deduplicate.scala
Normal file
@ -0,0 +1,264 @@
|
||||
package millfork.output
|
||||
|
||||
import millfork.{CompilationFlag, CompilationOptions}
|
||||
import millfork.assembly.AbstractCode
|
||||
import millfork.env.Environment
|
||||
|
||||
import scala.collection.mutable
|
||||
import scala.collection.mutable.ListBuffer
|
||||
|
||||
/**
|
||||
* @author Karol Stasiak
|
||||
*/
|
||||
abstract class Deduplicate[T <: AbstractCode](env: Environment, options: CompilationOptions) {
|
||||
|
||||
def apply(compiledFunctions: mutable.Map[String, CompiledFunction[T]]): Unit = {
|
||||
if (options.flag(CompilationFlag.OptimizeForSize)) {
|
||||
runStage(compiledFunctions, extractCommonCode)
|
||||
}
|
||||
runStage(compiledFunctions, deduplicateIdenticalFunctions)
|
||||
runStage(compiledFunctions, eliminateTailJumps)
|
||||
}
|
||||
|
||||
def runStage(compiledFunctions: mutable.Map[String, CompiledFunction[T]],
|
||||
function: (String, Map[String, Either[String, List[T]]]) => Seq[(String, CompiledFunction[T])]): Unit = {
|
||||
bySegment(compiledFunctions).foreach {
|
||||
case (segmentName, segContents) =>
|
||||
function(segmentName, segContents).foreach {
|
||||
case (fname, cf) => compiledFunctions(fname) = cf
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def extractCommonCode(segmentName: String, segContents: Map[String, Either[String, List[T]]]): Seq[(String, CompiledFunction[T])] = {
|
||||
var result = ListBuffer[(String, CompiledFunction[T])]()
|
||||
val chunks = segContents.flatMap{
|
||||
case (_, Left(_)) => Nil
|
||||
case (functionName, Right(code)) =>
|
||||
if (options.flag(CompilationFlag.OptimizeForSize)) {
|
||||
getExtractableSnippets(functionName, code)
|
||||
} else Nil
|
||||
}.flatMap { chunk =>
|
||||
for {
|
||||
start <- chunk.code.indices
|
||||
end <- start + 1 to chunk.code.length
|
||||
} yield CodeChunk(chunk.functionName, chunk.offset + start, chunk.offset + end)(chunk.code.slice(start, end))
|
||||
}.filter(_.codeSizeInBytes > 3).groupBy(_.code).filter{
|
||||
case (code, _) =>
|
||||
if (isBadExtractedCodeHead(code.head)) false
|
||||
else if (isBadExtractedCodeLast(code.last)) false
|
||||
else true
|
||||
}.mapValues(_.toSeq).filter {
|
||||
case (_, instances) =>
|
||||
val chunkSize = instances.head.codeSizeInBytes
|
||||
val extractedProcedureSize = chunkSize + 1
|
||||
val savedInCallers = (chunkSize - 3) * instances.length
|
||||
val maxPossibleProfit = savedInCallers - extractedProcedureSize
|
||||
// (instances.length >=2) println(s"Instances: ${instances.length}, max profit: $maxPossibleProfit: $instances")
|
||||
maxPossibleProfit > 0 && instances.length >= 2 // TODO
|
||||
}.flatMap(_._2).toSeq
|
||||
//println(s"Chunks: ${chunks.length} $chunks")
|
||||
val candidates: Seq[(Int, Map[List[T], Seq[CodeChunk[T]]])] = powerSet(chunks)((set, chunk) => !set.exists(_ & chunk)).filter(_.nonEmpty).filter(set => (for {
|
||||
x <- set
|
||||
y <- set
|
||||
if x != y
|
||||
} yield x & y).forall(_ == false)).toSeq.map(_.groupBy(_.code).filter(_._2.size >= 2).mapValues(_.toSeq)).filter(_.nonEmpty).map { map =>
|
||||
map.foldLeft(0) {
|
||||
(sum, entry) =>
|
||||
val chunkSize = entry._2.head.codeSizeInBytes
|
||||
val chunkCount = entry._2.size
|
||||
val extractedProcedureSize = chunkSize + 1
|
||||
val savedInCallers = (chunkSize - 3) * chunkCount
|
||||
sum + savedInCallers - extractedProcedureSize
|
||||
} -> map
|
||||
}.filter { set =>
|
||||
val allChunks = set._2.values.flatten
|
||||
(for {
|
||||
x <- allChunks
|
||||
y <- allChunks
|
||||
if x != y
|
||||
} yield x & y).forall(_ == false)
|
||||
}
|
||||
// candidates.sortBy(_._1).foreach {
|
||||
// case (profit, map) =>
|
||||
// if (profit > 0) {
|
||||
// println(s"Profit: $profit ${map.map { case (_, instances) => s"${instances.length}×${instances.head}" }.mkString(" ; ")}")
|
||||
// }
|
||||
// }
|
||||
if (candidates.nonEmpty) {
|
||||
val best = candidates.maxBy(_._1)
|
||||
//println(s"Best extraction candidate: $best")
|
||||
val allAffectedFunctions = best._2.values.flatten.map(_.functionName).toSet
|
||||
val toRemove = allAffectedFunctions.map(_ -> mutable.Set[Int]()).toMap
|
||||
val toReplace = allAffectedFunctions.map(_ -> mutable.Map[Int, String]()).toMap
|
||||
if (options.log.traceEnabled){
|
||||
options.log.debug(s"Extracted ${best._2.size} common code subroutines from ${allAffectedFunctions.size} functions, saving $best._1 bytes")
|
||||
}
|
||||
for((code, instances) <- best._2) {
|
||||
val newName = env.nextLabel("xc")
|
||||
result += newName -> NormalCompiledFunction(segmentName, createLabel(newName) :: tco(code :+ createReturn), hasFixedAddress = false)
|
||||
for(instance <- instances) {
|
||||
toReplace(instance.functionName)(instance.offset) = newName
|
||||
for (i <- instance.offset + 1 until instance.endOffset) {
|
||||
toRemove(instance.functionName) += i
|
||||
}
|
||||
}
|
||||
}
|
||||
for(functionName <- allAffectedFunctions) {
|
||||
result += functionName -> {
|
||||
val linesToRemove = toRemove(functionName)
|
||||
val linesToReplace = toReplace(functionName)
|
||||
val newCode = segContents(functionName).right.get.zipWithIndex.flatMap{
|
||||
case (line, i) =>
|
||||
if (linesToRemove(i)) None
|
||||
else if (linesToReplace.contains(i)) Some(createCall(linesToReplace(i)))
|
||||
else Some(line)
|
||||
}
|
||||
NormalCompiledFunction(segmentName, tco(newCode), hasFixedAddress = false)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
result.toSeq
|
||||
}
|
||||
|
||||
def deduplicateIdenticalFunctions(segmentName: String, segContents: Map[String, Either[String, List[T]]]): Seq[(String, CompiledFunction[T])] = {
|
||||
var result = ListBuffer[(String, CompiledFunction[T])]()
|
||||
val identicalFunctions = segContents.flatMap{
|
||||
case (name, code) => code.toOption.map(c => name -> actualCode(name, c))
|
||||
}.groupBy(_._2).values.toSeq.map(_.keySet).filter(set => set.size > 1)
|
||||
for(set <- identicalFunctions) {
|
||||
val representative = if (set("main")) "main" else set.head
|
||||
options.log.debug(s"Functions [${set.mkString(",")}] are identical")
|
||||
for (function <- set) {
|
||||
if (function != representative) {
|
||||
result += function -> RedirectedFunction(segmentName, representative, 0)
|
||||
} else {
|
||||
segContents(function) match {
|
||||
case Right(code) =>
|
||||
result += function -> NormalCompiledFunction(segmentName,
|
||||
set.toList.map(name => createLabel(name)) ++ actualCode(function, code),
|
||||
hasFixedAddress = false)
|
||||
case Left(_) =>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
result.toSeq
|
||||
}
|
||||
|
||||
private def follow(segContents: Map[String, Either[String, List[T]]], to: String): Option[String] = {
|
||||
var result: String = to
|
||||
val visited = mutable.Set[String]()
|
||||
do {
|
||||
segContents.get(to) match {
|
||||
case Some(Left(next)) =>
|
||||
if (visited(next)) return None
|
||||
visited += result
|
||||
result = next
|
||||
case Some(Right(_)) =>
|
||||
return Some(result)
|
||||
case _ => return None
|
||||
}
|
||||
} while(true)
|
||||
None
|
||||
}
|
||||
|
||||
def eliminateTailJumps(segmentName: String, segContents: Map[String, Either[String, List[T]]]): Seq[(String, CompiledFunction[T])] = {
|
||||
var result = ListBuffer[(String, CompiledFunction[T])]()
|
||||
val fallThroughList = segContents.flatMap {
|
||||
case (name, Right(code)) =>
|
||||
if (code.isEmpty) None
|
||||
else getJump(code.last)
|
||||
.filter(segContents.contains)
|
||||
.filter(_ != name)
|
||||
.filter(_ != "main")
|
||||
.flatMap(to => follow(segContents, to))
|
||||
.map(name -> _)
|
||||
case _ => None
|
||||
}
|
||||
val fallthroughPredecessors = fallThroughList.groupBy(_._2).mapValues(_.head._1) // TODO: be smarter than head
|
||||
fallthroughPredecessors.foreach {
|
||||
case (to, from) =>
|
||||
options.log.debug(s"Fallthrough from $from to $to")
|
||||
val init = segContents(from).right.get.init
|
||||
result += from -> NormalCompiledFunction(segmentName,
|
||||
init ++ segContents(to).right.get,
|
||||
hasFixedAddress = false
|
||||
)
|
||||
val initSize = init.map(_.sizeInBytes).sum
|
||||
if (initSize <= 2) {
|
||||
result += to -> RedirectedFunction(segmentName, from, initSize)
|
||||
} else {
|
||||
result += to -> NonexistentFunction()
|
||||
}
|
||||
}
|
||||
result.toSeq
|
||||
}
|
||||
|
||||
def tco(code: List[T]): List[T]
|
||||
|
||||
def isBadExtractedCodeHead(head: T): Boolean
|
||||
|
||||
def isBadExtractedCodeLast(head: T): Boolean
|
||||
|
||||
def getJump(line: T): Option[String]
|
||||
|
||||
def createCall(functionName: String): T
|
||||
|
||||
def createReturn(): T
|
||||
|
||||
def createLabel(name: String): T
|
||||
|
||||
def bySegment(compiledFunctions: mutable.Map[String, CompiledFunction[T]]): Map[String, Map[String, Either[String, List[T]]]] = {
|
||||
compiledFunctions.flatMap {
|
||||
case (name, NormalCompiledFunction(segment, code, false)) => Some((segment, name, Right(code))) // TODO
|
||||
case (name, RedirectedFunction(segment, target, 0)) => Some((segment, name, Left(target))) // TODO
|
||||
case _ => None
|
||||
}.groupBy(_._1).mapValues(_.map { case (_, name, code) => name -> code }.toMap)
|
||||
}
|
||||
|
||||
def actualCode(functionName: String, functionCode: List[T]): List[T]
|
||||
|
||||
def isExtractable(line: T): Boolean
|
||||
|
||||
def getExtractableSnippets(functionName: String, code: List[T]): List[CodeChunk[T]] = {
|
||||
var cursor = 0
|
||||
var mutCode = code
|
||||
val result = mutable.ListBuffer[CodeChunk[T]]()
|
||||
while (true) {
|
||||
val (bad, rest1) = mutCode.span(l => !isExtractable(l))
|
||||
mutCode = rest1
|
||||
cursor += bad.length
|
||||
val (good, rest2) = mutCode.span(l => isExtractable(l))
|
||||
mutCode = rest2
|
||||
if (good.nonEmpty) {
|
||||
result += CodeChunk(functionName, cursor, cursor + good.length)(good)
|
||||
cursor += good.length
|
||||
} else {
|
||||
//println(s"Snippets in $functionName: $result")
|
||||
return result.toList
|
||||
}
|
||||
}
|
||||
null
|
||||
}
|
||||
|
||||
def powerSet[A](t: Iterable[A])(f: (Set[A], A) => Boolean): Set[Set[A]] = {
|
||||
@annotation.tailrec
|
||||
def pwr(t: Iterable[A], ps: Set[Set[A]]): Set[Set[A]] =
|
||||
if (t.isEmpty) ps
|
||||
else pwr(t.tail, ps ++ (ps.filter(p => f(p, t.head)) map (_ + t.head)))
|
||||
pwr(t, Set(Set.empty[A]))
|
||||
}
|
||||
}
|
||||
|
||||
case class CodeChunk[T <: AbstractCode](functionName: String, offset: Int, endOffset: Int)(val code: List[T]) {
|
||||
val codeSizeInBytes: Int = code.map(_.sizeInBytes).sum
|
||||
|
||||
def &(that: CodeChunk[T]): Boolean =
|
||||
this.functionName == that.functionName &&
|
||||
this.offset <= that.endOffset &&
|
||||
that.offset <= this.endOffset
|
||||
|
||||
override def toString: String = s"$functionName:$offset:${code.map(_.toString.trim).mkString(";")}($codeSizeInBytes bytes)"
|
||||
}
|
@ -166,6 +166,9 @@ class MosAssembler(program: Program,
|
||||
}
|
||||
|
||||
override def bytePseudoopcode: String = "!byte"
|
||||
|
||||
override def deduplicate(options: CompilationOptions, compiledFunctions: mutable.Map[String, CompiledFunction[AssemblyLine]]): Unit =
|
||||
new MosDeduplicate(rootEnv, options).apply(compiledFunctions)
|
||||
}
|
||||
|
||||
|
||||
|
66
src/main/scala/millfork/output/MosDeduplicate.scala
Normal file
66
src/main/scala/millfork/output/MosDeduplicate.scala
Normal file
@ -0,0 +1,66 @@
|
||||
package millfork.output
|
||||
|
||||
import millfork.CompilationOptions
|
||||
import millfork.assembly.mos.{AddrMode, AssemblyLine, Opcode}
|
||||
import millfork.env.{Environment, Label, MemoryAddressConstant}
|
||||
import Opcode._
|
||||
import millfork.assembly.mos.AddrMode._
|
||||
|
||||
/**
|
||||
* @author Karol Stasiak
|
||||
*/
|
||||
class MosDeduplicate(env: Environment, options: CompilationOptions) extends Deduplicate[AssemblyLine](env, options) {
|
||||
override def getJump(line: AssemblyLine): Option[String] = line match {
|
||||
case AssemblyLine(Opcode.JMP, Absolute, MemoryAddressConstant(thing), _) => Some(thing.name)
|
||||
case _ => None
|
||||
}
|
||||
|
||||
override def createLabel(name: String): AssemblyLine = AssemblyLine.label(name)
|
||||
|
||||
override def actualCode(FunctionName: String, functionCode: List[AssemblyLine]): List[AssemblyLine] = {
|
||||
functionCode match {
|
||||
case AssemblyLine(Opcode.LABEL, _, MemoryAddressConstant(Label(FunctionName)), _) :: xs => xs
|
||||
case xs => xs
|
||||
}
|
||||
}
|
||||
|
||||
private val goodOpcodes = Set(
|
||||
ADC, SBC, CMP, AND, EOR, ORA,
|
||||
ADC_W, SBC_W, CMP_W, AND_W, EOR_W, ORA_W,
|
||||
ASL, ROL, LSR, ROR, INC, DEC,
|
||||
ASL_W, ROL_W, LSR_W, ROR_W, INC_W, DEC_W,
|
||||
NEG, ASR,
|
||||
LDA, STA, LDX, STX, LDY, STY, LDZ, STZ,
|
||||
LDA_W, STA_W, LDX_W, STX_W, LDY_W, STY_W, STZ_W,
|
||||
TAX, TXA, TAY, TYA, TXY, TYX, TAZ, TZA, XBA,
|
||||
SLO, SRE, RRA, RLA, ARR, ALR, ANC, SBX, LXA, XAA, DCP, ISC,
|
||||
CPX, CPY, CPZ, CPX_W, CPY_W,
|
||||
INX, INY, INZ, INX_W, INY_W,
|
||||
DEX, DEY, DEZ, DEX_W, DEY_W,
|
||||
BIT, TRB, TSB,
|
||||
JSR,
|
||||
NOP, WAI, STP,
|
||||
SED, CLD, SEC, CLC, CLV, SEI, CLI, SEP, REP,
|
||||
HuSAX, SAY, SXY,
|
||||
CLA, CLX, CLY,
|
||||
)
|
||||
|
||||
private val badAddressingModes = Set(Stack, IndexedSY, Relative)
|
||||
|
||||
override def isExtractable(line: AssemblyLine): Boolean =
|
||||
goodOpcodes(line.opcode) && !badAddressingModes(line.addrMode)
|
||||
|
||||
override def isBadExtractedCodeHead(head: AssemblyLine): Boolean = false
|
||||
|
||||
override def isBadExtractedCodeLast(head: AssemblyLine): Boolean = false
|
||||
|
||||
override def createCall(functionName: String): AssemblyLine = AssemblyLine.absolute(Opcode.JSR, Label(functionName))
|
||||
|
||||
override def createReturn(): AssemblyLine = AssemblyLine.implied(RTS)
|
||||
|
||||
override def tco(code: List[AssemblyLine]): List[AssemblyLine] = code match {
|
||||
case (call@AssemblyLine(JSR, Absolute, _, _)) :: AssemblyLine(RTS, _, _, _) :: xs => call.copy(opcode = JMP) :: tco(xs)
|
||||
case x :: xs => x :: tco(xs)
|
||||
case Nil => Nil
|
||||
}
|
||||
}
|
@ -635,6 +635,9 @@ class Z80Assembler(program: Program,
|
||||
}
|
||||
|
||||
override def bytePseudoopcode: String = "DB"
|
||||
|
||||
override def deduplicate(options: CompilationOptions, compiledFunctions: mutable.Map[String, CompiledFunction[ZLine]]): Unit =
|
||||
new Z80Deduplicate(rootEnv, options).apply(compiledFunctions)
|
||||
}
|
||||
|
||||
object Z80Assembler {
|
||||
|
75
src/main/scala/millfork/output/Z80Deduplicate.scala
Normal file
75
src/main/scala/millfork/output/Z80Deduplicate.scala
Normal file
@ -0,0 +1,75 @@
|
||||
package millfork.output
|
||||
|
||||
import millfork.CompilationOptions
|
||||
import millfork.assembly.z80.{ZOpcode, _}
|
||||
import millfork.env.{Environment, Label, MemoryAddressConstant}
|
||||
import ZOpcode._
|
||||
import millfork.node.ZRegister.SP
|
||||
|
||||
/**
|
||||
* @author Karol Stasiak
|
||||
*/
|
||||
class Z80Deduplicate(env: Environment, options: CompilationOptions) extends Deduplicate[ZLine](env, options) {
|
||||
override def getJump(line: ZLine): Option[String] = line match {
|
||||
case ZLine(JP, NoRegisters, MemoryAddressConstant(thing), _) => Some(thing.name)
|
||||
case _ => None
|
||||
}
|
||||
|
||||
override def createLabel(name: String): ZLine = ZLine.label(name)
|
||||
|
||||
override def actualCode(FunctionName: String, functionCode: List[ZLine]): List[ZLine] = {
|
||||
functionCode match {
|
||||
case ZLine(LABEL, _, MemoryAddressConstant(Label(FunctionName)), _) :: xs => xs
|
||||
case xs => xs
|
||||
}
|
||||
}
|
||||
|
||||
private val alwaysGoodOpcodes: Set[ZOpcode.Value] = Set(
|
||||
ADD, ADC, SUB, SBC, XOR, OR, AND, CP,
|
||||
LD, INC, DEC,
|
||||
DAA, CPL, SCF, CCF, NEG, EX_DE_HL,
|
||||
RLA, RRA, RLCA, RRCA,
|
||||
RL, RR, RLC, RRC, SLA, SLL, SRL, SRA, SWAP,
|
||||
RLD, RRD,
|
||||
EI, DI, IM, HALT, NOP,
|
||||
LDI, LDD, LDIR, LDDR,
|
||||
INI, IND, INIR, INDR,
|
||||
CPI, CPD, CPIR, CPDR,
|
||||
OUTI, OUTD, OUTIR, OUTDR,
|
||||
IN_IMM, OUT_IMM, IN_C, OUT_C,
|
||||
LD_AHLI, LD_AHLD, LD_HLIA, LD_HLDA,
|
||||
LDH_AC, LDH_AD, LDH_CA, LDH_DA,
|
||||
CALL,
|
||||
) ++ ZOpcodeClasses.AllSingleBit
|
||||
|
||||
private val conditionallyGoodOpcodes = Set(
|
||||
LD_16, ADD_16, SBC_16, ADC_16, INC_16, DEC_16,
|
||||
)
|
||||
|
||||
override def isExtractable(line: ZLine): Boolean = {
|
||||
alwaysGoodOpcodes(line.opcode) ||
|
||||
conditionallyGoodOpcodes(line.opcode) && (line.registers match {
|
||||
case OneRegister(SP) => false
|
||||
case TwoRegisters(_, SP) => false
|
||||
case TwoRegisters(SP, _) => false
|
||||
case _ => true
|
||||
})
|
||||
}
|
||||
|
||||
override def isBadExtractedCodeHead(head: ZLine): Boolean = false
|
||||
|
||||
override def isBadExtractedCodeLast(head: ZLine): Boolean = head.opcode match {
|
||||
case EI | DI | IM => true
|
||||
case _ => false
|
||||
}
|
||||
|
||||
override def createCall(functionName: String): ZLine = ZLine(CALL, NoRegisters, MemoryAddressConstant(Label(functionName)), elidable = false)
|
||||
|
||||
override def createReturn(): ZLine = ZLine.implied(RET)
|
||||
|
||||
override def tco(code: List[ZLine]): List[ZLine] = code match {
|
||||
case (call@ZLine(CALL, _, _, _)) :: ZLine(RET, NoRegisters, _, _) :: xs => call.copy(opcode = JP) :: tco(xs)
|
||||
case x :: xs => x :: tco(xs)
|
||||
case Nil => Nil
|
||||
}
|
||||
}
|
72
src/test/scala/millfork/test/DeduplicationSuite.scala
Normal file
72
src/test/scala/millfork/test/DeduplicationSuite.scala
Normal file
@ -0,0 +1,72 @@
|
||||
package millfork.test
|
||||
|
||||
import millfork.Cpu
|
||||
import millfork.test.emu._
|
||||
import org.scalatest.{FunSuite, Matchers}
|
||||
|
||||
/**
|
||||
* @author Karol Stasiak
|
||||
*/
|
||||
class DeduplicationSuite extends FunSuite with Matchers {
|
||||
|
||||
test("Code deduplication") {
|
||||
EmuOptimizedCmosRun(
|
||||
"""
|
||||
|
|
||||
| void main() {
|
||||
| times2(1)
|
||||
| shift_left(2)
|
||||
| nothing(2)
|
||||
| }
|
||||
| noinline byte shift_left(byte x) {
|
||||
| return x << 1
|
||||
| }
|
||||
| noinline byte times2(byte x) {
|
||||
| x *= 2
|
||||
| return x
|
||||
| }
|
||||
| noinline void nothing(byte x) {
|
||||
| }
|
||||
""".stripMargin)
|
||||
}
|
||||
|
||||
test("Subroutine extraction") {
|
||||
EmuSizeOptimizedCrossPlatformRun(Cpu.Mos, Cpu.Z80, Cpu.Intel8080)(
|
||||
"""
|
||||
| int24 output @$c000
|
||||
| void main() {
|
||||
| output.b0 = f(1)
|
||||
| output.b1 = g(2)
|
||||
| output.b2 = h(2)
|
||||
| }
|
||||
| noinline byte f(byte x) {
|
||||
| x += 2
|
||||
| x |= 4
|
||||
| x <<= 1
|
||||
| x &= 7
|
||||
| x -= 6
|
||||
| return x
|
||||
| }
|
||||
| noinline byte g(byte x) {
|
||||
| x += 3
|
||||
| x |= 4
|
||||
| x <<= 1
|
||||
| x &= 7
|
||||
| x -= 7
|
||||
| return x
|
||||
| }
|
||||
| noinline byte h(byte x) {
|
||||
| x += 5
|
||||
| x |= 4
|
||||
| x <<= 1
|
||||
| x &= 7
|
||||
| x -= 5
|
||||
| return x
|
||||
| }
|
||||
| noinline void nothing(byte x) {
|
||||
| }
|
||||
""".stripMargin) {m =>
|
||||
m.readMedium(0xc000) should equal(0x1FB00)
|
||||
}
|
||||
}
|
||||
}
|
@ -20,10 +20,32 @@ object EmuOptimizedRun extends EmuRun(
|
||||
OptimizationPresets.Good ++
|
||||
OptimizationPresets.Good)
|
||||
|
||||
object EmuSizeOptimizedRun extends EmuRun(
|
||||
Cpu.StrictMos,
|
||||
OptimizationPresets.NodeOpt,
|
||||
OptimizationPresets.AssOpt ++
|
||||
ZeropageRegisterOptimizations.All ++
|
||||
OptimizationPresets.Good ++
|
||||
OptimizationPresets.Good ++
|
||||
OptimizationPresets.Good ++ LaterOptimizations.Nmos ++
|
||||
OptimizationPresets.Good ++ LaterOptimizations.Nmos ++
|
||||
ZeropageRegisterOptimizations.All ++
|
||||
OptimizationPresets.Good ++
|
||||
OptimizationPresets.Good) {
|
||||
override def optimizeForSize = true
|
||||
}
|
||||
|
||||
|
||||
object EmuOptimizedZ80Run extends EmuZ80Run(Cpu.Z80, OptimizationPresets.NodeOpt, Z80OptimizationPresets.GoodForZ80)
|
||||
|
||||
object EmuSizeOptimizedZ80Run extends EmuZ80Run(Cpu.Z80, OptimizationPresets.NodeOpt, Z80OptimizationPresets.GoodForZ80) {
|
||||
override def optimizeForSize = true
|
||||
}
|
||||
|
||||
object EmuOptimizedIntel8080Run extends EmuZ80Run(Cpu.Intel8080, OptimizationPresets.NodeOpt, Z80OptimizationPresets.GoodForIntel8080)
|
||||
|
||||
object EmuSizeOptimizedIntel8080Run extends EmuZ80Run(Cpu.Intel8080, OptimizationPresets.NodeOpt, Z80OptimizationPresets.GoodForIntel8080) {
|
||||
override def optimizeForSize = true
|
||||
}
|
||||
|
||||
object EmuOptimizedSharpRun extends EmuZ80Run(Cpu.Sharp, OptimizationPresets.NodeOpt, Z80OptimizationPresets.GoodForSharp)
|
||||
|
@ -61,6 +61,8 @@ class EmuRun(cpu: millfork.Cpu.Value, nodeOptimizations: List[NodeOptimization],
|
||||
|
||||
def blastProcessing = false
|
||||
|
||||
def optimizeForSize = false
|
||||
|
||||
private val timingNmos = Array[Int](
|
||||
7, 6, 0, 8, 3, 3, 5, 5, 3, 2, 2, 2, 4, 4, 6, 6,
|
||||
2, 5, 0, 8, 4, 4, 6, 6, 2, 4, 2, 7, 4, 4, 7, 7,
|
||||
@ -133,6 +135,7 @@ class EmuRun(cpu: millfork.Cpu.Value, nodeOptimizations: List[NodeOptimization],
|
||||
CompilationFlag.EmitEmulation65816Opcodes -> (platform.cpu == millfork.Cpu.Sixteen),
|
||||
CompilationFlag.Emit65CE02Opcodes -> (platform.cpu == millfork.Cpu.CE02),
|
||||
CompilationFlag.EmitHudsonOpcodes -> (platform.cpu == millfork.Cpu.HuC6280),
|
||||
CompilationFlag.OptimizeForSize -> optimizeForSize,
|
||||
CompilationFlag.OptimizeForSpeed -> blastProcessing,
|
||||
CompilationFlag.OptimizeForSonicSpeed -> blastProcessing
|
||||
// CompilationFlag.CheckIndexOutOfBounds -> true,
|
||||
|
@ -0,0 +1,27 @@
|
||||
package millfork.test.emu
|
||||
|
||||
import millfork.Cpu
|
||||
import millfork.output.MemoryBank
|
||||
|
||||
/**
|
||||
* @author Karol Stasiak
|
||||
*/
|
||||
object EmuSizeOptimizedCrossPlatformRun {
|
||||
def apply(platforms: Cpu.Value*)(source: String)(verifier: MemoryBank => Unit): Unit = {
|
||||
val (_, mm) = if (platforms.contains(Cpu.Mos)) EmuSizeOptimizedRun.apply2(source) else Timings(-1, -1) -> null
|
||||
val (_, mz) = if (platforms.contains(Cpu.Z80)) EmuSizeOptimizedZ80Run.apply2(source) else Timings(-1, -1) -> null
|
||||
val (_, mi) = if (platforms.contains(Cpu.Intel8080)) EmuSizeOptimizedIntel8080Run.apply2(source) else Timings(-1, -1) -> null
|
||||
if (platforms.contains(Cpu.Mos)) {
|
||||
println(f"Running 6502")
|
||||
verifier(mm)
|
||||
}
|
||||
if (platforms.contains(Cpu.Z80)) {
|
||||
println(f"Running Z80")
|
||||
verifier(mz)
|
||||
}
|
||||
if (platforms.contains(Cpu.Intel8080)) {
|
||||
println(f"Running 8080")
|
||||
verifier(mi)
|
||||
}
|
||||
}
|
||||
}
|
@ -24,6 +24,8 @@ import org.scalatest.Matchers
|
||||
class EmuZ80Run(cpu: millfork.Cpu.Value, nodeOptimizations: List[NodeOptimization], assemblyOptimizations: List[AssemblyOptimization[ZLine]]) extends Matchers {
|
||||
def inline: Boolean = false
|
||||
|
||||
def optimizeForSize: Boolean = false
|
||||
|
||||
private val TooManyCycles: Long = 1500000
|
||||
|
||||
def apply(source: String): MemoryBank = {
|
||||
@ -38,6 +40,7 @@ class EmuZ80Run(cpu: millfork.Cpu.Value, nodeOptimizations: List[NodeOptimizatio
|
||||
val platform = EmuPlatform.get(cpu)
|
||||
val extraFlags = Map(
|
||||
CompilationFlag.InlineFunctions -> this.inline,
|
||||
CompilationFlag.OptimizeForSize -> this.optimizeForSize,
|
||||
CompilationFlag.EmitIllegals -> (cpu == millfork.Cpu.Z80),
|
||||
CompilationFlag.LenientTextEncoding -> true)
|
||||
val options = CompilationOptions(platform, millfork.Cpu.defaultFlags(cpu).map(_ -> true).toMap ++ extraFlags, None, 0, JobContext(log, new LabelGenerator))
|
||||
|
Loading…
x
Reference in New Issue
Block a user