# The budgets that `test_speed.py` checks, for rustymath's Python API. Each section starts with an item type in brackets. Each line under it holds a Python expression and its two native budgets, separated by tabs: first for macOS on Apple silicon, where matrix products and decompositions run on Accelerate, then for Linux on CI's x86 runner, where they run on rustymath's own kernels. `rm.timing().column` picks the budget a machine checks. The expression names the arrays that `test_speed.py` makes. A budget counts copies of 65,536 items of the section's type, the number of items in each case's main input.
# An initial budget is twice what a first-principles analysis says the case should cost, and at least 0.5. A later budget comes from a fuller understanding of the case than that analysis gives, such as a platform difference or a cost the analysis missed. A comment above each case, or above its group, gives the expected cost and its reason, as in "1 copy: one read and one write per item", and records each later change with its reason. `basedpl.bpltests.set_budgets` writes budgets from expected costs.
# One expected cost serves both platforms at first. A platform's budget differs only where a specific difference on that platform is found, such as Accelerate's matrix products on macOS.
# A case that costs more than its budget needs study, which finds either a speed bug to fix or something the analysis missed. Avoid raising a budget to match a run without understanding why the case costs what it does.
[float64]
# 1.6 copies, for two reads and one write per item, plus a tenth of a copy for the Python call.
x+y	3.2	3.2
# 1.1 copies, for one read and one write, plus a tenth of a copy for the Python call.
x*2	2.2	2.2
# 1.1 copies, for one read and one write, plus a tenth of a copy for the Python call.
-x	2.2	2.2
# 7.1 copies, for Exp's 7 copies, plus a tenth of a copy for the Python call.
rm.exp(x)	15	15
# 1.9 copies, for reading the Boolean mask, x and y, a select, one write, plus a tenth of a copy for the Python call.
rm.where(mask, x, y)	3.8	3.8
# 1.6 copies, for one read, two comparisons, one write, plus a tenth of a copy for the Python call.
rm.clip(x, -0.5, 0.5)	3.2	3.2
# 1.9 copies, for reading m and a, a select and one write, with a 256-item mask that broadcasts over m's rows and stays in cache, plus a tenth of a copy for the Python call. The broadcast mask takes the general ternary route through `read_runs`, which reads each 256-item repeat of the mask as one item.
rm.where(row > 0, m, a)	3.8	3.8
# 1.6 copies, for one read of m, two comparisons and one write, with bounds from a 256-item row that broadcasts over m's rows and stays in cache, plus a tenth of a copy for the Python call. The broadcast bounds take the general ternary route through `read_runs`, which reads each 256-item repeat of a bound as one item.
rm.clip(m, -row, row)	3.2	3.2
# 0.6 copies, for one read per item, plus a tenth of a copy for the Python call.
rm.sum(x)	1.2	1.2
# 1.1 copies, for one read and one write, plus a tenth of a copy for the Python call.
rm.cumsum(x)	2.2	2.2
# 1.1 copies, for reading m and writing the result; the row stays in cache, plus a tenth of a copy for the Python call.
m+row	2.2	2.2
# 2.6 copies, for a blocked transposed read (about 1), a read and a write, plus a tenth of a copy for the Python call.
m.T+m	5.2	5.2
# 0.6 copies, for one read per item, plus a tenth of a copy for the Python call.
rm.sum(m.T, axis=0)	1.2	1.2
# 0.6 copies, for one read per item, plus a tenth of a copy for the Python call.
rm.max(m.T, axis=1)	1.2	1.2
# 0.6 copies, for one reversed read per item, plus a tenth of a copy for the Python call.
rm.sum(x[::-1])	1.2	1.2
# 0.8 copies, for one read and two ops per item, plus a tenth of a copy for the Python call.
rm.minmax(m.T)	1.6	1.6
# 1.1 copies, for one reversed read and one write, plus a tenth of a copy for the Python call.
x[::-1]+1	2.2	2.2
# 1.4 copies, for strided reads that still load every cache line of x and y (1), and writing half as many items (0.25), plus a tenth of a copy for the Python call.
x[::2]+y[::2]	2.8	2.8
# 64 copies, for 256 FMAs per result item, a quarter copy each, on rustymath's own kernels. macOS runs it on Accelerate, which this analysis doesn't cover, so its macOS budget is unchanged.
a@b	27	130
# 64 copies, for 256 FMAs per result item, a quarter copy each, on rustymath's own kernels. macOS runs it on Accelerate, which this analysis doesn't cover, so its macOS budget is unchanged.
a.T@b	24	130
# 64 copies, for 256 FMAs per result item, a quarter copy each, on rustymath's own kernels. macOS runs it on Accelerate, which this analysis doesn't cover, so its macOS budget is unchanged.
a[::-1]@b	27	130
# 3 copies, for a random gather from L2, with about 16 loads in flight (2), and reading the 8-byte positions (0.5), plus a tenth of a copy for the Python call.
x[idx]	6	6
# 4 copies, for reading both position arrays (1), forming each offset, a random gather from L2 (2), a write, plus a tenth of a copy for the Python call. It costs about 12.7 copies on macOS and about 13 on CI, by its old budget, more than twice that, so both budgets are unchanged until the cost is studied.
m[r, c]	26	26
# 4 copies, for reading idx and y (1), a random read and write of x in L2 (3), plus a tenth of a copy for the Python call. It costs about 12.6 copies on macOS and about 13 on CI, by its old budget, more than twice that, so both budgets are unchanged until the cost is studied.
rm.at(rm.add, x, idx, y)	26	26
# 56 copies, for an unstable comparison sort of 65,536 float keys, computed once: about 16 levels per item at about 3.5 copies a level, the standard library sort's cost on random keys, which falls with lane length from about 5 at 256 items, plus a pass that computes the keys, one that writes the floats back, and a tenth of a copy for the Python call. A radix sort of the keys costs more below 2^17 items. A vectorized sort kernel would reach about 24, as "Left for later" in basedpl's `meta/next-phases.md` records. It costs about 54 copies on macOS, down from about 87 when each comparison computed two keys.
rm.sort(x)	120	120
# 3.3 copies, for 9 FMAs per output item (2.25), one read and one write, plus a tenth of a copy for the Python call. It costs about 9.7 copies on macOS and about 9.5 on CI, by its old budget, more than twice that, so both budgets are unchanged until the cost is studied.
rm.correlate(img, k)	19	19
# 5 copies, for 16 FMAs per output item (4), one read and one write, plus a tenth of a copy for the Python call. It costs about 12.2 copies on macOS and about 14 on CI, by its old budget, more than twice that, so both budgets are unchanged until the cost is studied.
rm.convolve(x, kern)	25	28
# 1.1 copies, for reading each frame item once and adding it into the output, plus a tenth of a copy for the Python call.
rm.overlap_add(frames, 32)	2.2	2.2
# 1.1 copies, for one copy of the NumPy array's items, plus a tenth of a copy for the Python call.
rm.array(xn)	2.2	2.2
# 0.1 copies, for the call alone, since the buffer protocol lends the items without copying.
np.asarray(x)	0.5	0.5
[float32]
# 1.7 copies, for two reads and one write per item, plus the call, which costs twice as much against a 32-bit copy.
xf+yf	3.4	3.4
# 5.7 copies, for Exp's 5.5 copies at f32, plus the call.
rm.exp(xf)	12	12
# 2.1 copies, for reading the mask, xf and yf, a select, one write,, plus the call.
rm.where(mask, xf, yf)	4.2	4.2
# 0.7 copies, for one read per item, plus the call.
rm.sum(xf)	1.4	1.4
