[quickjs-devel] Re: C stack usage

  • From: Ondřej Jirman <deibeemoocheuguhumoh@xxxxxx>
  • To: quickjs-devel@xxxxxxxxxxxxx
  • Date: Wed, 21 Aug 2019 19:46:57 +0200

Hi,

On Wed, Aug 21, 2019 at 11:44:38AM -0400, Simon CORSIN wrote:

Hey folks,

I have been experimenting with QuickJS and so far it's been great. I had
some questions regarding the C stack frame usage. It seems that each call
to *JS_CallInternal* uses about 10K of C stack in debug when running in
64bits on my machine, which hinders how many JS calls we can make before
getting a stack overflow. Compiling and running with optimization does help
significantly, but we are still far from what Node (V8) or Safari
(JavaScript) can achieve.

When I compile quickjs with gcc -fstack-usage it says:

quickjs.c:14800:16:JS_CallInternal      1680    dynamic

That's probably the best way to debug this is by using -fdump-rtl-expand
and inspecting the resulting '.expand' file.

;; Function JS_CallInternal (JS_CallInternal, funcdef_no=517, decl_uid=7196, 
cgraph_uid=518, symbol_order=526)

Partition 243: size 80 align 16
        sf_s
Partition 297: size 32 align 16
        ops
Partition 348: size 16 align 16
        this_obj        val     tmp     tmp     tmp     tmp     tmp     tmp
        tmp     tmp     tmp2    bfunc   obj     proto   proto   val     val
        val     val     op1     op1     op1     op1

Partition 347: size 16 align 16
        val     val     tmp1    method  method  val     val     value   op1
        op1     op1     op2     op1     op1     op2     op1     op1     op1
        op1     op1     op1     op1     op1     op1

Partition 346: size 16 align 16
        obj     setter  op1     op1
Partition 287: size 16 align 16
        getter  t
Partition 244: size 16 align 16
        ret_val
Partition 241: size 8 align 8
        ctx_3361
Partition 239: size 8 align 8
        ctx_3359
Partition 237: size 8 align 8
        ctx_3357
Partition 235: size 8 align 8
        ctx_3355
Partition 233: size 8 align 8
        ctx_3353
Partition 231: size 8 align 8
        ctx_3351
Partition 229: size 8 align 8
        ctx_3349
Partition 227: size 8 align 8
        ctx_3347
Partition 225: size 8 align 8
        ctx_3345
Partition 223: size 8 align 8
        ctx_3343
Partition 221: size 8 align 8
        ctx_3341
Partition 219: size 8 align 8

[snip.......]

a lot of ctx_#### thingies stored at different locations

So it looks like gcc manages to share the same location for some variables,
but not for 'ctx' local var.

Why? I don't know.

But if you look at the full dump, you can probably help the compiler
if you move val and atom local variable declarations to the top of
the function and share those. Maybe. I'd have to try.

But maybe looking at this data may help guide the changes.

regards,
        o.


Consider this example:

(function() {
function countRecursion(obj) {
obj.total += 1;

countRecursion(obj);
}

const obj = {
total: 0
};

try {
countRecursion(obj);
} catch (err) {}

console.log('Total recursion: ', obj.total);
})();


If I compile qjs in debug mode and run it, I get *27* recursions before
getting a stack overflow. In release mode I get *259* (almost 10x
improvement). For comparison, in Node I get *13966* recursions, in Safari
*13933*, in Duktape *9998*.
I understand of course that those results will depend on the max stack size
value configured in each of those engines. QuickJS has a default of 256KB,
which seems like a reasonable default amount. For reference on iOS, the
default max stack size for secondary threads is 512KB, so the fact that
QuickJS is set to half of that seems fine to me.

I've been toying around trying to find a way to make *JS_CallInternal* use
less C stack. According to
http://events17.linuxfoundation.org/sites/events/files/slides/GCC%252FClang%20Optimizations%20for%20Embedded%20Linux.pdf
,
GCC has a *-fconserve-stack* option, however it doesn't seem to exist on
clang at least not on the version I have on my machine. I haven't found any
compiler options that helped in raising the number I get when I run my
recursion benchmark. I then tried to remove the inline functions within
quickjs itself and replaced them by just regular function calls, and got
those results:


*Before removing inlines:*
./qjs recursion.js
Total recursion:  259
./qjs-debug recursion.js
Total recursion:  27


*After removing inlines:*./qjs recursion.js
Total recursion:  292
./qjs-debug recursion.js
Total recursion:  35

Removing inline function calls did help raising the JS call limit a bit,
maybe at the cost of reduced performance (I didn't check the real impact on
performance though). I then tried to reduce the number of local variables
used, by re-using local variables instead of creating new ones. For
example, instead of using *JSValue obj1* it uses a shared *JSValue tmp*
used by all bytecode operations that requires to store a *JSValue* on the
stack. I didn't go too deep into this, just wanted to see if it could help
and this is the result I got:

./qjs recursion.js
Total recursion:  297
./qjs-debug recursion.js
Total recursion:  39

I was actually surprised to see that it helped at all. I'd expect the
compiler would be able to optimize that out fairly easily.

That being explained, these are my questions:
- What do you think would be the best way to significantly improve the JS
call limit?
- Can this be done without having to move the JS stack from the C stack
into the heap?
- If you can think of some low hanging fruits, can you give me any pointers
of what would need to be done? I'd be happy to help if needed.

Thanks again for the awesome work!

Cheers,
Simon

Other related posts: