Skip to content

Latest commit

 

History

History
294 lines (212 loc) · 7.79 KB

README_JIT.md

File metadata and controls

294 lines (212 loc) · 7.79 KB

Espruino JIT compiler

This compiler allows Espruino to compile JS code into ARM Thumb code.

Right now this roughly doubles execution speed.

Works:

  • Assignments
  • Maths operators, postfix operators
  • Function calls
  • Member access (with . or [])
  • for (;;) loops
  • if ()
  • i++ / ++i
  • i+=
  • ternary operators
  • ~i/!i/+i/-i
  • Function arguments
  • var/const/let (const/let scoping does not work at the moment)
  • On the whole functions that can't be JITed will produce a message on the console and will be treated as normal functions.
  • Short-circuit execution (&&/||)
  • Array [] and Object {} declarations

Doesn't work:

  • new X(...)
  • Everything not mentioned under Works

Performance:

  • When calling a JIT function, we use existing FunctionCall code to set up args and an execution scope (so args can be passed in)
  • Variables are referenced at the start just once and stored on the stack
    • We could also maybe extend it to allow caching of constant field accesses, for instance 'console.log'
  • Built-in global functions are called directly which is a ton faster, but methods like 'console.log' are not currently
  • Peephole optimisation could still be added (eg. removing push r0, pop r0) but this is the least of our worries
  • Stuff is in place to allow ints to be stored on the stack and converted when needed. This could allow us to keep some vars as ints, but control flow makes this hard
  • When a function is called we load up the address as a 32 bit literal each time. We could maybe have a constant pool or local stub functions?

Possible improvements:

  • We always output a return undefined even if the function has already returned

Testing

Linux

  • Build for Linux USE_JIT=1 DEBUG=1 make
  • Test with ./espruino --test-jit - doesn't do much useful right now
  • CLI test ./espruino -e 'function jit() {"jit";return 123;}'
  • On Linux builds, a file jit.bin is created each time JIT runs. It contains the raw Thumb code.
  • Disassemble binary with arm-none-eabi-objdump -D -Mforce-thumb -b binary -m cortex-m4 jit.bin

You can see what code is created with stuff like:

./espruino -e "E.setFlags({jitDebug:1});trace(function jit() {'jit';return 1+2;})"

./espruino -e 'E.setFlags({jitDebug:1});function jit() {"jit";return "Hello"}'

./espruino -e 'E.setFlags({jitDebug:1});function jit() {"jit";print(42)}'

./espruino -e 'E.setFlags({jitDebug:1});function jit() {"jit";i=5}'

./espruino -e 'E.setFlags({jitDebug:1});function jit() {"jit";if (i<3) print("T"); else print("X");}}'

./espruino -e 'E.setFlags({jitDebug:1});function jit() {"jit";for (i=0;i<5;i=i+1) print(i);}'

Raspberry Pi

The Pi can execute Thumb-2 code (Pi 3 and on only)

  • Just build a normal Pi Binary on the Pi: USE_JIT=1 DEBUG=1 make
  • CLI test ./espruino -e 'function jit() {"jit";print("Hello World");};jit()'
  • This may or may not work - sometimes it does (especially when launched from GDB) but I'm unsure why it's flakey!
  • Dump binary on pi with objdump -D -Mforce-thumb -b binary -m arm jit.bin

Build for an actual device

  • Build for ARM: USE_JIT=1 BOARD=BOARD_NAME RELEASE=1 make flash
  • You can also add CFLAGS+=-DDEBUG_JIT_CALLS=1 to ensure that function names are included in debug info even for a release build
// Enable debug output
E.setFlags({jitDebug:1});


function jit() {'jit';return 1;}
jit()==1

function jit() {'jit';return 1+2+3+4+5;}
jit()==15

function jit() {'jit';return 'Hello';}
jit()=="Hello"

function jit() {'jit';return true;}
jit()==true

var test = "Hello world";
function jit() {'jit';return test;}
jit()=="Hello world";

function t() { print("Hello"); }
function jit() {'jit';t();}
jit(); // prints 'hello'

function jit() {'jit';print(42);}
jit(); // prints 42


function jit() {'jit';print(42);return 123;}
jit()==123 // prints 42, returns 123

function jit() {'jit';return !123;}
jit()==false
function jit() {'jit';return !0;}
jit()==true
function jit() {'jit';return ~0;}
jit()==-1
function jit() {'jit';return -(1);}
jit()==-1
function jit() {'jit';return +"0123";}
jit()==83 // octal!

E.setFlags({jitDebug:1});
function jit(a) {'jit';return a?5:10;}
jit(1)==5
jit(0)==10

function t() { return "Hello"; }
function jit() {'jit'; return t()+" world";}
jit()=="Hello world"

function jit() {'jit';digitalWrite(LED1,1);}
jit(); // LED on


