Commit 6f4e54b
authored
Optimization: std.sort should only evaluate keyF once per array element (#245)
This PR improves the performance of `std.sort` and related functions
when `keyF` is used.
The existing implementation evaluates `keyF` multiple times per input
element because:
- Prior to sorting, it evaluates `keyF` on all elements to check whether
all keys are of the same type.
- During sorting, it re-evaluates `keyF` on every pair of compared
elements.
In the best case (an already-sorted array), this performs ~3x more
evaluations than needed because each element participates in up to two
extra unnecessary comparisons. In the worst case, we have to do
additional comparisons during sorting and the unnecessary work will be
even higher.
### The fix
The fix:
- Precompute all keys up front (which we already do for type-checking
purposes).
- Sort an array of indices using a comparator which fetches their
corresponding precomputed keys
- Use the sorted indices to project out the array values in the correct
order
I also made a few other small improvements:
- Explicitly error out when trying to sort arrays of booleans: neither
jsonnet nor go-jsonnet supports this. The existing sjsonnet code didn't
either, but failed with a confusing `"Cannot sort with key values that
are not all the same type"` error because `Val.True` and `Val.False` are
different classes. The existing code which did class equality checks on
`Val.Bool` would never match because `Val.Bool` is an abstract class.
- Avoid allocating a `keyTypes` set: we can simply check that all other
elements match the first type's element. This saves some garbage
allocations in the common case.
- Move `.force` calls earlier so that we don't have to call them in the
sort comparator.
### Benchmarking results
Consider the following toy benchmark case:
```jsonnet
local largeArr = [
{
complexKey: { a: i, b: i+1, c: i+2 },
value: "val" + i
}
for i in std.range(0, 9999)
];
local sortedArr = std.sort(
largeArr,
keyF=function(x) std.toString(x.complexKey)
);
{ sortedArrSample: sortedArr[0:5] }
```
With the `RunProfiler` we can see an enormous difference in the number
of `std.toString` invocations via the key function: with a standard 5
benchmark runs, we expect to see only 50,000 hits but the old code ran
it `241,210` times!
I also measured performance on one of our real-world jsonnet bundles,
where this PR's optimization cut one expensive target's runtime by 25%.1 parent 89c04a0 commit 6f4e54b
2 files changed
Lines changed: 28 additions & 18 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1700 | 1700 | | |
1701 | 1701 | | |
1702 | 1702 | | |
1703 | | - | |
1704 | | - | |
1705 | | - | |
| 1703 | + | |
| 1704 | + | |
| 1705 | + | |
| 1706 | + | |
| 1707 | + | |
| 1708 | + | |
1706 | 1709 | | |
1707 | 1710 | | |
1708 | 1711 | | |
1709 | | - | |
1710 | | - | |
1711 | | - | |
1712 | | - | |
1713 | | - | |
1714 | | - | |
| 1712 | + | |
| 1713 | + | |
| 1714 | + | |
| 1715 | + | |
| 1716 | + | |
| 1717 | + | |
1715 | 1718 | | |
1716 | | - | |
| 1719 | + | |
1717 | 1720 | | |
| 1721 | + | |
| 1722 | + | |
1718 | 1723 | | |
1719 | | - | |
1720 | | - | |
1721 | | - | |
| 1724 | + | |
| 1725 | + | |
| 1726 | + | |
1722 | 1727 | | |
| 1728 | + | |
| 1729 | + | |
1723 | 1730 | | |
1724 | | - | |
| 1731 | + | |
1725 | 1732 | | |
1726 | | - | |
| 1733 | + | |
1727 | 1734 | | |
1728 | | - | |
1729 | | - | |
1730 | | - | |
| 1735 | + | |
1731 | 1736 | | |
1732 | 1737 | | |
1733 | 1738 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
57 | 57 | | |
58 | 58 | | |
59 | 59 | | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
60 | 65 | | |
61 | 66 | | |
62 | 67 | | |
| |||
0 commit comments