brython-dev / brython

Brython (Browser Python) is an implementation of Python 3 running in the browser
BSD 3-Clause "New" or "Revised" License
6.35k stars 507 forks source link

[OPTIMISATION] Improving argument parsing #2283

Open denis-migdal opened 10 months ago

denis-migdal commented 10 months ago

Current tasklist: https://github.com/brython-dev/brython/issues/2283#issuecomment-1779264084

See [2275] for other optimizations about function calls.

========================================================

I started working on the optimisation of $B.args0().

I will edit this first message in the future to give a link to a github dedicated to this project + to give a summary of the results and the current progression of the project.

I will use the messages below to discuss about this issue.

Some ideas here.

denis-migdal commented 9 months ago

Tsk, previous idea (I hid it) won't work as functions can be substituted/redefined in Python...

Such a shame, this could have lead to precomputation of part of the result which would make argument parsing almost without any cost in some cases...

Python really have rules that prevents lot of optimizations...

denis-migdal commented 9 months ago

Okay, still, I got a great idea of optimization for calls using named arguments.

Such calls is written as :

$B.$call(....)(a,b, {$kw: [{}, ...]})

Meaning that, when we do not have a **kwarg parameter, we already have an object containing some of the function parameter.

So, inside the parser, instead of having to create a new result = {} then later filling it with the value of args[i].$kw[0], we can just directly use args[i].$kw[0] as result. And just have to count the number of element prefilled in our result.

This can possibly generate a big speed up.

Of course, we could even rewrite function calls as :

$B.$call(....)(a,b,
                      2, // pre-compute number of named arguments
                      {a: 32, b:34}, // named arguments
                      null // **kargs arguments, can be [...] if there are some
                      )

Sure, it'll create an object even when we do not use named arguments, BUT, we'd be able to recycle it if function doesn't accept **kwargs, or if the precomputed number of named arguments is 0 (i.e. is empty). Could be negative if there are no named arguments, but there is **kargs arguments, which would allow the checks with a single comparison instead of 2.

Another optimization, Object.create(null) seems 1.18x faster than {} for creation, access, and in. Almost identical for modification. {} is used in 368 places in Brython code.

EDIT: not a good idea, modification if forbidden in strict mode. Creation with __proto__: null is very slow.

PierreQuentel commented 9 months ago

In the commit above and the previous ones I have reduced the size of generated Javascript code for functions by delegating the creation of f.$infos to functions $B.make_function_infos() and $B.make_code_attr(). I have also removed f.$defaults in $B.make_function_defaults()

denis-migdal commented 9 months ago

Cool.

I have currently some work to do + some personal matter so I'm not quite available theses days.

But once I'll have a little time I'll add the little opti I though for named arguments ;).

PierreQuentel commented 9 months ago

When publishing 3.12.1 and running the speed test script, I observed that function creation was slower than before, which is logical since the function argument parser is built on function creation, and it takes a little time.

In commit 4f5bc28278495476ada1bfb2223ce6e14f5cc03f I have slightly modified the feature : the parser is only built the first time the function is called. With this change, function creation is now faster, and execution is the same.

denis-migdal commented 9 months ago

When publishing 3.12.1 and running the speed test script, I observed that function creation was slower than before, which is logical since the function argument parser is built on function creation, and it takes a little time.

There are several things :

  1. the creation of the parser function, which is indeed slow as we use Function. We could pre-create some parsers that are used very often. But I don't think this will be a huge gain.
  2. finding which parser to use. This isn't optimized.

Please note that Function is very slow as we have to parse JS code in order to create the function. This cost is "hidden" when you directly write the function in the code as it is parsed before you start executing.

In commit 4f5bc28 I have slightly modified the feature : the parser is only built the first time the function is called. With this change, function creation is now faster, and execution is the same.

Hum, you add a condition (${name2}.$args_parser ?? $B.make_args_parser(${name2})) on each function calls ? Conditions are very expensive for such code.

Maybe you can do it like that to remove the condition:

function make_args_parser_then_parse(fct, args) {
     fct.args_parser = $B.make_args_parser(fct);            // replace the initial fct.args_parser by the newly created parser.
     return fct.args_parser(fct, args);                                // you can do it in one line if you want.
}

// create a new function:
function foo() {} // do stuff
foo.args_parser = make_args_parser_then_parse; // called upon first parsing, create the parser then parse.
// consecutive parsing will call directly the parser.

In the same way, maybe there is a way to build the function $infos only the first time it is needed (and build the parser at the same time ?) :


// create a new function:
function foo() {} // do stuff
Object.defineProperty("$infos", {
    // some config.
    get: function() { const $infos = createInfos(fct); Object.defineProperty("$infos",  $infos); return $infos;  }
});

Something like that. Then createInfos could also call $B.make_args_parser to prevent doing some operations twice. Then, we could replace make_args_parser_then_parse by make_$infos_and_parser_then_parse.

denis-migdal commented 9 months ago

Hum Object.defineProperty() seems quite slow, a little too much. Then the best solution would be using make_args_parser_then_parse.

Do we need $infos outside of the args_parser() / set kw defaults / etc. ?

I'll also have to do some benchmarks to see where this extra-cost comes from. If it is due to Function, I do not think we really need to bother a lot. It's just that previously the cost was hidden has the JS code parsing time wasn't considered. If it is something else, I'll have to take a look to the optimizations possible.

denis-migdal commented 9 months ago

Made a PR ( #2336 ) for the little opti I though about (x1.11 in parsing of named argument when no **kwargs parameter).