| From: | Matthias Dötsch <matze(at)mdoetsch(dot)de> |
|---|---|
| To: | pgsql-hackers(at)lists(dot)postgresql(dot)org |
| Cc: | amitlan(at)postgresql(dot)org, dgrowleyml(at)gmail(dot)com |
| Subject: | Perf: Specialize ExecIndexOnlyScan() like ExecSeqScan() |
| Date: | 2026-10-14 10:36:49 |
| Message-ID: | 3684e94b-3077-4430-94d9-e5411aa5f110@mdoetsch.de |
| Views: | Whole Thread | Raw Message | Download mbox | Resend email |
| Thread: | |
| Lists: | pgsql-hackers |
Hey,
this is my first patch for Postgres, so please let me know if I got
something about the contribution policies wrong.
Our main workload does a lot of index-only scans, and in perf profiles of it
a good part of the time showed up under ExecScan(). Digging into the
code I noticed that IndexScan, IndexOnlyScan and BitmapHeapScan all
still go through the generic out-of-line ExecScan(). So each tuple pays
for an indirect call to the access method and for the runtime EPQ /
qual / projection checks. SeqScan got rid of that with fb9f95502 (and
83a53572a6f made sure SeqNext actually gets inlined).
The attached patch does the same for index-only scans. There are now
ExecIndexOnlyScan, ...WithQuals, ...WithProject, ...WithQualProject and
...EPQ, the variant is picked in ExecInitIndexOnlyScan(), and
IndexOnlyNext() is pg_always_inline. The EPQ variant just calls
ExecScan() like ExecSeqScanEPQ does.
One thing that cost me bit more time: the runtime key setup (which may call
ExecReScan()) has to happen before the pg_assume() calls. If it comes
after, gcc can't know that pstate->qual is still non-NULL after the
call, keeps the !qual && !projInfo branch of ExecScanExtended() and you
end up with a second inlined copy of IndexOnlyNext() in each variant.
I added a comment for that.
Test setup: table with id int primary key and two more int columns, 5M
rows, vacuumed so it's all-visible. enable_seqscan and enable_bitmapscan
off, jit off, no parallel. gcc 12, -O2, i7-13700H.
instructions latency
SELECT count(*) FROM t
WHERE id BETWEEN 1 AND 1000000 -5.2% -5 to -11%
same, plus AND id % 3 = 0 -4.8% -3 to -5%
SELECT id + 1 FROM t
WHERE id BETWEEN 1 AND 1000000 OFFSET 2000000 -6.3% -4 to -10%
Instructions are from callgrind (single user mode), so those are kinda
deterministic. Latency is median pgbench latency over 5 interleaved
rounds, server pinned to one P-core. It's a range because I ran
everything a second time with -falign-functions=64 and the wall clock
results moved by several percent just from the the layout change. So I
wouldn't put too much weight on the exact latency numbers, but it was
faster in all cases.
tbh the code gets bigger: the per-tuple function is 576-858 bytes depending
on the variant, and the IOS functions together go from ~0.6kB to
~3.5kB.
Tests: regress and isolation pass with cassert, and CI on my github
fork is green on all platforms. There is no existing test that runs an
index-only scan under EPQ though. I checked that path manually with a
correlated subquery in an UPDATE that has to wait for a concurrent
update. I can turn that into an isolation test if people think it's
worth it.
any thoughts?
Best,
Matthias
| Attachment | Content-Type | Size |
|---|---|---|
| v2-0001-Specialize-ExecIndexOnlyScan-like-ExecSeqScan.patch | text/x-patch | 7.8 KB |
| From | Date | Subject | |
|---|---|---|---|
| Next Message | Ashutosh Sharma | 2026-10-14 10:47:28 | Re: Skip prefetch for block references that follow a FPW or WILL_INIT of the same block |
| Previous Message | Nikhil Sontakke | 2026-10-14 10:31:49 | Re: Publication DDL can race with a concurrent UPDATE |