-
Notifications
You must be signed in to change notification settings - Fork 1.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Linking is O(M_binders * N_body_instructions), could be O(N' log M) or even O(N'), where N' is just unbound instructions #1380
Comments
We can add
Another thought is that we may eventually want to close the set of builtins for the "jq" module (as it were) or otherwise compile only the ones we need. |
I think I'll do the following:
|
I like the idea of compiling only those we need, but it may be difficult to identify those at runtime. Symbols that reach the end of compilation/binding and still haven't been bound should be builtins. But builtins can also bind against other builtins, so, do we keep digging into some builtin list each time we find a symbol we don't know? The nice thing about builtins right now is that they're just a jq source file... Closing the Food for thought... |
Another possibility is pre-byte-compiling the builtins. That would require a serialization format (which would have to preserve binding information). It would also require a two-pass build, first to build a bootstrap jq, and then to build jq. |
Came across this after observing some apparently-quadratic linking time in the context of trying to speed up jq "startup". To provide some context, as it turns out, jq takes quite a bit of of time to do absolutely nothing:
This matters to me because I found myself using jq in a shell script which invokes jq ~3k times, on separate files, which means actually spending several cpu-minutes (which is still like, half a minute) waiting for jq, and as it turns out, this is the majority of the time jq spends when doing just about anything. So an strace shows that it's not doing anything strange on that from after some instrumenting I concluded that (a) most of the time is spent loading builtins, and (b) most of that time is spent in So I tried ripping out all the jq-defined builtins:
... which suggests that we really can get down to, if not on par with /bin/true, at least down to that order of magnitude, along with the perl/sed/awk crew (which seems like where we want to be if we're positioned as "sed for JSON data"), instead of halfway between python2 and GHC:
Stepping back a bit. I confirmed that library linking was indeed as quadratic as it looked on inspection:
... but this didn't jive with what I've seen with generated jq programs that contain hundreds of function definitions, which don't take nearly as long to run:
(We hit ARG_MAX beyond ~7k defs, and the parser bails after 9993 definitions anyway, but this looks O(n) to me and even if it isn't it's Fast Enough.) This suggests a possible stupid optimization:
|
Maybe it would be useful to provide a "don't include builtin.jq" flag? Almost all the jq-defined builtins are definable using documented parts of the jq language, except for
|
Name resolution in libraries is basically O(n^2) in number of bindings and the impact of builtin.jq on jq's startup time turns out to be absolutely awful. The situation is actually much better for inline definitions alongside the 'program', which are at least discarded immediately if unreferenced. Given this, it could be useful for jq programs that need would otherwise waste a lot of time in linking because they are run frequently on small (up to a few MB...) inputs to skip builtin.jq, which by definition can be defined entirely in jq, albeit possibly using internal interfaces. See also: jqlang#1380
@muhmuhten It might be possible to apply something of a bandaid... Basically, change |
I think I'll just add |
Name resolution in libraries is basically O(n^2) in number of bindings and the impact of builtin.jq on jq's startup time turns out to be absolutely awful. The situation is actually much better for inline definitions alongside the 'program', which are at least discarded immediately if unreferenced. Given this, it could be useful for jq programs that need would otherwise waste a lot of time in linking because they are run frequently on small (up to a few MB...) inputs to skip builtin.jq, which by definition can be defined entirely in jq, albeit possibly using internal interfaces. See also: jqlang#1380
Name resolution in libraries is basically O(n^2) in number of bindings and the impact of builtin.jq on jq's startup time turns out to be absolutely awful. The situation is actually much better for inline definitions alongside the 'program', which are at least discarded immediately if unreferenced. Given this, it could be useful for jq programs that need would otherwise waste a lot of time in linking because they are run frequently on small (up to a few MB...) inputs to skip builtin.jq, which by definition can be defined entirely in jq, albeit possibly using internal interfaces. See also: jqlang#1380
Name resolution in libraries is basically O(n^2) in number of bindings and the impact of builtin.jq on jq's startup time turns out to be absolutely awful. The situation is actually much better for inline definitions alongside the 'program', which are at least discarded immediately if unreferenced. Given this, it could be useful for jq programs that need would otherwise waste a lot of time in linking because they are run frequently on small (up to a few MB...) inputs to skip builtin.jq, which by definition can be defined entirely in jq, albeit possibly using internal interfaces. See also: jqlang#1380
Name resolution in libraries is basically O(n^2) in number of bindings and the impact of builtin.jq on jq's startup time turns out to be absolutely awful. The situation is actually much better for inline definitions alongside the 'program', which are at least discarded immediately if unreferenced. Given this, it could be useful for jq programs that need would otherwise waste a lot of time in linking because they are run frequently on small (up to a few MB...) inputs to skip builtin.jq, which by definition can be defined entirely in jq, albeit possibly using internal interfaces. See also: jqlang#1380
Name resolution in libraries is basically O(n^2) in number of bindings and the impact of builtin.jq on jq's startup time turns out to be absolutely awful. The situation is actually much better for inline definitions alongside the 'program', which are at least discarded immediately if unreferenced. Given this, it could be useful for jq programs that need would otherwise waste a lot of time in linking because they are run frequently on small (up to a few MB...) inputs to skip builtin.jq, which by definition can be defined entirely in jq, albeit possibly using internal interfaces. See also: jqlang#1380
Name resolution in libraries is basically O(n^2) in number of bindings and the impact of builtin.jq on jq's startup time turns out to be absolutely awful. The situation is actually much better for inline definitions alongside the 'program', which are at least discarded immediately if unreferenced. Given this, it could be useful for jq programs that need would otherwise waste a lot of time in linking because they are run frequently on small (up to a few MB...) inputs to skip builtin.jq, which by definition can be defined entirely in jq, albeit possibly using internal interfaces. See also: jqlang#1380
Linking is now fast again thanks to @muhmuhten's various contributions, especially #1834. Block binding is still quadratic, but jq startup times are now remarkably fast -- as fast as they were back in 1.5. |
Linking is O(M_binders * N_body_instructions), could be O(N' log M) or even O(N'), where N' is just unbound instructions.
As we add builtins, linking will slow.
We've already optimized binding of global
$var
s defined with--arg*
.We could now optimize the binding of builtins and binders exported by modules.
There are two independent things to do about this:
Build a list of unbound instructions as we do any binding so that once we have that list we need only traverse it (this is where
N'
comes from).Sort the globals to bind by name and ariness, then traverse the body (or list of unbound insts in it) to find unbound insts and then binary search the table of binders (this is where
log M
comes from).Alternatively build a hash table of the globals to get down to
O(N')
(assuming we really getO(1)
from the hash table).We could build the sorted / hash tables as we compile a module.
The cost of sorting or building a hash table has to be taken into account. For large programs it will be negligible, as it is a one-time cost, but most jq programs are small... A hash table will probably perform best in this sense, since insertion should be fast too.
The C-coded builtins can be kept sorted manually. No need to sort it at run-time, nor hash it.
src/builtin.jq
can't be sorted because some of those builtins depend on others.The text was updated successfully, but these errors were encountered: