add command line option 'optfloatx' to explicitly re-enable float expr optimization as this can increase code size significantly.

The output size of the various example programs using floating point, when not using this optimization, has been reduced significantly.
The resulting code runs a (tiny) bit slower though.
This commit is contained in:
Irmen de Jong 2021-11-03 22:52:08 +01:00
parent b4fa72c058
commit eea3fb48a8
15 changed files with 66 additions and 44 deletions

View File

@ -3,17 +3,19 @@ package prog8.optimizer
import prog8.ast.IStatementContainer import prog8.ast.IStatementContainer
import prog8.ast.Node import prog8.ast.Node
import prog8.ast.Program import prog8.ast.Program
import prog8.ast.base.DataType
import prog8.ast.expressions.BinaryExpression import prog8.ast.expressions.BinaryExpression
import prog8.ast.expressions.augmentAssignmentOperators import prog8.ast.expressions.augmentAssignmentOperators
import prog8.ast.statements.AssignTarget import prog8.ast.statements.AssignTarget
import prog8.ast.statements.Assignment import prog8.ast.statements.Assignment
import prog8.ast.walk.AstWalker import prog8.ast.walk.AstWalker
import prog8.ast.walk.IAstModification import prog8.ast.walk.IAstModification
import prog8.compilerinterface.CompilationOptions
import prog8.compilerinterface.ICompilationTarget import prog8.compilerinterface.ICompilationTarget
import prog8.compilerinterface.isInRegularRAMof import prog8.compilerinterface.isInRegularRAMof
class BinExprSplitter(private val program: Program, private val compTarget: ICompilationTarget) : AstWalker() { class BinExprSplitter(private val program: Program, private val options: CompilationOptions, private val compTarget: ICompilationTarget) : AstWalker() {
// override fun after(decl: VarDecl, parent: Node): Iterable<IAstModification> { // override fun after(decl: VarDecl, parent: Node): Iterable<IAstModification> {
// TODO somehow if we do this, the resulting code for some programs (cube3d.p8) gets hundreds of bytes larger...: // TODO somehow if we do this, the resulting code for some programs (cube3d.p8) gets hundreds of bytes larger...:
@ -38,6 +40,11 @@ class BinExprSplitter(private val program: Program, private val compTarget: ICom
val binExpr = assignment.value as? BinaryExpression val binExpr = assignment.value as? BinaryExpression
if (binExpr != null) { if (binExpr != null) {
if(binExpr.inferType(program).istype(DataType.FLOAT) && !options.optimizeFloatExpressions)
return noModifications
/* /*
Reduce the complexity of a (binary) expression that has to be evaluated on the eval stack, Reduce the complexity of a (binary) expression that has to be evaluated on the eval stack,

View File

@ -15,10 +15,21 @@ import kotlin.math.log2
import kotlin.math.pow import kotlin.math.pow
/* /*
todo add more expression optimizations todo add more peephole expression optimizations
Investigate what optimizations binaryen has, also see https://egorbo.com/peephole-optimizations.html Investigate what optimizations binaryen has, also see https://egorbo.com/peephole-optimizations.html
*(&X) => X
X % 1 => 0
X / 1 => X
X ^ -1 => ~x
X >= 1 => X > 0
X < 1 => X <= 0
X + С1 == C2 => X == C2 - C1
((X + C1) + C2) => (X + (C1 + C2))
((X + C1) + (Y + C2)) => ((X + Y) + (C1 + C2))
*/ */

View File

@ -2,6 +2,7 @@ package prog8.optimizer
import prog8.ast.IBuiltinFunctions import prog8.ast.IBuiltinFunctions
import prog8.ast.Program import prog8.ast.Program
import prog8.compilerinterface.CompilationOptions
import prog8.compilerinterface.ICompilationTarget import prog8.compilerinterface.ICompilationTarget
import prog8.compilerinterface.IErrorReporter import prog8.compilerinterface.IErrorReporter
@ -59,8 +60,8 @@ fun Program.simplifyExpressions() : Int {
return opti.applyModifications() return opti.applyModifications()
} }
fun Program.splitBinaryExpressions(compTarget: ICompilationTarget) : Int { fun Program.splitBinaryExpressions(options: CompilationOptions, compTarget: ICompilationTarget) : Int {
val opti = BinExprSplitter(this, compTarget) val opti = BinExprSplitter(this, options, compTarget)
opti.visit(this) opti.visit(this)
return opti.applyModifications() return opti.applyModifications()
} }

View File

@ -35,6 +35,7 @@ private fun compileMain(args: Array<String>): Boolean {
val outputDir by cli.option(ArgType.String, fullName = "out", description = "directory for output files instead of current directory").default(".") val outputDir by cli.option(ArgType.String, fullName = "out", description = "directory for output files instead of current directory").default(".")
val dontWriteAssembly by cli.option(ArgType.Boolean, fullName = "noasm", description="don't create assembly code") val dontWriteAssembly by cli.option(ArgType.Boolean, fullName = "noasm", description="don't create assembly code")
val dontOptimize by cli.option(ArgType.Boolean, fullName = "noopt", description = "don't perform any optimizations") val dontOptimize by cli.option(ArgType.Boolean, fullName = "noopt", description = "don't perform any optimizations")
val optimizeFloatExpressions by cli.option(ArgType.Boolean, fullName = "optfloatx", description = "optimize float expressions (warning: can increase program size)")
val watchMode by cli.option(ArgType.Boolean, fullName = "watch", description = "continuous compilation mode (watches for file changes), greatly increases compilation speed") val watchMode by cli.option(ArgType.Boolean, fullName = "watch", description = "continuous compilation mode (watches for file changes), greatly increases compilation speed")
val slowCodegenWarnings by cli.option(ArgType.Boolean, fullName = "slowwarn", description="show debug warnings about slow/problematic assembly code generation") val slowCodegenWarnings by cli.option(ArgType.Boolean, fullName = "slowwarn", description="show debug warnings about slow/problematic assembly code generation")
val quietAssembler by cli.option(ArgType.Boolean, fullName = "quietasm", description = "don't print assembler output results") val quietAssembler by cli.option(ArgType.Boolean, fullName = "quietasm", description = "don't print assembler output results")
@ -75,7 +76,8 @@ private fun compileMain(args: Array<String>): Boolean {
for(filepathRaw in moduleFiles) { for(filepathRaw in moduleFiles) {
val filepath = pathFrom(filepathRaw).normalize() val filepath = pathFrom(filepathRaw).normalize()
val compilationResult = compileProgram(filepath, val compilationResult = compileProgram(filepath,
dontOptimize!=true, dontWriteAssembly!=true, slowCodegenWarnings==true, quietAssembler==true, dontOptimize!=true, optimizeFloatExpressions==true,
dontWriteAssembly!=true, slowCodegenWarnings==true, quietAssembler==true,
compilationTarget, srcdirs, outputPath) compilationTarget, srcdirs, outputPath)
results.add(compilationResult) results.add(compilationResult)
} }
@ -114,7 +116,8 @@ private fun compileMain(args: Array<String>): Boolean {
val compilationResult: CompilationResult val compilationResult: CompilationResult
try { try {
compilationResult = compileProgram(filepath, compilationResult = compileProgram(filepath,
dontOptimize!=true, dontWriteAssembly!=true, slowCodegenWarnings==true, quietAssembler==true, dontOptimize!=true, optimizeFloatExpressions==true,
dontWriteAssembly!=true, slowCodegenWarnings==true, quietAssembler==true,
compilationTarget, srcdirs, outputPath) compilationTarget, srcdirs, outputPath)
if(!compilationResult.success) if(!compilationResult.success)
return false return false

View File

@ -11,13 +11,11 @@ import prog8.ast.walk.AstWalker
import prog8.ast.walk.IAstModification import prog8.ast.walk.IAstModification
import prog8.ast.walk.IAstVisitor import prog8.ast.walk.IAstVisitor
import prog8.compiler.astprocessing.isSubroutineParameter import prog8.compiler.astprocessing.isSubroutineParameter
import prog8.compilerinterface.InternalCompilerException import prog8.compilerinterface.*
import prog8.compilerinterface.ICompilationTarget
import prog8.compilerinterface.IErrorReporter
import prog8.compilerinterface.isInRegularRAMof
internal class BeforeAsmGenerationAstChanger(val program: Program, val errors: IErrorReporter, private val compTarget: ICompilationTarget) : AstWalker() { internal class BeforeAsmGenerationAstChanger(val program: Program, private val options: CompilationOptions,
private val errors: IErrorReporter) : AstWalker() {
override fun after(decl: VarDecl, parent: Node): Iterable<IAstModification> { override fun after(decl: VarDecl, parent: Node): Iterable<IAstModification> {
if(decl.type==VarDeclType.VAR && decl.value != null && decl.datatype in NumericDatatypes) if(decl.type==VarDeclType.VAR && decl.value != null && decl.datatype in NumericDatatypes)
@ -33,8 +31,12 @@ internal class BeforeAsmGenerationAstChanger(val program: Program, val errors: I
// But it can only be done if the target variable IS NOT OCCURRING AS AN OPERAND ITSELF. // But it can only be done if the target variable IS NOT OCCURRING AS AN OPERAND ITSELF.
if(!assignment.isAugmentable if(!assignment.isAugmentable
&& assignment.target.identifier != null && assignment.target.identifier != null
&& assignment.target.isInRegularRAMof(compTarget.machine)) { && assignment.target.isInRegularRAMof(options.compTarget.machine)) {
val binExpr = assignment.value as? BinaryExpression val binExpr = assignment.value as? BinaryExpression
if(binExpr!=null && binExpr.inferType(program).istype(DataType.FLOAT) && !options.optimizeFloatExpressions)
return noModifications
if (binExpr != null && binExpr.operator !in comparisonOperators) { if (binExpr != null && binExpr.operator !in comparisonOperators) {
if (binExpr.left !is BinaryExpression) { if (binExpr.left !is BinaryExpression) {
if (binExpr.right.referencesIdentifier(*assignment.target.identifier!!.nameInSource.toTypedArray())) { if (binExpr.right.referencesIdentifier(*assignment.target.identifier!!.nameInSource.toTypedArray())) {

View File

@ -29,8 +29,10 @@ class CompilationResult(val success: Boolean,
val importedFiles: List<Path>) val importedFiles: List<Path>)
// TODO refactor the gigantic list of parameters
fun compileProgram(filepath: Path, fun compileProgram(filepath: Path,
optimize: Boolean, optimize: Boolean,
optimizeFloatExpressions: Boolean,
writeAssembly: Boolean, writeAssembly: Boolean,
slowCodegenWarnings: Boolean, slowCodegenWarnings: Boolean,
quietAssembler: Boolean, quietAssembler: Boolean,
@ -53,14 +55,18 @@ fun compileProgram(filepath: Path,
val totalTime = measureTimeMillis { val totalTime = measureTimeMillis {
// import main module and everything it needs // import main module and everything it needs
val (programresult, compilationOptions, imported) = parseImports(filepath, errors, compTarget, sourceDirs) val (programresult, compilationOptions, imported) = parseImports(filepath, errors, compTarget, sourceDirs)
compilationOptions.slowCodegenWarnings = slowCodegenWarnings with(compilationOptions) {
compilationOptions.optimize = optimize this.slowCodegenWarnings = slowCodegenWarnings
this.optimize = optimize
this.optimizeFloatExpressions = optimizeFloatExpressions
}
program = programresult program = programresult
importedFiles = imported importedFiles = imported
processAst(program, errors, compilationOptions) processAst(program, errors, compilationOptions)
if (compilationOptions.optimize) if (compilationOptions.optimize)
optimizeAst( optimizeAst(
program, program,
compilationOptions,
errors, errors,
BuiltinFunctionsFacade(BuiltinFunctions), BuiltinFunctionsFacade(BuiltinFunctions),
compTarget compTarget
@ -262,13 +268,13 @@ private fun processAst(program: Program, errors: IErrorReporter, compilerOptions
errors.report() errors.report()
program.variousCleanups(program, errors) program.variousCleanups(program, errors)
errors.report() errors.report()
program.checkValid(compilerOptions, errors, compilerOptions.compTarget) program.checkValid(errors, compilerOptions)
errors.report() errors.report()
program.checkIdentifiers(errors, compilerOptions) program.checkIdentifiers(errors, compilerOptions)
errors.report() errors.report()
} }
private fun optimizeAst(program: Program, errors: IErrorReporter, functions: IBuiltinFunctions, compTarget: ICompilationTarget) { private fun optimizeAst(program: Program, compilerOptions: CompilationOptions, errors: IErrorReporter, functions: IBuiltinFunctions, compTarget: ICompilationTarget) {
// optimize the parse tree // optimize the parse tree
println("Optimizing...") println("Optimizing...")
@ -279,7 +285,7 @@ private fun optimizeAst(program: Program, errors: IErrorReporter, functions: IBu
while (true) { while (true) {
// keep optimizing expressions and statements until no more steps remain // keep optimizing expressions and statements until no more steps remain
val optsDone1 = program.simplifyExpressions() val optsDone1 = program.simplifyExpressions()
val optsDone2 = program.splitBinaryExpressions(compTarget) val optsDone2 = program.splitBinaryExpressions(compilerOptions, compTarget)
val optsDone3 = program.optimizeStatements(errors, functions, compTarget) val optsDone3 = program.optimizeStatements(errors, functions, compTarget)
program.constantFold(errors, compTarget) // because simplified statements and expressions can result in more constants that can be folded away program.constantFold(errors, compTarget) // because simplified statements and expressions can result in more constants that can be folded away
errors.report() errors.report()
@ -294,7 +300,7 @@ private fun postprocessAst(program: Program, errors: IErrorReporter, compilerOpt
program.addTypecasts(errors) program.addTypecasts(errors)
errors.report() errors.report()
program.variousCleanups(program, errors) program.variousCleanups(program, errors)
program.checkValid(compilerOptions, errors, compilerOptions.compTarget) // check if final tree is still valid program.checkValid(errors, compilerOptions) // check if final tree is still valid
errors.report() errors.report()
val callGraph = CallGraph(program) val callGraph = CallGraph(program)
callGraph.checkRecursiveCalls(errors) callGraph.checkRecursiveCalls(errors)
@ -315,7 +321,7 @@ private fun writeAssembly(program: Program,
compilerOptions: CompilationOptions compilerOptions: CompilationOptions
): WriteAssemblyResult { ): WriteAssemblyResult {
// asm generation directly from the Ast // asm generation directly from the Ast
program.processAstBeforeAsmGeneration(errors, compilerOptions.compTarget) program.processAstBeforeAsmGeneration(compilerOptions, errors)
errors.report() errors.report()
// printAst(program) // printAst(program)

View File

@ -15,9 +15,8 @@ import java.util.*
import kotlin.io.path.Path import kotlin.io.path.Path
internal class AstChecker(private val program: Program, internal class AstChecker(private val program: Program,
private val compilerOptions: CompilationOptions,
private val errors: IErrorReporter, private val errors: IErrorReporter,
private val compTarget: ICompilationTarget private val compilerOptions: CompilationOptions
) : IAstVisitor { ) : IAstVisitor {
override fun visit(program: Program) { override fun visit(program: Program) {
@ -773,7 +772,7 @@ internal class AstChecker(private val program: Program,
override fun visit(char: CharLiteral) { override fun visit(char: CharLiteral) {
try { // just *try* if it can be encoded, don't actually do it try { // just *try* if it can be encoded, don't actually do it
compTarget.encodeString(char.value.toString(), char.altEncoding) compilerOptions.compTarget.encodeString(char.value.toString(), char.altEncoding)
} catch (cx: CharConversionException) { } catch (cx: CharConversionException) {
errors.err(cx.message ?: "can't encode character", char.position) errors.err(cx.message ?: "can't encode character", char.position)
} }
@ -785,7 +784,7 @@ internal class AstChecker(private val program: Program,
checkValueTypeAndRangeString(DataType.STR, string) checkValueTypeAndRangeString(DataType.STR, string)
try { // just *try* if it can be encoded, don't actually do it try { // just *try* if it can be encoded, don't actually do it
compTarget.encodeString(string.value, string.altEncoding) compilerOptions.compTarget.encodeString(string.value, string.altEncoding)
} catch (cx: CharConversionException) { } catch (cx: CharConversionException) {
errors.err(cx.message ?: "can't encode string", string.position) errors.err(cx.message ?: "can't encode string", string.position)
} }
@ -1259,7 +1258,7 @@ internal class AstChecker(private val program: Program,
// check if the floating point values are all within range // check if the floating point values are all within range
val doubles = value.value.map {it.constValue(program)?.number!!.toDouble()}.toDoubleArray() val doubles = value.value.map {it.constValue(program)?.number!!.toDouble()}.toDoubleArray()
if(doubles.any { it < compTarget.machine.FLOAT_MAX_NEGATIVE || it > compTarget.machine.FLOAT_MAX_POSITIVE }) if(doubles.any { it < compilerOptions.compTarget.machine.FLOAT_MAX_NEGATIVE || it > compilerOptions.compTarget.machine.FLOAT_MAX_POSITIVE })
return err("floating point value overflow") return err("floating point value overflow")
return true return true
} }

View File

@ -11,18 +11,17 @@ import prog8.ast.walk.AstWalker
import prog8.ast.walk.IAstModification import prog8.ast.walk.IAstModification
import prog8.compiler.BeforeAsmGenerationAstChanger import prog8.compiler.BeforeAsmGenerationAstChanger
import prog8.compilerinterface.CompilationOptions import prog8.compilerinterface.CompilationOptions
import prog8.compilerinterface.ICompilationTarget
import prog8.compilerinterface.IErrorReporter import prog8.compilerinterface.IErrorReporter
import prog8.compilerinterface.IStringEncoding import prog8.compilerinterface.IStringEncoding
internal fun Program.checkValid(compilerOptions: CompilationOptions, errors: IErrorReporter, compTarget: ICompilationTarget) { internal fun Program.checkValid(errors: IErrorReporter, compilerOptions: CompilationOptions) {
val checker = AstChecker(this, compilerOptions, errors, compTarget) val checker = AstChecker(this, errors, compilerOptions)
checker.visit(this) checker.visit(this)
} }
internal fun Program.processAstBeforeAsmGeneration(errors: IErrorReporter, compTarget: ICompilationTarget) { internal fun Program.processAstBeforeAsmGeneration(compilerOptions: CompilationOptions, errors: IErrorReporter) {
val fixer = BeforeAsmGenerationAstChanger(this, errors, compTarget) val fixer = BeforeAsmGenerationAstChanger(this, compilerOptions, errors)
fixer.visit(this) fixer.visit(this)
while(errors.noErrors() && fixer.applyModifications()>0) { while(errors.noErrors() && fixer.applyModifications()>0) {
fixer.visit(this) fixer.visit(this)

View File

@ -42,6 +42,7 @@ class TestCompilerOnExamples {
compileProgram( compileProgram(
filepath, filepath,
optimize, optimize,
optimizeFloatExpressions = false,
writeAssembly = true, writeAssembly = true,
slowCodegenWarnings = false, slowCodegenWarnings = false,
quietAssembler = true, quietAssembler = true,

View File

@ -47,6 +47,7 @@ class TestCompilerOptionSourcedirs {
compileProgram( compileProgram(
filepath = filePath, filepath = filePath,
optimize = false, optimize = false,
optimizeFloatExpressions = false,
writeAssembly = true, writeAssembly = true,
slowCodegenWarnings = false, slowCodegenWarnings = false,
quietAssembler = true, quietAssembler = true,

View File

@ -40,6 +40,7 @@ internal fun compileFile(
return compileProgram( return compileProgram(
filepath, filepath,
optimize, optimize,
optimizeFloatExpressions = false,
writeAssembly = writeAssembly, writeAssembly = writeAssembly,
slowCodegenWarnings = false, slowCodegenWarnings = false,
quietAssembler = true, quietAssembler = true,

View File

@ -28,4 +28,5 @@ data class CompilationOptions(val output: OutputType,
) { ) {
var slowCodegenWarnings = false var slowCodegenWarnings = false
var optimize = false var optimize = false
var optimizeFloatExpressions = false
} }

View File

@ -104,6 +104,8 @@ Also it is possible to specify more than one main module to compile:
this can be useful to quickly recompile multiple separate programs quickly. this can be useful to quickly recompile multiple separate programs quickly.
(compiling in a batch like this is a lot faster than invoking the compiler again once per main file) (compiling in a batch like this is a lot faster than invoking the compiler again once per main file)
A short list and explanation of the options is printed if you launch the compiler with ``-h`` or ``-help``.
Module source code files Module source code files
------------------------ ------------------------

View File

@ -1,24 +1,10 @@
TODO TODO
==== ====
For next compiler release (7.2) For next compiler release (7.2)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* FIX CRASH BUG *
when compiling petaxian: (attack.p8)
Exception in thread "main" prog8.ast.base.FatalAstException: vardecls for variables, with initial numerical value, should have been rewritten as plain vardecl + assignment VarDecl(name=attack_num, vartype=VAR, datatype=UBYTE, value=DirectMemoryRead([IdentifierRef([eRef]) + NumericLiteral(UWORD:11)]), pos=[attack.p8: line 72 col 5-45])
at prog8.compiler.BeforeAsmGenerationAstChanger.after(BeforeAsmGenerationAstChanger.kt:24)
at prog8.ast.walk.AstWalker.visit(AstWalker.kt:239)
at prog8.ast.statements.VarDecl.accept(AstStatements.kt:248)
- analyze (and fix?): TODO why are these bigger now than before the var-initializer optimization:
; cube3d-float (THIS ONE IS A LOT BIGGER!!)
- fix the asm-labels problem (github issue #62)
- find a way to optimize asm-subroutine param passing where it now sometimes uses the evalstack? - find a way to optimize asm-subroutine param passing where it now sometimes uses the evalstack?
- fix the asm-labels problem (github issue #62)
@ -30,6 +16,7 @@ Blocked by Commander-x16 v39 release
Future Future
^^^^^^ ^^^^^^
- document the various compiler command line options in more detail. See "Compiling program code" in the docs
- get rid of all TODO's in the code - get rid of all TODO's in the code
- improve testability further, add more tests - improve testability further, add more tests
- replace certain uses of inferredType.getOr(DataType.UNDEFINED) by i.getOrElse({ errorhandler }) - replace certain uses of inferredType.getOr(DataType.UNDEFINED) by i.getOrElse({ errorhandler })

View File

@ -31,6 +31,7 @@ class RequestParser : Take {
val a = form.param("a").single() val a = form.param("a").single()
val compilationResult = compileProgram(Path.of(a), val compilationResult = compileProgram(Path.of(a),
optimize = true, optimize = true,
optimizeFloatExpressions = false,
writeAssembly = true, writeAssembly = true,
slowCodegenWarnings = true, slowCodegenWarnings = true,
compilationTarget = "c64", compilationTarget = "c64",