Skip to content

Vector like and in never dispatch to the worker pool, while the same predicates inside select do #608

Description

@vbmithr

Summary

like and in applied to a vector never dispatch to the worker pool. The same
predicates inside a select do. On a 16-thread host that is a 4-6x gap for
like, and it also inflates the in gap reported in #593.

Comparison primitives are not affected — == over the same column scales.

Measurement

like over a 10M-element SYM column (2.62M distinct, ~303 MB symbol heap),
median of 5, same data in all three rows:

-c 1 -c 2 -c 4 -c 8 -c 16 scaling
(sum (like url "*google*")) 238.8 231.2 229.0 224.4 226.2 ms 1.06x
select where: (in-memory) 159.4 80.2 52.0 45.0 39.8 ms 4.0x
select where: (parted) 76.0 44.4 26.6 22.0 20.6 ms 3.7x

The bare form is flat across the whole range — it is serial. Note it is also
1.5x slower than the fused form at -c 1, so parallel dispatch is the larger
part of the gap but not all of it.

in behaves the same way, and == does not. 351,393-element SYM column:

-c 1 -c 4 -c 16 scaling
(sum (== col s)) 100 20 20 us 5.0x
(sum (in col one)) 2660 1920 1900 us 1.4x
(sum (in col many)) 4180 3660 3660 us 1.14x
select where: (== c s) 140 60 80 us 2.3x

Reproducer

(set NL 351393)
(set insts (as 'SYM (map (fn [i] (format "INST%" i)) (til 766))))
(set col (take insts NL))
(set one (take insts 1))
(set t (table [c] (list col)))
(set s (first insts))
(set bench
  (fn [name n f]
    (do (f) (let t0 (.time.now)) (let i 0)
        (while (< i n) (f) (let i (+ i 1)))
        (println name "  " (/ (* 1000.0 (- (.time.now) t0)) n) " us"))))
(bench "BARE  (== col s)   " 50 (fn [] (sum (== col s))))
(bench "BARE  (in col one) " 50 (fn [] (sum (in col one))))
(bench "FUSED select == s  " 50 (fn [] (count (select {from: t where: (== c s)}))))

Run at -c 1 and -c 16 and compare. == scales, in does not.

Why it matters

  1. It is invisible. Both forms are correct and idiomatic, and nothing
    indicates that one is single-threaded. Lifting a predicate out of a select
    into a variable — a refactor that looks free — silently costs the pool.

  2. It distorts in with a one-element right side builds a hash instead of lowering to an equality scan (~27x) #593. That issue reports (in col one) at 67x (== col x).
    That ratio is two defects stacked, and they separate cleanly by core count:

    -c 1    in/==  = 2660/100 =  26.6x     algorithmic only
    -c 16   in/==  = 1900/20  =  95.0x     algorithmic + missing parallelism
    

    So roughly 3.6x of the gap on a 16-thread host is == scaling while in
    stays serial; the remaining ~27x is the hash-vs-scan issue in with a one-element right side builds a hash instead of lowering to an equality scan (~27x) #593 is really
    about. Worth separating there, since fixing one does not fix the other.

  3. It interacts with Execution pool recruits every core regardless of work size: 2.7x the CPU for 1.2x the speed #599/Worker pool defaults to logical CPUs, not physical cores: ~2x the CPU on SMT hosts #606. Discussion there is about the pool recruiting
    too many workers; this is a set of primitives that recruit none.

What we would ask for

Dispatch vector like and in the way == is already dispatched, or document
that these primitives are serial outside a query so the fused form can be
chosen deliberately.

We are not blocked: our own hot paths use the fused form. This was found while
measuring #606 and looks like a straightforward gap rather than a design choice.


Measured on rayforce 2.6.2.r237.gc5556113, 8 physical / 16 logical
(Ryzen AI 7 350).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions