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.Node
import prog8.ast.Program
import prog8.ast.base.DataType
import prog8.ast.expressions.BinaryExpression
import prog8.ast.expressions.augmentAssignmentOperators
import prog8.ast.statements.AssignTarget
import prog8.ast.statements.Assignment
import prog8.ast.walk.AstWalker
import prog8.ast.walk.IAstModification
import prog8.compilerinterface.CompilationOptions
import prog8.compilerinterface.ICompilationTarget
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> {
// 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
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,

View File

@ -15,10 +15,21 @@ import kotlin.math.log2
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
*(&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.Program
import prog8.compilerinterface.CompilationOptions
import prog8.compilerinterface.ICompilationTarget
import prog8.compilerinterface.IErrorReporter
@ -59,8 +60,8 @@ fun Program.simplifyExpressions() : Int {
return opti.applyModifications()
}
fun Program.splitBinaryExpressions(compTarget: ICompilationTarget) : Int {
val opti = BinExprSplitter(this, compTarget)
fun Program.splitBinaryExpressions(options: CompilationOptions, compTarget: ICompilationTarget) : Int {
val opti = BinExprSplitter(this, options, compTarget)
opti.visit(this)
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 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 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 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")
@ -75,7 +76,8 @@ private fun compileMain(args: Array<String>): Boolean {
for(filepathRaw in moduleFiles) {
val filepath = pathFrom(filepathRaw).normalize()
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)
results.add(compilationResult)
}
@ -114,7 +116,8 @@ private fun compileMain(args: Array<String>): Boolean {
val compilationResult: CompilationResult
try {
compilationResult = compileProgram(filepath,
dontOptimize!=true, dontWriteAssembly!=true, slowCodegenWarnings==true, quietAssembler==true,
dontOptimize!=true, optimizeFloatExpressions==true,
dontWriteAssembly!=true, slowCodegenWarnings==true, quietAssembler==true,
compilationTarget, srcdirs, outputPath)
if(!compilationResult.success)
return false

View File

@ -11,13 +11,11 @@ import prog8.ast.walk.AstWalker
import prog8.ast.walk.IAstModification
import prog8.ast.walk.IAstVisitor
import prog8.compiler.astprocessing.isSubroutineParameter
import prog8.compilerinterface.InternalCompilerException
import prog8.compilerinterface.ICompilationTarget
import prog8.compilerinterface.IErrorReporter
import prog8.compilerinterface.isInRegularRAMof
import prog8.compilerinterface.*
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> {
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.
if(!assignment.isAugmentable
&& assignment.target.identifier != null
&& assignment.target.isInRegularRAMof(compTarget.machine)) {
&& assignment.target.isInRegularRAMof(options.compTarget.machine)) {
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.left !is BinaryExpression) {
if (binExpr.right.referencesIdentifier(*assignment.target.identifier!!.nameInSource.toTypedArray())) {

View File

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

View File

@ -15,9 +15,8 @@ import java.util.*
import kotlin.io.path.Path
internal class AstChecker(private val program: Program,
private val compilerOptions: CompilationOptions,
private val errors: IErrorReporter,
private val compTarget: ICompilationTarget
private val compilerOptions: CompilationOptions
) : IAstVisitor {
override fun visit(program: Program) {
@ -773,7 +772,7 @@ internal class AstChecker(private val program: Program,
override fun visit(char: CharLiteral) {
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) {
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)
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) {
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
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 true
}

View File

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

View File

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

View File

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

View File

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

View File

@ -28,4 +28,5 @@ data class CompilationOptions(val output: OutputType,
) {
var slowCodegenWarnings = 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.
(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
------------------------

View File

@ -1,24 +1,10 @@
TODO
====
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?
- fix the asm-labels problem (github issue #62)
@ -30,6 +16,7 @@ Blocked by Commander-x16 v39 release
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
- improve testability further, add more tests
- 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 compilationResult = compileProgram(Path.of(a),
optimize = true,
optimizeFloatExpressions = false,
writeAssembly = true,
slowCodegenWarnings = true,
compilationTarget = "c64",