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 (;;)
loopsif ()
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
- 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);}'
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 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
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