Summary
The execution pool splits a query across every available worker regardless of
how much work there is. Past a handful of workers the coordination cost grows
faster than the saving, so total CPU rises steeply while wall time barely
improves. A service that is not latency-bound pays several times the CPU it
needs and has no way to ask for the cheaper trade.
Reproducer
200 inner-joins of a 3-row key set against a 130,000-row table, the shape a
market-data service runs continuously:
(set N 130000)
(set syms (map (fn [i] (parse (format "INST%" i))) (til 773)))
(set t (table [venue instrument session message_id px]
(list (take ['BFU] N) (take syms N) (take ['S1] N) (til N) (til N))))
(set ks (select {from: t by: [venue instrument] n: (count px)}))
(set k3 (select {from: ks take: 3}))
(set n 0)
(times 200 (set n (+ n (count (inner-join [venue instrument] t k3)))))
/usr/bin/time on a 16-core machine, 2.6.2.r229.ge7770412:
| cores |
user CPU |
wall |
speedup |
CPU vs -c 1 |
-c 1 |
0.71 s |
0.71 s |
1.0x |
1.00x |
-c 2 |
0.71 s |
0.36 s |
2.0x |
1.00x |
-c 4 |
0.80 s |
0.22 s |
3.2x |
1.13x |
-c 16 |
1.93 s |
0.18 s |
3.9x |
2.72x |
-c 2 is free: half the wall time for the same CPU. -c 4 costs 13% for a
3.2x speedup. -c 16 costs 172% more CPU to go 1.2x faster than -c 4.
Small queries are unaffected — 20,000 joins over a 10-row table cost 0.05 s at
every core count, so nothing is dispatched for those. The effect starts where
the pool starts splitting.
Impact in production
A depth-of-book service of ours, default pool on a 16-core host, against an
identical instance on the same live feed with -c 2, in lock-step on every
state counter throughout:
default (16c) 50% 13% 23% 44% 57% 64%
-c 2 25% 10% 15% 23% 27% 31%
Half the CPU, identical throughput, identical progress. A second run comparing
-c 1 with -c 4 was monotonic in the same direction. The service's own batch
takes 2.5-5.7 ms against a ~26 ms inter-batch gap, so the latency the pool buys
is worth nothing to it — and .sys.querylog reports effective parallelism of
~0.0002 for a typical batch, while perf put 73% of samples in worker_loop.
Notably our other Rayforce services on the same host and the same feed sit at
1-2% CPU: they do not run joins large enough to dispatch, so they never meet
this. It is specific to whoever does the table-sized work.
What we are asking for
- Scale the worker count to the work, not to the machine — a morsel-count
or row-count heuristic, so a 130k-row join does not recruit 16 threads for a
1.2x gain over 4. The measurements above suggest the knee is low.
- Failing that, a throughput mode — a way to say "minimise CPU, I am not
latency-bound". -c is that knob today, but it is a blunt global: it also
caps the genuinely parallel work a process may want elsewhere.
We have set -c 2 and consider the immediate problem solved for us; this is
filed because the default costs every non-latency-bound service the same
multiple, silently.
Measured on rayforce 2.6.2.r229.ge7770412, 16-core host.
Summary
The execution pool splits a query across every available worker regardless of
how much work there is. Past a handful of workers the coordination cost grows
faster than the saving, so total CPU rises steeply while wall time barely
improves. A service that is not latency-bound pays several times the CPU it
needs and has no way to ask for the cheaper trade.
Reproducer
200 inner-joins of a 3-row key set against a 130,000-row table, the shape a
market-data service runs continuously:
/usr/bin/timeon a 16-core machine, 2.6.2.r229.ge7770412:-c 1-c 1-c 2-c 4-c 16-c 2is free: half the wall time for the same CPU.-c 4costs 13% for a3.2x speedup.
-c 16costs 172% more CPU to go 1.2x faster than-c 4.Small queries are unaffected — 20,000 joins over a 10-row table cost 0.05 s at
every core count, so nothing is dispatched for those. The effect starts where
the pool starts splitting.
Impact in production
A depth-of-book service of ours, default pool on a 16-core host, against an
identical instance on the same live feed with
-c 2, in lock-step on everystate counter throughout:
Half the CPU, identical throughput, identical progress. A second run comparing
-c 1with-c 4was monotonic in the same direction. The service's own batchtakes 2.5-5.7 ms against a ~26 ms inter-batch gap, so the latency the pool buys
is worth nothing to it — and
.sys.querylogreports effective parallelism of~0.0002 for a typical batch, while
perfput 73% of samples inworker_loop.Notably our other Rayforce services on the same host and the same feed sit at
1-2% CPU: they do not run joins large enough to dispatch, so they never meet
this. It is specific to whoever does the table-sized work.
What we are asking for
or row-count heuristic, so a 130k-row join does not recruit 16 threads for a
1.2x gain over 4. The measurements above suggest the knee is low.
latency-bound".
-cis that knob today, but it is a blunt global: it alsocaps the genuinely parallel work a process may want elsewhere.
We have set
-c 2and consider the immediate problem solved for us; this isfiled because the default costs every non-latency-bound service the same
multiple, silently.
Measured on
rayforce2.6.2.r229.ge7770412, 16-core host.