perf: update the accumulated value of a reduce expression in place

_assign and _modify create an allocator on every call, so the value they
are given is never one that allocator created, and updateObject and
updateArrayIndex always take the copying branch. Accumulating into an
object or an array with a reduce expression therefore copies the whole
accumulated value on every iteration, which is quadratic in the number
of elements. The query of #241,

    [range(20000)] | reduce .[] as $item ({}; .[($item|tostring)] = {})

took between 10 and 21 seconds here over three runs, against 0.03
seconds for jq.

Create the allocator once per reduce expression and pass it to the
assignment as a third argument, so that the accumulated value is
allocated on the first iteration and modified in place afterwards. The
query above now takes 0.03 seconds, and the memory allocated by an
accumulating reduction grows linearly instead of quadratically: 2.1 MB
instead of 545 MB for 4000 elements.

This is only correct while the accumulated value, and the values the
allocator created inside it, are reachable through the reduction alone.
Query#inPlaceUpdate therefore requires the update query to be a single
assignment, and its right-hand side to neither observe the input value,
which would let the assigned value alias the value it is assigned into,
nor yield more than one value, which would run the assignment again
against a value it already modified. Everything else keeps copying. A
foreach expression is left alone, because it yields every intermediate
accumulated value.
This commit is contained in:
dualfroz committed 2026-09-14 07:31:57 +02:00
1 parent 33d02f2fdb
commit 6d290823f7
4 files changed
+456 -43

No files matched your search

+62
View File
@@ -3642,6 +3642,68 @@
expected: |
[[1,1],[1]]
- name: reduce with assignment in update
args:
- -c
- 'reduce range(3) as $i ({}; .[$i|tostring] = {}),
reduce range(3) as $i ({}; .a[$i|tostring] = $i),
reduce range(3) as $i ([]; .[$i] = $i),
reduce range(3) as $i ({}; .[$i|tostring] += 1)'
input: 'null'
expected: |
{"0":{},"1":{},"2":{}}
{"a":{"0":0,"1":1,"2":2}}
[0,1,2]
{"0":1,"1":1,"2":1}
- name: reduce with assignment of the accumulated value
args:
- -c
- 'reduce range(3) as $i ({}; .a = .),
reduce range(3) as $i ({}; .a = {v: .}),
reduce range(3) as $i ({}; . as $p | .a = $i | .b = $p)'
input: 'null'
expected: |
{"a":{"a":{"a":{}}}}
{"a":{"v":{"a":{"v":{"a":{"v":{}}}}}}}
{"a":2,"b":{"a":1,"b":{"a":0,"b":{}}}}
- name: reduce with assignment of multiple values
args:
- -c
- 'reduce range(2) as $i ({"k":"k"}; .[.k] = ("x","y")),
reduce range(2) as $i ({}; (.a = 1), (.b = 2))'
input: 'null'
expected: |
{"k":"y","y":"y"}
{"b":2}
- name: reduce with assignment of a shared value
args:
- -c
- '{"a":1} as $x | reduce range(2) as $i ({}; .[$i|tostring] = $x) |
[(.["0"].a = 9), $x]'
input: 'null'
expected: |
[{"0":{"a":9},"1":{"a":1}},{"a":1}]
- name: reduce with assignment result reused
args:
- -c
- 'reduce range(3) as $i ({}; .[$i|tostring] = $i) as $r |
[($r | .["0"] = 9), $r]'
input: 'null'
expected: |
[{"0":9,"1":1,"2":2},{"0":0,"1":1,"2":2}]
- name: foreach with assignment in update
args:
- -c
- '[foreach range(3) as $i ({}; .[$i|tostring] = $i)]'
input: 'null'
expected: |
[{"0":0},{"0":0,"1":1},{"0":0,"1":1,"2":2}]
- name: foreach iterator
args:
- 'foreach range(5) as $item (0; $item)'