Espruino speed improvements - compiled JavaScript?

Posted on
Page
of 3
/ 3
Last Next
  • Not sure if anyone's interested, but I came up with a way of doing a kind of 'JIT' compilation of JavaScript inside the Web IDE (it work the same way inline assembler does at the moment).

    I posted an 'issue' and a very hacky branch for this up on GitHub. It's not usable at the moment but I'd be interested to see what everyone thinks about it:

    https://github.com/espruino/EspruinoTools/issues/4

    I thought it'd be a nightmare, but it looks like it'll be possible to get a useful subset of JavaScript compiled quite easily.

  • Sounds great, I hope I may be able to give it a try during the weekend!

  • Oh cool!

    Very interested to see how that goes. I think it would be key to figure out what sort of JS ought to be compiled like that, and which stuff shouldn't be. There'd need to be some way to tell it to compile or not compile a piece of code.

  • If the first statement is a string saying "compiled", it'll have a go at it. It's pretty basic :)

    I got a bit carried away... If you now do:

    function a(b) { "compiled";return b+1; }
    

    it's compiled to:

    var a = E.asm("JsVar (JsVar)",
      "  ldr r0, const_0",
      "  bl jspeiFindInScopes",
      "  push {r0}",
      "  ldr r0, const_1",
      "  bl jsvNewFromInteger",
      "  push {r0}",
      "  pop {r0}",
      "  pop {r1}",
      "  movs r2, #43",
      "  bl jsvMathsOp",
      "  push {r0}",
      "  pop {r0}",
      "  bx lr",
      "jspeiFindInScopes:",
      "jsvNewFromInteger:",
      "jsvNewFromFloat:",
      "jsvNewFromString:",
      "jsvMathsOp:",
      "const_0:",
      "  .word 0x62",
      "const_1:",
      "  .word 0x1");
    

    which is then assembled to:

    var ASM_BASE=process.memory().stackEndAddress;
    var ASM_BASE1=ASM_BASE+1/*thumb*/;
    [0x4807,0xf000,0xf80d,0xb401,0x4806,0xf000,0xf809,0xb401,0xbc01,0xbc02,0x222b,0xf000,0xf803,0xb401,0xbc01,0x4770,0x62,0x0,0x1,0x0].forEach(function(v) { poke16((ASM_BASE+=2)-2,v); });
    var a = E.nativeCall(ASM_BASE1, "JsVar (JsVar)")
    

    It still needs a lot of work - like getting the function pointers for jsvNewFromInteger, and also being aware of which variables are parameters/local, but actually it's very promising indeed.

  • This is super exciting. The Espruino has always been a hybrid of native code and JS, but this makes the native code side much more accessible, and gives us a way to address the cases where speed actually limits us on the Espruino.

    Where does it put the compiled code? It looks like... just after the end of the stack. Isn't that where the last JSVars go? It sounds like you'd need to also reduce the number of jsvars by ceil((ASM_BASE+/16) when the compiled code is uploaded, otherwise, if it uses the last jsvar, it'll trash the compiled code. Also raises the question of how to save() these, though that might not be hard if they're stored in the same area as jsvars.

  • This is really cool... since you know the guts, interfacing between compiled and interpreted can be very optimal (and programmer transparent). It is like REXX (on main frames and os/2) did as well as IBM's/OTI's Smalltalk... which compiled to the actual hardware and hid it within the actual file as property or loaded only the JIT stuff. Since JIT on espruino is not an option, why not doing it by the IDE on upload onto the board... I hope that referencing still goes through the JS-specific 'very late' binding... than there is no reason why js and compile js can not work in coexistence...

  • At the moment the assembler is at the end of the stack (it grows down) - so adding more code reduces the amount of recursion - but not the amount of variables.

    In 1v72 I actually added the ability to have continuous blocks of memory via ArrayBuffers, so putting assembler into the variable space would be fine now, and would allow everything to be saved. It's something I'll hopefully add soon.

  • Another update. It now actually does stuff...

    function a(b) { 
      "compiled";
      return b+"World"; 
    }
    console.log(a("Hello "))
    

    Compiles to:

    var ASM_BASE=process.memory().stackEndAddres­s;
    var ASM_BASE1=ASM_BASE+1/*thumb*/;
    [0xb580,0xb401,0x9800,0xb401,0xa00b,0xbf00,0x4f09,0x697f,0xf000,0xf80e,0xb401,0x222b,0xbc02,0xbc01,0x4f05,0x68bf,0xf000,0xf806,0xb401,0xbc01,0xe7ff,0xbc08,0xbd80,0x4770,0x4738,0xbf00,0x74,0x2000,0x6f57,0x6c72,0x64,0x0].forEach(function(v) { poke16((ASM_BASE+=2)-2,v); });
    
    var a = E.nativeCall(ASM_BASE1, "JsVar (JsVar)")
    console.log(a("Hello "))
    

    Which does:

     _____                 _
    |   __|___ ___ ___ _ _|_|___ ___
    |   __|_ -| . |  _| | | |   | . |
    |_____|___|  _|_| |___|_|_|_|___|
              |_| http://espruino.com
     1v72 Copyright 2014 G.Williams
    >echo(0);
    Hello World
    > 
    

    I doubt I'll get much more time to work on it over christmas, but it's looking very promising indeed. Needs a lot of tidying, and I've got to add calls to jsvUnLock and fake a 'frame pointer', but then I can start looking at things like control flow.

  • arr = new Uint8Array(1000);
    for (var i in arr) arr[i] = Math.random()*256;
    
    function slow(b,c) { 
      return b + c;
    }
    
    function fast(b,c) { 
      "compiled";
      return b + c;
    }
    
    function test() {
      t = getTime();
      console.log(arr.reduce(slow));
      console.log("Slow = ",getTime()-t);
    
      t = getTime();
      console.log(arr.reduce(fast));
      console.log("Fast = ",getTime()-t);
    }
    
    test();
    

    gives:

     _____                 _
    |   __|___ ___ ___ _ _|_|___ ___
    |   __|_ -| . |  _| | | |   | . |
    |_____|___|  _|_| |___|_|_|_|___|
              |_| http://espruino.com
     1v72 Copyright 2014 G.Williams
    >echo(0);
    129986
    Slow =  0.21045684814
    129986
    Fast =  0.03233909606
    

    And that's just the start...

  • One more:

    function f() {'compiled'; 
      Xr=0;
      Xi=0;
      i=0;
      while ((i<16) & ((Xr*Xr+Xi*Xi)<4))  {
        t=Xr*Xr - Xi*Xi + Cr;
        Xi=2*Xr*Xi+Ci;
        Xr=t;
        i=i+1;
      }
    }
    
    var i,t,Xr,Xi,Cr,Ci;
    
    for (y=0;y<32;y++) {
      line="";
      Ci=(4.0*y/32)-2.0;
      for (x=0;x<32;x++) {
        Cr=(4.0*x/32)-2.0;  
        f();
        line += (i&1)?"*":" ";
       }
       print(line);
    }
    

    So locking is now sorted and this is powerful enough for while loops - for instance for the mandelbrot fractal. There's some minor glitch with the maths but it looks very promising.

    Interestingly this still isn't super fast because of the variable lookup, but that's something that will change soon too!

  • I love the option to precompile functions.
    Therefore I copied the js_compiler branch of tools to my computer.
    After doing some minor changes, like adding script tag, changing env.getBoardData, I got the compiler running.
    Since I don't have tools(and knowledge) to compile firmware, testing was like swimming without water.
    Anyway, I would like to add some comments.

    1. I would like to get an additional window , to switch between JS, Blockly and Assembler source. Compiler could send compiled source to Assembler window.
    2. If you have 2 functions in source, both are compiled but for 2nd JS-source is still sent to Espruino board
    3. Get exports directly from process.env could become a problem, the more entrypoints you have. Would it be possible, to read them similiar to Board Info from your server ?
    4. Enable handling of comments in assembler source, and add more comments to compiler. This would be a big help for understanding, and may be optimizing.
    5. Add a command to install binaries from SD-card.
    6. Extend projects to handle assembler modules. I could do this, as long as I get some guidelines. Simple first step could be to add read and write function for asselmbler in projects plugin.
    7. I'm pretty sure, it will take a lot of time to get a full blown up compiler. As long as we don't have this, it would help to get guidelines for implementing capable functions.

    Christmas is gone, but sometimes wishes come true later too.....

  • I think , from my personal point of view , this is one of the few things missing Espruino . The speed , compilation and/or pseudocode/asembler generation that are key to success of Espruino .

    Most functions that are programmed in a microcontroller pro/hobby, like a leds, some analog readings on these calculations, etc ... every 100ms or a reasonable time, do not penalize the overall implementation of these "events" , but if we add too many other low priority process, over a others CPU-time with the run-time-compilation, and if the project is complicated , it can penalize both implementation and memory usage. The compilation before flash, and the generation of ByteCode (outher o inside microcotroller), is great!

    I encourage you dig splendid views to ELUA project (http://www.eluaproject.net/overview/status). A "languaje script " based on Lua, very mature, and where use of compiling and generating ByteCode done, even when the "source code" ( script ) is stored in Flash memory or a SDCard, less flash memory usage, better speed and low RAM. Lua like a language, I do not like too much. I prefer Javascript. See Cross-compiling at http://www.eluaproject.net/doc/v0.8/en_using.html

    Thank you for this fantastic tool. You encouragement to proceed further in this line ( ByteCode / compilation ) because I believe that is the only (or can be a) problem for Freduino.

  • @JumJum... more Christmas-es to come... and many worth-while events in between: Man muss die Feste feiern wie sie fallen! ;-)

  • @JumJum, the 'automated build' actually builds all commits in all branches, so for instance if you look on https://github.com/espruino/Espruino/tree/js_compiler you'll get the commit hash cca740699928e7f5ee1cee62e43299e19b33f10f, which you can then use as the URL, eg: http://www.espruino.com/binaries/git/commits/cca740699928e7f5ee1cee62e43299e19b33f10f/

    If you try that build, you should be able to get the compiled code in the Web IDE to work with it. Thanks for the other discoveries too - I knew when I wrote the code that I should have tested it with two compiled functions :)

    The real killer thing for this will be the new 'setWatch' functionality which allows you to run code in an interrupt. Unfortunately there are some issues with accessing JavaScript variables directly from an IRQ, but hopefully I can overcome those with a bit of work.

  • Thanks to your hint, I got it running, firmware and compiler.
    Main problem I have is to read process.env
    In most connects, more than 50%, it failes. In log I see a part of process.env only.

  • That's odd - that code has been working for ages. And you're connecting over USB?

    It could be that the timeout for reading it is too short on some computers?

  • The problem never went away on my computer.
    Its running windows 8.1 64bit, 4 cores, 8gb ram, connected over USB.

    • tried to give it more time for reading(up to 10 secs), did not help.
    • added timeout in env.js before calling core.utils.execute Expr.., did not help
    • reading process.env from terminalwindow always works fine

    In log I see, process.env-object stops somewhere in exports, like this
    No result found - just got "echo(0);\r\n<<<<<{\"VERSION\":\"1v72\",\"BUILD_DATE\":\"Dec 24 2014\",\"BUILD_TIME\":\"12:10:46\",\"GIT_COMMIT\":\"cca740699928e7f5ee1cee62e43299e19b33f10f\",\"BOARD\":\"ESPRUINOBOARD\",\"CHIP\":\"STM32F103RCT6\",\"CHIP_FAMILY\":\"STM32F1\",\"FLASH\":262144,\"RAM\":49152,\"SERIAL\":\"33ffd905-41573033-07710743\",\"CONSOLE\":\"USB\",\"EXPORT\":[\"jsvLock,jsvLockAgain,jsvUnLock,jsvMathsOp,jsvMathsOpSkipNames,jsvNewFromFloat,jsvNewFromInteger,jsvNewFromString,jsvNewFromBool,jsvGetFloat,jsvGetInteger,jsvGetBool,jspeiFindInScopes,jspReplaceW"

  • Some more checks:

    • added core.utils.executeExpr .... process.env")... into compile.js, does not help
    • added core.utils.executeExpr .... process.env.EXPORT")... into compile.js, runs more often, but still fails sometimes
  • For testing I added listening to processor getWatched (in terminal.js) to compile.js
    This works fine, at least in about 50 runs.
    It also runs fine, in cases where utils.executeExpression returns undefined.

  • Just seen, ESP8266 and compile branch made it to V72 (as far as my understanding is correct)
    I would like to do some testing on both, should I start now or wait for rollout of v72 ?

  • I'd start now - I'm not quite sure when I'll release 1v72. There were some big changes (eg. Flat Arrays) so I want to make sure it's all working well before I do one.

    Make sure you get the latest EspruinoTools - the assembler is now put into flat arrays, which means you can save() it :)

  • Now that you opened Pandoras Box ;-) some more questions:
    Is there a way to get SW-Reference for nightly builds ?
    Are local variables in functions supported (latest webIDE already in use) ?
    Is there a way to get adress of Flat arrays to be used in Assembler ?

  • Is there a way to get SW-Reference for nightly builds ?

    You could find what the git commit # is (via process.env worst case), check that version out, and run scripts/build_docs.py

    Are local variables in functions supported (latest webIDE already in use) ?

    Not yet, no... Hopefully that's not a long way off though.

    Is there a way to get address of Flat arrays to be used in Assembler ?

    Get the address of the JsVar (which will just be in the register) and add 16 to it (or 12 if you're on a device with a tiny amount of RAM).

    By the way, running 'compiled' JS in an IRQ (via setWatch(,,{irq:true})) is still unreliable. You'll be fine using assembler though - it's the allocation of JsVars that doesn't work well just yet.

  • How do we make build_docs.py run? I've also been really wishing we could get the docs - the docs on the site are just are so old compared to what we're using, but when I run it, it barfs:

    [root@ip-10-194-211-195 scripts]# ./build_docs.py
    Script location /var/espbuild/espruino/scripts
    Scanning for jswrap.c files
    Traceback (most recent call last):
    File "./build_docs.py", line 28, in
    jsondatas = common.get_jsondata(True)
    File "/var/espbuild/espruino/scripts/common.py", line 98, in get_jsondata
    jswraps = subprocess.check_output(["find", ".", "-name", >"jswrap*.c"]).strip().split("\n")
    AttributeError: 'module' object has no attribute 'check_output'

    Looking back, maybe it would have been a good idea to have released v72 right about here, before starting on flat strings...

           Fix pin namings on Nucleo boards
           Fix addition of stdlib's exit on Nucleo debug
           Allow setWatch to execute native functions inside the IRQ
           When dumping typed arrays, use the size if all elements are 0 (fix #448)
           eval() can now access local variables and function arguments (fix #460)
    
  • maybe it would have been a good idea to have released v72 right about here, before starting on flat strings...

    maybe... :) At least 1v72 is going to be epic when it finally comes out. I think it'll be time for it soon.

    How do we make build_docs.py run?

    Which version of python are you using? Seems to work with the 2.7.3 which is the default here.

  • Post a reply
    • Bold
    • Italics
    • Link
    • Image
    • List
    • Quote
    • code
    • Preview
About

Espruino speed improvements - compiled JavaScript?

Posted by Avatar for Gordon @Gordon

Actions