Optimizing Monkey C code

I've been working on a data field for a while, and one of the hardest things to deal with is code size, especially if you want your code to work on devices like the fr235, which only allows 16k for data fields.

Some of my observations:

  • consts and enums have code size cost, even if you don't use them (i.e just declaring an enum will reduce the memory available).
  • Using consts and enums generates more code than using the corresponding constants directly - especially if you follow the lead of the garmin samples and fully qualify their names (ie $.Foo.Bar.Baz, when Bar.Baz would have pinned it down). Fully qualifying probably reduces the runtime overhead, but it definitely generates more code.
  • The monkeyc compiler doesn't do constant folding at all. So const FOO = 1+1; generates more code than const FOO = 2;
  • Initializing arrays, especially if you initialize them with named constants, can take a lot of code. eg an array of heart-rate-zone colors [Graphics.COLOR_LT_GREY, Graphics.COLOR_BLUE, Graphics.COLOR_GREEN, Graphics.COLOR_YELLOW, Graphics.COLOR_RED] takes hundreds of bytes. If you can get away with 6 bit colors (for this particular array that's not a problem, and no mip device needs more than 6 bit color) you can pack 5 6-bit words into a Number, and then extract them on the fly. Even though the extraction code is a little complex, doing so saved me more than 350 bytes in this case

The downside of trying to work around all the above is that now your code is littered with meaningless numbers. So I started looking at a preprocessing step so I could keep the consts and enums, but get the benefits of using the literals directly. An add hoc regex based approach shaved 300 bytes off my data field, but I could see there were more wins to be had - eg constant folding after substituting in values could save more code; removing unused enums (including enums that *became* unused as a result of the preprocessing step) could save more.

I'd recently written a monkey-c code formatter, as a plugin for Prettier, so I could use that to produce an AST, modify the AST, and then use Prettier to print out the resulting code, and then build that. The result is the latest version of Prettier-extension-monkeyc. Using that takes about 1k off my data field's peak memory, which is *huge* for a 16k data field.

The extension is pretty much plug-and-play. Install it, and you get new commands in the command palette. "Prettier Monkey C: Build and Run Optimized Project", and "Prettier Monkey C: Export Optimized Project" are probably the most useful commands. If you just want to look at the code changes, "Prettier Monkey C: Generate Optimized Project" will just generate the optimized sources in bin/optimized for you to inspect.

Currently, barrels are not supported, but anything else that doesn't work is a bug.

I'd appreciate any feedback!

  • One thing that was done back when all devices had 16k for a DF was avoid doing layouts and stick with direct dc calls.

    When you use a barrel on any devices, there about 1k overhead.  Use jungles to include the code, but don't use the .barrel file.

  • I'd seen a lot of discussion about layouts etc (much of advice from you), so just wanted to focus on things I hadn't seen discussed, and that I could actually do something about with source transformations.

    But yes, more advice would be:

    • Don't use classes if you can help it, they have a lot of overhead; prefer Dictionaries, but
    • Don't use Dictionaries, they have a lot of overhead vs Arrays with named constant keys (especially if you use my extension to kill the constants).
    • Keep layout info in JsonData, or for older devices in String resources, so that only the current layout takes up space; and if you stick to flat arrays of Strings and Numbers, its more memory efficient than nested data structures, and much easier to parse from Strings (just a string split, followed by toNumber() on some of the elements of the resulting array).

    I've not tried to use barrels yet, so thats not been a problem for me... but good to know.

  • Can you give examples for arrays with named constants?

    Let's say you constants are: KEY1 = 1, KEY3 = 3, KEY8 = 8, data = [KEY1: "a", KEY3: "b", KEY8: "c"]. How would you initialize the array? (note, I intentionally gave you keys with "holes" between them)

    What do you mean on "layout info in JsonData"?

  • I guess I meant "if you can help it", as in the previous line. But my point is that { KEY1 => "a", KEY3 => "b", KEY8 => "c" } is considerably more expensive (in code size, and probably runtime) than { 1 => "a", 3 => b, 8 => "c" }. And if there's some way to reorganize your code so you can use arrays (eg by changing to KEY1=0, KEY3=1, KEY8=2), then ["a", "b", "c"] is much cheaper again, even if you then access it as array[KEY1] (again, accessing it as array[0] would be even cheaper - but thats something my extension can do for you automatically).

  • The best time to optimize for both size and performance is when you are first writing the app.  As you understand more of what's going on, you may find you replace big hunks of code.  (some of my apps lhave far different code than they did when I first wrote them in 2015)

    My main example is the use of has.  It's expensive performance wise.  It's much "cheaper" to set a Boolean and check that when needed,  But it costs the memory of a Boolean.  There are also trade offs.

  • Yes, no argument there. The app I'm currently working on was up to ~48k peak memory before I realized that memory was even an issue. By redesigning things and being very very careful with memory I've got it down to below 16k while adding more features.

    But without giving up on readability, I was pretty much stuck at that point. And thats what this extension is for. I can throw in as many named constants as I like, and use them freely, and the extension will remove them all at build time.

  • I just posted a new version that optimizes separately for debug/release builds. This means that you can do things like:

    (:release)
    const DEBUG = false;
    (:debug)
    const DEBUG = true;
    function doSomething() {
      if (DEBUG) {
        System.println("This will be completely optimized away in release builds");
      }
    }

    and pay no overhead at all in release builds (the whole if is removed before the build starts).