Skip to content

in with a one-element right side builds a hash instead of lowering to an equality scan (~27x) #593

Description

@vbmithr

Summary

in compares every element of its left side against every element of its
right side. There is no hash set, and no specialisation for a single-element
right side, so a membership test against a column is O(n·m) and is beaten by
hand-written equality predicates by two to three orders of magnitude.

Measurement

A 351,393-element symbol column (766 distinct values), right side varied:

right side (in col right)
1 symbol 6,500 us
10 symbols 11,950 us
100 symbols 76,800 us
766 symbols 298,200 us

For comparison, on the same column, (== col 'SYM) — the same question for
one element — is about 16 us. in is ~400x slower than the equality it could
lower to, before any set is involved.

Why it matters to us

We maintain an order book table (levels, ~351k rows, keyed conceptually by
(venue, instrument)) and frequently need "the rows belonging to these few
books". The three shapes available today:

  • (left-join [venue instrument] levels keys) — correct, but sized by
    levels: ~13 ms whether the right side names one book or ten.
  • (in instrument syms) — sized by n·m as above, and also wrong on its own
    for a compound key: (and (in venue vs) (in instrument is)) matches
    cross-venue pairs that are in neither input.
  • OR-ed per-pair predicates, (or (and (== v ...) (== i ...)) ...) — what we
    ended up writing. 50 us for one book, 600 us for ten, linear in the number
    of books, and it crosses the join's flat cost at ~216 books.

The third is fastest for small key sets and is what we shipped, but it is a
loop building a boolean expression by hand, and it is linear where a hash set
would be flat.

What we are asking for

  1. Hash the right side of in when it is a vector above some small
    length, so membership is O(n + m) rather than O(n·m). The machinery
    already exists — hash_row_keys is what the join builds.
  2. Specialise a one-element right side to the equality scan. (in col [x]) should not cost 400x (== col x).

Optionally, and more useful still for our case:

  1. A compound membership — a way to ask "is this row's (a, b) in this
    two-column table" without materialising a composite key column and without
    a full join. This is the primitive our workload actually wants; in per
    column cannot express it correctly.

Prior art

q's in hashes its right side, and ?/in over a compound key is expressed
with a keyed table lookup that probes a hash rather than scanning.


Measured on rayforce 2.6.2 (the build carrying bc350ddc, the while form).

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