function jit() {'jit';return i++;}
i=0;jit()==0 && i==1

function jit() {'jit';return ++i;}
i=0;jit()==1 && i==1

function jit() {'jit';return i+=" world";}
i="hello";jit()=="hello world" && i=="hello world";

function jit() {'jit';return i-=2;}
i=3;jit()==1 && i==1

function jit() {'jit';i=42;}
jit();i==42

function jit() {'jit';return 1<2;}
jit()==true

function jit() {"jit";if (i<3) print("T"); else print("X");print("--")}
i=2;jit(); // prints T,--
i=5;jit(); // prints X,--


function jit() {"jit";for (i=0;i<5;i=i+1) print(i);}
jit(); // prints 0,1,2,3,4

function jit() {"jit";for (i=0;i<5;i++) print(i);}
jit(); // prints 0,1,2,3,4

function jit() {"jit";for (var i=0;i<5;++i) print(i);}
jit(); // prints 0,1,2,3,4

function jit() {"jit";while (0) {}}
jit();
function jit() {"jit";while (1) return 42;}
jit()==42
function jit() {"jit";while (0) return 0;return 42;}
jit()==42

function jit(i) {"jit";while (i--) print(i);}
jit(5) // prints 4,3,2,1,0

function jit() {"jit";while (i--) j++;}
i=1;j=0;jit();  // ok, does nothing
function jit() {"jit";while (0) print(5); print("Done"); } jit(); // prints 'Done'

function jit() {"jit";do { print(i); } while (i--);}
i=5;jit(); // prints 5,4,3,2,1,0

function nojit() {for (i=0;i<1000;i=i+1);}
function jit() {"jit";for (i=0;i<1000;i=i+1);}
t=getTime();jit();getTime()-t // 0.11 sec
t=getTime();nojit();getTime()-t // 0.28 sec


a = {b:42,c:function(){print("hello",this)}};
function jit() {"jit";return a.b;}
jit()==42
function jit() {"jit";return a["b"];}
jit()==42
function jit() {"jit";a.c();}
jit(); // prints 'hello {b:42,...}'

a=Uint8Array([42])
function jit(){"jit";var i=0;return a[i];}
jit()==42

function jit(a,b) {'jit';return a+"Hello world"+b;}
jit(1,2)=="1Hello world2"

function jit() {'jit';return [1,2,1+2,"Hello","World"];}
jit()=="1,2,3,Hello,World"

function jit() {'jit';return {a:42,b:10,12:5};}
JSON.stringify(jit()) == '{"a":42,"b":10,"12":5}'

E.setFlags({jitDebug:1});
function jit() {'jit';return 0&&2;}
jit()==0
function jit() {'jit';return 3&&2;}
jit()==2
function jit() {'jit';return 0||2;}
jit()==2
function jit() {'jit';return 3||2;}
jit()==3


jit = {a:42, jit:function(){'jit';return this.a;}}
jit.jit()==42

function nojit() {
  for (var i=0;i<10000;i++) {
    digitalWrite(LED,1);
    digitalWrite(LED,0);
  }
}
function jit() {"jit";
  for (var i=0;i<10000;i++) {
    digitalWrite(LED,1);
    digitalWrite(LED,0);
  }
}
t=getTime();nojit();getTime()-t // 6.96
t=getTime();jit();getTime()-t   // 2.02


t=getTime();function jit() {"jit";
  for (var i=0;i<10;i++) {
    print("Start");
    digitalWrite(LED,1);
    digitalWrite(LED,0);
    print("Stop");
  }
};print("JIT compile time", getTime()-t,"s")

Run JIT on ARM and then disassemble:

// on ARM
function jit() {"jit";return 1;}
print(btoa(jit["\xffcod"]))
// prints ASBL8Kz7AbQBvHBH

// On Linux
echo ASBL8Kz7AbQBvHBH | base64 -d  > jit.bin
arm-none-eabi-objdump -D -Mforce-thumb -b binary -m cortex-m4 jit.bin

Seeing what GCC does:

// test.c
void main() {
  int data[400];
  volatile int x = data[1];
}
arm-none-eabi-gcc -Os -mcpu=cortex-m4 -mthumb -mabi=aapcs -mfloat-abi=hard -mfpu=fpv4-sp-d16 -nostartfiles test.c
arm-none-eabi-objdump -D -Mforce-thumb -m cortex-m4 a.out

Useful links

http://www.cs.cornell.edu/courses/cs414/2001FA/armcallconvention.pdf https://developer.arm.com/documentation/ddi0308/d/Thumb-Instructions/Alphabetical-list-of-Thumb-instructions/B https://community.arm.com/arm-community-blogs/b/architectures-and-processors-blog/posts/condition-codes-1-condition-flags-and-codes