Hi,

I continued the heap-only experiment I described earlier in this
thread and attached v2 as a two-patch series. This is an alternative
implementation of that experiment; it does not replace or supersede
Amit's v8 series.

I think a batched executor does not necessarily require a new API for
passing batches between plan nodes. Not every node will benefit from
batching; the most promising nodes are those that process a large stream
of tuples. A sequential scan is a natural first candidate. Since
heapam is currently the only table AM implementation shipped in core,
these patches keep the experiment local to heap scans.

The first patch uses information that heap page mode already collects.
After heapgettup_pagemode() finds the visible offsets on a page, the
buffer heap slot records that page-local batch. heap_getnextslot() can
then advance through the remaining offsets without another call to
heapgettup_pagemode(). The scan still returns one ordinary slot at a
time to its parent, so this changes neither the table AM API nor the
interface between plan nodes.

The second patch uses the same page batch to evaluate a supported
initial prefix of SeqScan quals over as many as 64 tuples. It gathers
Datums in a tight loop and then calls the existing operator functions in
another tight loop. A 64-bit mask records passing rows. With multiple
supported quals, each qual narrows the mask, and later quals are called
only for rows that passed the earlier ones.

This is not SIMD. The gain comes from better locality and from doing
less executor work between adjacent operator calls. An important
consequence is that operators need no new batch or SIMD-specific API;
the code calls their existing scalar functions and also supports user
types and cross-type operators.

The batch path currently accepts binary OpExpr clauses of these forms:

  Var op Const/Param
  Const/Param op Var

Here Param means PARAM_EXTERN or PARAM_EXEC.

The operator must return bool, must not return a set, and its function
must be marked IMMUTABLE, STRICT, and LEAKPROOF. These restrictions are
important because a batch may evaluate up to 63 later rows before the
parent asks for them. Such functions must have no side effects, and
their catalog declarations must be correct. Other clauses use the
existing row-at-a-time path.

The patches do not reorder quals. They batch only the supported initial
prefix in the order chosen by the planner. The first unsupported qual
and everything after it remain row-at-a-time. This also retains the
planner's potentially useful ordering for tuple deformation and
short-circuit evaluation.

Projection still uses the normal ExecProject() path, and it runs only
for rows that pass all quals. Thus projection gets no batching benefit
in this version. EPQ and cases without a suitable heap page batch also
fall back to the existing scalar behavior.

The patches are:

  0001 Avoid repeated heapgettup_pagemode() calls within a heap page
  0002 Evaluate SeqScan quals in heap page batches

I also reran the benchmark against the same current master and against
Amit's v8 applied to that master. The base was master at 1c4b1de8885;
v8 applied cleanly. pass-all is a > 0, and pass-none is a < 0. Each
cell below is the change from unpatched master for 5M / 10M rows;
negative values mean faster.

all-visible:

  query                    this series       Amit v8
  count(*)                 -6.5% / -7.0%     -9.0% / -10.4%
  count(*) WHERE pass-all  -17.6% / -17.7%   -5.3% / -4.3%
  count(*) WHERE pass-none -21.6% / -22.0%   -8.7% / -9.4%

not-all-visible:

  query                    this series       Amit v8
  count(*)                 -5.2% / -3.8%     -9.2% / -6.8%
  count(*) WHERE pass-all  -17.3% / -16.4%   -4.3% / -3.4%
  count(*) WHERE pass-none -24.8% / -20.4%   -12.3% / -7.4%

In this run, v8 was somewhat faster for count(*) without a qual. For
the two qual cases, local batch evaluation made this series about
13-14% faster than v8.

The tables contained a 100-byte text value and about 43 tuples per heap
page. They were prewarmed and fit in shared_buffers. Parallel query,
JIT, synchronized scans, and autovacuum were disabled. The
not-all-visible case used frozen heap tuples with the visibility map
truncated.

I used an -O2 build on an Apple M5 Pro. For each query I ran three
warmups followed by 31 measured EXPLAIN ANALYZE executions and used the
median. I repeated each variant twice in symmetric order and averaged
the two medians to reduce frequency drift.

The full regression suite passes for the two-patch series.

I would appreciate feedback on both the heap-local batch representation
and the idea of keeping batch qual evaluation inside SeqScan while
preserving the ordinary slot interface above it.

Best regards,
Denis Smirnov

Attachment: v2-0001-Avoid-repeated-heapgettup_pagemode-calls-within-a.patch
Description: Binary data

Attachment: v2-0002-Evaluate-SeqScan-quals-in-heap-page-batches.patch
Description: Binary data

Reply via email to