Skip to content

Type and Dataflow analysis

markw65 edited this page Jun 29, 2024 · 4 revisions

The optimizer can perform type and dataflow analysis.

These optimizations are controlled by the options trustDeclaredTypes, propagateTypes and sizeBasedPRE.

  • trustDeclaredTypes - If this option is set, then any declared types will be used to augment the analysis. This can enable more precise optimizations - but can produce incorrect results if your type declarations are inaccurate.

  • propagateTypes - If this option is set, then the compiler will perform data flow analysis on the types. Without this, local variables won't get type information.

  • sizeBasedPRE - If this option is set, then the compiler will perform data flow analysis to find values that are used repeatedly within a function, and put the value into a local variable to save code size.

Trust Declared Types

Given:

function foo(x as Number) as Number {
  return x * 2;
}

function bar() as Number {
  return foo(1) + 3 + 2;
}

If the option is enabled, then the compiler knows that foo returns a number, and can re-order bar's return expression to foo(1) + 5. When its disabled, foo might return a string, so it can't optimize the result (if foo returned "foo", the result would be "foo32", rather than "foo5" in this case).

Propagate Types

Given:

var x = 0;
if (x != 0) {
  System.println("X is not zero");
}

When propagateTypes is enabled, the compiler knows that the x in if (x != 0) is zero, and can optimize the test away. When it's disabled, it knows nothing about the condition, and can't optimize it away.

Note that in this context, types can include what would normally be thought of as values. The type 0 is just a more specialized instance of Number; just as Array<Number> is a more specialized instance of Array.

A downside of propagateTypes is that it can increase code size. For example:

var x = 0;
foo(x, x, x);

will be optimized to

foo(0, 0, 0);

but each of those zero's takes 5 bytes to produce, while each x only takes 2. The assignment to x also takes 2 bytes, so the original code takes 5 (for the zero) + 2 (assignment to x) + 2*3 = 13 bytes (plus the size of the call), while the modified code uses 5*3 = 15 bytes.

And this is where sizeBasedPRE comes in.

Size Based PRE

Regardless of how the code was originally written, when this option is enabled the compiler will take

foo(0, 0, 0)

and produce

var tmp=0;
foo(tmp, tmp, tmp)

Similarly, it will take multiple uses of a module or class scope variable, and put the value in a local:

var mGlobal as Number = 0;
function foo() as Number {
  System.println(mGlobal);
  return mGlobal + 1;
}
var mGlobal as Number = 0;
function foo() as Number {
    var pre_mGlobal;
    pre_mGlobal = mGlobal;
    System.println(pre_mGlobal);
    return pre_mGlobal + 1;
}

Size Based PRE Skip Literals

In some cases, Garmin's type checker will report an error if a local variable with a known value is used in place of a literal with the known value.

eg

function foo(x as [Number, Boolean]) as Void {
  var a = x[0] + 1; // Ok, Garmin's type checker knows that x[0] contains a Number
  var b = 0;
  var c = x[b] + 1; // Not Ok, Garmin's type checker thinks that x[b] is either a Number or a Boolean
}

Unfortunately, sizeBasedPRE can introduce this pattern. This is really a Garmin problem, and one way to deal with it is probably to disable Garmin's type checker on the optimized code. If you're using the post build optimizer, however, it also does sizeBasedPRE for literals, so another workaround is to turn off sizedBasedPRE for literals in the source to source optimizer.

That's the purpose of the preSkipLiterals flag.

Clone this wiki locally