-
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
reduce is a dog (even in jq>1.4) #618
Comments
Would you mind providing the foreach and recurse versions you made? I think I have an idea. |
|
Well, those definitely confirm my suspicion. Looks like the problem with the reduce version is the The foreach and recurse versions don't do any array appending themselves. As a result, they're both quite snappy! I'm not sure what we can do to improve upon this. Perhaps we can find a way to speed up the array copy process, but that seems unlikely to me, given the way the types work internally. @nicowilliams? |
@wtlangford - Some time ago, when I was working on a (pretty nifty) programming language, this issue about how far to extend an array was resolved basically by doubling. However, naively trying to increase new_length in jvp_array_write reveals dragons. (I'd be interested to know why.) On the other hand, wrapping recurse in [] does work nicely, so maybe it's time to retire the old implementation of reduce? |
Keep in mind that the block of code you're looking at does a couple of things. I would say we definitely do not retire this implementation of reduce. While not necessarily the recommended call for reducing values into an array, it is still quite good at reducing into objects and numbers and so on. That being said, I am looking for a way to make array duplication less expensive. |
So there are at least two distinct issues:
In this case, it is clear that the array at .[2] can be "recycled". |
I'm not sure what you mean by recycled... |
jvp_array_write() should probably increase the allocation size |
Oh, right:
1.5, right. |
BTW, the problem in the reduce version is a typo in your code. In the On Thu, Nov 13, 2014 at 1:34 PM, Nico Williams [email protected] wrote:
|
Nope, the point there was to get an array of the indices that pass a test. On Thu, Nov 13, 2014, 14:37 Nico Williams [email protected] wrote:
|
Oh, I should stop trying to do two things at once. |
@wtlangford wrote:
Under certain circumstances, there is no need for an array to be copied. (E.g. in the program I had at first assumed that the jq compiler was good at recycling in this sense, but I'm beginning to realize that there must be lots of succulent low-hanging recycling opportunities to be picked. |
jq recycles array and object values when they have only one reference. |
Your example should definitely recycle that array. |
Hi, @nicowilliams, and welcome back to the party. Based on my timings, it's hard to believe the array is being properly recycled in the way I think it should be:
Results (u+s):
|
I've tracked this down to assignment expanding to calls to Using I.e., this:
goes fast (no reallocs), while this:
goes slow (reallocs once, in this case). A fix would have to arrange to not have an internal var of any kind, using In the meantime you have a workaround: use |
Oh, another possibility would be clearing the reduction state var |
It's trickier than this. |
@nicowilliams wrote:
Excellent. I should have guessed the culprit was "=" -- in my experience, it usually is! Thanks.
A workaround for the "=" problem, but are you implying this implies a workaround for the "reduce"-related issue as well? |
It'd be nice to fix this in 1.5, but I don't think we have the energy for it. |
@pkoppstein It turns out |
Yes, but with foreach, there's an easy workaround. Is there one for reduce? Meanwhile, thanks for giving this your attention. The exponential growth of required memory for "map/reduce" is something devoutly to be avoided. |
@pkoppstein Please re-post your foreach workaround, a complete test case. BTW, I'm working with the hypothesis that |
Actually, the places where we need a The problem is that we have a reference count leak. There is no corresponding memory leak because of the way stack unwinding works: we always free jv's when unwinding. But the reference count leak causes the optimized path of jq's copy-on-write behavior to not work in Those two leaks come from the fact that the value at the top of the stack isn't actually at the top when those opcodes are executed: they follow a Therefore, for now, I'm going with the following fix:
That works and passes all tests. |
@nicowilliams - Thanks for the fix! For future reference, the "foreach" code is all there in my first two postings in this thread, but here's everything in one bundle:
|
@pkoppstein Can you confirm that this fixes it for |
(I myself confirmed it by observing a simple test case's debug trace with refcounts in the output.) I do wonder how to write a test for this... I don't want to time something. I think I'll just compare output with debug tracing, though that seems brittle. |
OK, so here's the last word on this bug. The problem was that The better fix for this issue is not to add (A generalization would be to add an operand to |
Hi. First of all, kudos for documenting this bug so well cause it was not clear at all why one would want to replace a value with null on the stack, and in this way fix performance. I've considered the possibility of making FORK pop and push, and this won't work, actually. The problem is that fork should provide the same top of the stack for the initial execution and for the backtrack jump. This is achieved by restoring stack pointer to the value at the time of fork point creation. I believe that a cleaner fix to this issue would be to use a local var for |
Sorry, on the second thought there is even a simpler fix to this, I guess.
You see, the first time FORK is executed it saves fork point and proceeds to The backtrack from |
For reduce we may benefit from a FORK1 instruction. What is similar between reduce and foreach - they both don't want the same value after the FORK backtracks (as I showed above, foreach can optimise the fork away completely, but in the current state it just discards the input by backtracking). What is different between reduce and foreach - reduce does require the FORK because it wants to proceed further with the accumulated value. BUT that instruction is a LOADV, which simply discards the input - it could be a dup, that's it.
Here are the two options I see that avoid the spooky DUPN:
P.S. I think that LOADVN should store a jv_invalid() into the frame var, just like the frame initialisation code does. This should mean "the value is consumed" and if one needs to store another value there it must be an explicit action. If one fails, |
The following function uses "reduce" to emit the "equilibrium indices" of the input array. An equivalent version written using recurse or foreach runs nicely in jq>1.4 (i.e. those versions that have TCO and foreach), but in both jq 1.4 and jq>1.4, the version using reduce has roughly exponentially worse performance in the length of the input array.
Here are some gross timings using jq>1.4 (i.e. based simply on "time jq ...") for the following query:
Since the
recurse
andforeach
versions of this program are fine in jq>1,4, the problem is evidently with the implementation of "reduce". Hopefully a skilled eye will be able to identify it; if not, then maybe "reduce" should be implemented using "foreach"!The text was updated successfully, but these errors were encountered: