[PATCH] Speed up repeat() for larger counts

Started by ChenhuiMo3 months ago16 messageshackers
Beta feature

Hackorum builds and tests every patch posted to the lists, not only commitfest submissions. This is Hackorum's own CI rather than the PostgreSQL project's, and it is still under testing - please report anything that looks wrong.

appliessuccessCI history

You can run a PostgreSQL built from this patch straight from Docker, with no checkout and no build:

docker run --rm -p 5432:5432 ghcr.io/hackorum-dev/postgres-patch:t253193
psql -h localhost -U postgres

Built from patchset v14 (message #14), October 23, 2026 at 01:29 AM.

Every patchset is also pushed to a branch of our PostgreSQL fork, so you can check out the same tree CI built. Without a PostgreSQL checkout:

git clone --branch t253193_14 https://github.com/hackorum-dev/postgres.git

In a checkout you already have, add the fork once:

git remote add hackorum https://github.com/hackorum-dev/postgres.git

then, for this patchset and every later one:

git fetch hackorum t253193_14 && git checkout t253193_14

Patchset v14 (message #14) is on t253193_14

Jump to latest
#1ChenhuiMo
chenhuimo.mch@qq.com

Hi all,

This patch optimizes repeat(text, int).

The current implementation constructs the result by copying the source text
once per repetition.  For larger repeat counts, that leads to many small
memcpy() calls.  This patch keeps the existing linear path for small
repeat counts, and uses a doubling strategy for larger ones by repeatedly
copying the already-generated prefix of the destination buffer.

In local testing, this reduced total runtime in a small repeat()-focused
microbenchmark, with the largest gains appearing in medium and large repeat
count cases.  Small-count cases did not show obvious regressions after
keeping the original path below a small cutoff.

The patch is attached.

Thanks for your time, and any feedback would be appreciated.

Best regards,
Chenhui Mo

Attachments:

t253193_1
0001-speed-up-repeat-for-larger-counts.patchapplication/octet-stream; charset=utf-8; name=0001-speed-up-repeat-for-larger-counts.patchDownload+26-3
#2Jeevan Chalke
jeevan.chalke@enterprisedb.com
In reply to: ChenhuiMo (#1)
Re: [PATCH] Speed up repeat() for larger counts

Hello ChenhuiMo,

This is a really interesting optimization — doing memcpy() exponentially is
a great idea for larger counts. My main use case for repeat() is generating
large files and padding, where count is usually large, so this change helps
exactly the workload I care about.

That said, I'm not entirely sure how common this usage pattern is in
general production workloads, so I was initially unsure whether the added
complexity is worth it. Setting that aside, the performance gain for short
strings repeated a large number of times is real, so I'm fine with getting
this in.

A few things worth looking at:

1. The magic number 8 is unexplained. The naive-loop fallback threshold
seems quite low — should it be closer to a few dozen, say 64? I benchmarked
a range of thresholds and counts: the doubling approach turns out to beat
the naive loop even at very small counts (as low as 2–3), so 8 already
looks like a reasonable cutoff; raising it to 64 would likely make counts
in the 8–64 range slower, not faster. Regardless of the exact value chosen,
the constant needs a comment explaining why it was picked.

2. Because of the exponential growth, CHECK_FOR_INTERRUPTS() granularity
drops off logarithmically; the later doubling steps can copy hundreds of MB
in a single memcpy that can't be interrupted partway through. Would it help
to cap the chunk size, say at 64–100MB? I tested this: capping adds only a
handful of extra memcpy calls even for very large repeats, and the overhead
was within noise; so it looks like a cheap way to bound worst-case
interrupt latency.

3. The doubling logic itself has no comment. I think a comment there is a
must; the self-overlapping-safe memcpy trick isn't obvious at a glance.

4. It's worth adding tests that exercise count at the boundary values to
guard against regressions if this logic gets refactored later.

5. Cosmetic: add a blank line between the variable declaration and the
first statement in the new block.

Thanks

On Sun, Jul 26, 2026 at 11:00 AM ChenhuiMo <chenhuimo.mch@qq.com> wrote:

Hi all,

This patch optimizes repeat(text, int).

The current implementation constructs the result by copying the source text
once per repetition. For larger repeat counts, that leads to many small
memcpy() calls. This patch keeps the existing linear path for small
repeat counts, and uses a doubling strategy for larger ones by repeatedly
copying the already-generated prefix of the destination buffer.

In local testing, this reduced total runtime in a small repeat()-focused
microbenchmark, with the largest gains appearing in medium and large repeat
count cases. Small-count cases did not show obvious regressions after
keeping the original path below a small cutoff.

The patch is attached.

Thanks for your time, and any feedback would be appreciated.

Best regards,
Chenhui Mo

--
*Jeevan Chalke*
*Senior Principal Engineer, Engineering Manager*
*Product Development*

enterprisedb.com <https://www.enterprisedb.com&gt;

#3Heikki Linnakangas
heikki.linnakangas@enterprisedb.com
In reply to: Jeevan Chalke (#2)
Re: [PATCH] Speed up repeat() for larger counts

On 27/07/2026 10:31, Jeevan Chalke wrote:

Hello ChenhuiMo,

This is a really interesting optimization — doing memcpy() exponentially
is a great idea for larger counts. My main use case for repeat() is
generating large files and padding, where count is usually large, so
this change helps exactly the workload I care about.

That said, I'm not entirely sure how common this usage pattern is in
general production workloads, so I was initially unsure whether the
added complexity is worth it. Setting that aside, the performance gain
for short strings repeated a large number of times is real, so I'm fine
with getting this in.

Yeah, probably the most common case is something like repeat('x',
1000000). It might even be worth special-casing the single-byte case and
turn that into a single memset() call.

A few things worth looking at:

1. The magic number 8 is unexplained. The naive-loop fallback threshold
seems quite low — should it be closer to a few dozen, say 64? I
benchmarked a range of thresholds and counts: the doubling approach
turns out to beat the naive loop even at very small counts (as low as 2–
3), so 8 already looks like a reasonable cutoff; raising it to 64 would
likely make counts in the 8–64 range slower, not faster. Regardless of
the exact value chosen, the constant needs a comment explaining why it
was picked.

2. Because of the exponential growth, CHECK_FOR_INTERRUPTS() granularity
drops off logarithmically; the later doubling steps can copy hundreds of
MB in a single memcpy that can't be interrupted partway through. Would
it help to cap the chunk size, say at 64–100MB? I tested this: capping
adds only a handful of extra memcpy calls even for very large repeats,
and the overhead was within noise; so it looks like a cheap way to bound
worst-case interrupt latency.

Beyond certain size, I'd guess it might even become slower, if the
string no longer fits in the L0 CPU cache for example. Also, if the
fast-path used a constant size, like 64 bytes, maybe the compiler could
optimize the memcpy() into a single SIMD instruction or something.

- Heikki

#4David Rowley
dgrowleyml@gmail.com
In reply to: Heikki Linnakangas (#3)
Re: [PATCH] Speed up repeat() for larger counts

On Mon, 27 Jul 2026 at 22:18, Heikki Linnakangas <hlinnaka@iki.fi> wrote:

Beyond certain size, I'd guess it might even become slower, if the
string no longer fits in the L0 CPU cache for example. Also, if the
fast-path used a constant size, like 64 bytes, maybe the compiler could
optimize the memcpy() into a single SIMD instruction or something.

I read this earlier and wondered the same thing. The thing that might
save it would be the hardware prefetcher seeing the linear memory
access and loading the cachelines back again before there's a stall.
However, that assumes memory can keep up with a modern memcpy(), and I
believe that a good AVX512 memcpy could well outpace even DDR5 RAM
speed. Capping the chunks at something L1-sized might solve that. It
might be worth the author looking at what I did with
ESCAPE_JSON_FLUSH_AFTER in json.c. Benchmarks for that in [1]/messages/by-id/CAApHDvpi7FkbNenAB+XKPtejDmVMx7iEfxKH=LN_rZDGvLoGcA@mail.gmail.com. I
expect it'll be diminishing returns with memcpy sizes beyond a handful
of kilobytes.

It does feel like a rather incomplete performance patch as it doesn't
come with any results to demonstrate that it actually has a positive
impact on performance. It would be good to see a script posted here
that tests various input lengths and repeat counts that people could
easily run to see if they get the same improvement on their hardware.

David

[1]: /messages/by-id/CAApHDvpi7FkbNenAB+XKPtejDmVMx7iEfxKH=LN_rZDGvLoGcA@mail.gmail.com

#5Jeevan Chalke
jeevan.chalke@enterprisedb.com
In reply to: David Rowley (#4)
Re: [PATCH] Speed up repeat() for larger counts

On Mon, Jul 27, 2026 at 7:11 PM David Rowley <dgrowleyml@gmail.com> wrote:

On Mon, 27 Jul 2026 at 22:18, Heikki Linnakangas <hlinnaka@iki.fi> wrote:

Beyond certain size, I'd guess it might even become slower, if the
string no longer fits in the L0 CPU cache for example. Also, if the
fast-path used a constant size, like 64 bytes, maybe the compiler could
optimize the memcpy() into a single SIMD instruction or something.

I read this earlier and wondered the same thing. The thing that might
save it would be the hardware prefetcher seeing the linear memory
access and loading the cachelines back again before there's a stall.
However, that assumes memory can keep up with a modern memcpy(), and I
believe that a good AVX512 memcpy could well outpace even DDR5 RAM
speed. Capping the chunks at something L1-sized might solve that. It
might be worth the author looking at what I did with
ESCAPE_JSON_FLUSH_AFTER in json.c. Benchmarks for that in [1]. I
expect it'll be diminishing returns with memcpy sizes beyond a handful
of kilobytes.

It does feel like a rather incomplete performance patch as it doesn't
come with any results to demonstrate that it actually has a positive
impact on performance. It would be good to see a script posted here
that tests various input lengths and repeat counts that people could
easily run to see if they get the same improvement on their hardware.

+1. While the simple example shows good performance, I agree with David
that having actual performance numbers and a script would be helpful here.

David

[1]
/messages/by-id/CAApHDvpi7FkbNenAB+XKPtejDmVMx7iEfxKH=LN_rZDGvLoGcA@mail.gmail.com

--
*Jeevan Chalke*
*Senior Principal Engineer, Engineering Manager*
*Product Development*

enterprisedb.com <https://www.enterprisedb.com&gt;

#6Jan Nidzwetzki
jan@planetscale.com
In reply to: Jeevan Chalke (#5)
Re: [PATCH] Speed up repeat() for larger counts

Hi Hackers,

On 28.07.26 04:15, Jeevan Chalke wrote:

On Mon, Jul 27, 2026 at 7:11 PM David Rowley <dgrowleyml@gmail.com <mailto:dgrowleyml@gmail.com>> wrote:
On Mon, 27 Jul 2026 at 22:18, Heikki Linnakangas <hlinnaka@iki.fi <mailto:hlinnaka@iki.fi>> wrote:

Beyond certain size, I'd guess it might even become slower, if the
string no longer fits in the L0 CPU cache for example. Also, if the
fast-path used a constant size, like 64 bytes, maybe the compiler could
optimize the memcpy() into a single SIMD instruction or something.

I read this earlier and wondered the same thing. The thing that might
save it would be the hardware prefetcher seeing the linear memory
access and loading the cachelines back again before there's a stall.
However, that assumes memory can keep up with a modern memcpy(), and I
believe that a good AVX512 memcpy could well outpace even DDR5 RAM
speed. Capping the chunks at something L1-sized might solve that. It
might be worth the author looking at what I did with
ESCAPE_JSON_FLUSH_AFTER in json.c. Benchmarks for that in [1]. I
expect it'll be diminishing returns with memcpy sizes beyond a handful
of kilobytes.

It does feel like a rather incomplete performance patch as it doesn't
come with any results to demonstrate that it actually has a positive
impact on performance. It would be good to see a script posted here
that tests various input lengths and repeat counts that people could
easily run to see if they get the same improvement on their hardware.

+1. While the simple example shows good performance, I agree with David that having actual performance numbers and a script would be helpful here.

I ran a few experiments on two of my systems (a modern ARM CPU with
large caches and an older Intel CPU with small caches) using the patch
and the attached benchmark script (repeat_bench.sql).

* System 1: Debian 13.5 VM on an Apple M5 Max,
32MB of L2 cache / 24 MB of L3 cache,
36 GB RAM, gcc 14.2.0 -O2

* System 2: Debian 13.5 on an Intel Pentium Silver J5005,
4 MB L2 cache, no L3 cache,
16 GB RAM, gcc 14.2.0 -O2

Using the SQL benchmark script, I get these results:

System 1: Without the patch (master at 72e6184)
=====

jan2=# SELECT * FROM repeat_bench_sql();
output_mb | source_len | repeat_count | best_ms | ns_per_byte
-----------+------------+--------------+----------+-------------
0.0 | 0 | 10000000 | 15.8710 |
16.0 | 1 | 16777216 | 24.7230 | 1.4736
16.0 | 10 | 1677721 | 2.0880 | 0.1245
16.0 | 100 | 167772 | 0.5810 | 0.0346
16.0 | 1024 | 16384 | 0.5130 | 0.0306
16.0 | 4096 | 4096 | 0.5040 | 0.0300
16.0 | 16384 | 1024 | 0.4880 | 0.0291
16.0 | 65536 | 256 | 0.4010 | 0.0239
16.0 | 262144 | 64 | 0.5000 | 0.0298
16.0 | 1048576 | 16 | 0.5020 | 0.0299
64.0 | 1 | 67108864 | 111.7530 | 1.6652
64.0 | 10 | 6710886 | 14.8360 | 0.2211
64.0 | 100 | 671088 | 8.9660 | 0.1336
64.0 | 1024 | 65536 | 8.7150 | 0.1299
64.0 | 4096 | 16384 | 8.8710 | 0.1322
64.0 | 16384 | 4096 | 8.8390 | 0.1317
64.0 | 65536 | 1024 | 8.5130 | 0.1269
64.0 | 262144 | 256 | 8.7930 | 0.1310
64.0 | 1048576 | 64 | 9.0270 | 0.1345
256.0 | 1 | 268435456 | 421.7930 | 1.5713
256.0 | 10 | 26843545 | 56.2210 | 0.2094
256.0 | 100 | 2684354 | 32.6070 | 0.1215
256.0 | 1024 | 262144 | 31.8170 | 0.1185
256.0 | 4096 | 65536 | 31.8400 | 0.1186
256.0 | 16384 | 16384 | 32.1850 | 0.1199
256.0 | 65536 | 4096 | 31.7850 | 0.1184
256.0 | 262144 | 1024 | 32.8390 | 0.1223
256.0 | 1048576 | 256 | 32.9460 | 0.1227

System 1: With the patch applied
=====

jan2=# SELECT * FROM repeat_bench_sql();
output_mb | source_len | repeat_count | best_ms | ns_per_byte
-----------+------------+--------------+---------+-------------
0.0 | 0 | 10000000 | 0.0000 |
16.0 | 1 | 16777216 | 0.9460 | 0.0564
16.0 | 10 | 1677721 | 0.9350 | 0.0557
16.0 | 100 | 167772 | 0.7410 | 0.0442
16.0 | 1024 | 16384 | 0.6820 | 0.0407
16.0 | 4096 | 4096 | 0.5850 | 0.0349
16.0 | 16384 | 1024 | 0.5250 | 0.0313
16.0 | 65536 | 256 | 0.5060 | 0.0302
16.0 | 262144 | 64 | 0.5060 | 0.0302
16.0 | 1048576 | 16 | 0.5070 | 0.0302
64.0 | 1 | 67108864 | 8.0250 | 0.1196
64.0 | 10 | 6710886 | 7.6990 | 0.1147
64.0 | 100 | 671088 | 7.6640 | 0.1142
64.0 | 1024 | 65536 | 8.1790 | 0.1219
64.0 | 4096 | 16384 | 8.1190 | 0.1210
64.0 | 16384 | 4096 | 8.0930 | 0.1206
64.0 | 65536 | 1024 | 7.8130 | 0.1164
64.0 | 262144 | 256 | 7.7870 | 0.1160
64.0 | 1048576 | 64 | 7.7850 | 0.1160
256.0 | 1 | 268435456 | 30.5510 | 0.1138
256.0 | 10 | 26843545 | 29.3930 | 0.1095
256.0 | 100 | 2684354 | 29.3650 | 0.1094
256.0 | 1024 | 262144 | 30.8200 | 0.1148
256.0 | 4096 | 65536 | 29.7990 | 0.1110
256.0 | 16384 | 16384 | 29.8490 | 0.1112
256.0 | 65536 | 4096 | 29.7460 | 0.1108
256.0 | 262144 | 1024 | 29.6070 | 0.1103
256.0 | 1048576 | 256 | 29.8270 | 0.1111

System 2: Without the patch (master at 72e6184)
=====

jan2=# SELECT * FROM repeat_bench_sql();
output_mb | source_len | repeat_count | best_ms | ns_per_byte
-----------+------------+--------------+-----------+-------------
0.0 | 0 | 10000000 | 68.5470 |
16.0 | 1 | 16777216 | 118.7810 | 7.0799
16.0 | 10 | 1677721 | 16.1220 | 0.9609
16.0 | 100 | 167772 | 5.5260 | 0.3294
16.0 | 1024 | 16384 | 5.6850 | 0.3389
16.0 | 4096 | 4096 | 5.9350 | 0.3538
16.0 | 16384 | 1024 | 5.8580 | 0.3492
16.0 | 65536 | 256 | 5.6150 | 0.3347
16.0 | 262144 | 64 | 5.6130 | 0.3346
16.0 | 1048576 | 16 | 5.4430 | 0.3244
64.0 | 1 | 67108864 | 673.6170 | 10.0377
64.0 | 10 | 6710886 | 222.5620 | 3.3164
64.0 | 100 | 671088 | 179.0040 | 2.6674
64.0 | 1024 | 65536 | 178.2080 | 2.6555
64.0 | 4096 | 16384 | 182.1820 | 2.7147
64.0 | 16384 | 4096 | 180.0070 | 2.6823
64.0 | 65536 | 1024 | 179.4350 | 2.6738
64.0 | 262144 | 256 | 179.1260 | 2.6692
64.0 | 1048576 | 64 | 183.2310 | 2.7304
256.0 | 1 | 268435456 | 2669.7590 | 9.9456
256.0 | 10 | 26843545 | 872.4090 | 3.2500
256.0 | 100 | 2684354 | 699.4290 | 2.6056
256.0 | 1024 | 262144 | 690.5680 | 2.5726
256.0 | 4096 | 65536 | 705.9170 | 2.6297
256.0 | 16384 | 16384 | 700.8910 | 2.6110
256.0 | 65536 | 4096 | 702.4840 | 2.6170
256.0 | 262144 | 1024 | 695.1320 | 2.5896
256.0 | 1048576 | 256 | 722.5480 | 2.6917

System 2: With the patch applied
=====

jan2=# SELECT * FROM repeat_bench_sql();
output_mb | source_len | repeat_count | best_ms | ns_per_byte
-----------+------------+--------------+----------+-------------
0.0 | 0 | 10000000 | 0.0000 |
16.0 | 1 | 16777216 | 5.8740 | 0.3501
16.0 | 10 | 1677721 | 5.7070 | 0.3402
16.0 | 100 | 167772 | 5.5940 | 0.3334
16.0 | 1024 | 16384 | 5.9030 | 0.3518
16.0 | 4096 | 4096 | 5.9310 | 0.3535
16.0 | 16384 | 1024 | 5.9300 | 0.3535
16.0 | 65536 | 256 | 5.8300 | 0.3475
16.0 | 262144 | 64 | 5.6770 | 0.3384
16.0 | 1048576 | 16 | 5.9440 | 0.3543
64.0 | 1 | 67108864 | 189.5320 | 2.8242
64.0 | 10 | 6710886 | 186.9150 | 2.7853
64.0 | 100 | 671088 | 187.3550 | 2.7918
64.0 | 1024 | 65536 | 186.8650 | 2.7845
64.0 | 4096 | 16384 | 187.5210 | 2.7943
64.0 | 16384 | 4096 | 188.7260 | 2.8122
64.0 | 65536 | 1024 | 187.4840 | 2.7937
64.0 | 262144 | 256 | 186.8230 | 2.7839
64.0 | 1048576 | 64 | 188.0490 | 2.8021
256.0 | 1 | 268435456 | 730.2740 | 2.7205
256.0 | 10 | 26843545 | 731.0500 | 2.7234
256.0 | 100 | 2684354 | 732.5100 | 2.7288
256.0 | 1024 | 262144 | 732.6140 | 2.7292
256.0 | 4096 | 65536 | 732.1680 | 2.7275
256.0 | 16384 | 16384 | 732.9070 | 2.7303
256.0 | 65536 | 4096 | 740.0030 | 2.7567
256.0 | 262144 | 1024 | 739.3400 | 2.7543
256.0 | 1048576 | 256 | 739.1520 | 2.7536

Comments
=====

Using the new shortcut for a source_len of 0 is highly beneficial. On
both CPUs the patch is much faster for very short sources and slower
from source_len ~ 100 upwards. On the M5 this is only visible with 16 MB
of output, where the patch is up to 33% slower (source_len 1024: 0.5130
ms -> 0.6820 ms). On the J5005 the same crossover is visible at every
output size, with a ~5% regression for source_len >= 100.

Further Tests
=====

To understand the problem better, I wrote an extension function (see
repeatbench--1.0.sql, repeatbench.c, repeatbench.control, Makefile) that
tries out a few strategies and numbers:

* master - the repeat() algorithm of the current master branch.
* doubling - the algorithm proposed in the patch.
* nobranch - the algorithm proposed in the patch but without the
special 'count < 8' branch.
* cap512 - doubling roughly capped at 512 bytes.
* cap4k - doubling roughly capped at 4 KB.
* cap64k - doubling roughly capped at 64 KB.
* nocap - doubling roughly capped at PG_INT32_MAX, so never in practice
(same as nobranch but with the capping machinery).
* memset - performing a memset when source = 1B (as suggested
by Heikki).

The extension checks every strategy against the built-in repeat() before
reporting any timing.

System 1
=====

jan2=# SELECT * from repeat_bench();
source | repeats | output | master | doubling | nobranch | cap512 | cap4k | cap64k | nocap | memset | fastest
--------+-----------+----------+----------+----------+----------+--------+--------+---------+---------+---------+----------
32 B | 2 | 64 B | 5 ns | 0.98x | 0.99x | 0.88x | 0.88x | 0.88x | 0.88x | - | master
32 B | 4 | 128 B | 6 ns | 0.90x | 1.38x | 1.20x | 1.20x | 1.20x | 1.20x | - | nobranch
32 B | 6 | 192 B | 8 ns | 0.92x | 1.33x | 1.26x | 1.26x | 1.26x | 1.26x | - | nobranch
32 B | 7 | 224 B | 10 ns | 0.93x | 1.42x | 1.35x | 1.35x | 1.35x | 1.35x | - | nobranch
32 B | 8 | 256 B | 10 ns | 1.50x | 1.57x | 1.50x | 1.50x | 1.50x | 1.50x | - | nobranch
32 B | 9 | 288 B | 10 ns | 1.38x | 1.43x | 1.43x | 1.43x | 1.43x | 1.44x | - | nocap
32 B | 12 | 384 B | 13 ns | 1.50x | 1.56x | 1.22x | 1.25x | 1.26x | 1.28x | - | nobranch
32 B | 16 | 512 B | 16 ns | 1.26x | 1.24x | 1.22x | 1.23x | 1.22x | 1.23x | - | doubling
32 B | 64 | 2 KB | 61 ns | 2.24x | 2.26x | 2.22x | 2.24x | 2.24x | 2.25x | - | nobranch
32 B | 4096 | 128 KB | 3.4 us | 2.51x | 2.84x | 2.35x | 2.38x | 2.44x | 2.49x | - | nobranch
1 B | 16777216 | 16 MB | 20.0 ms | 79.58x | 79.84x | 89.11x | 90.73x | 127.12x | 79.97x | 161.94x | memset
10 B | 1677721 | 16 MB | 1.5 ms | 5.84x | 5.86x | 6.62x | 5.96x | 9.88x | 5.86x | - | cap64k
100 B | 167772 | 16 MB | 308.8 us | 1.23x | 1.24x | 1.39x | 0.72x | 2.17x | 1.24x | - | cap64k
1 KB | 16384 | 16 MB | 258.8 us | 1.03x | 1.04x | 1.01x | 1.19x | 1.64x | 1.04x | - | cap64k
64 KB | 256 | 16 MB | 211.3 us | 0.85x | 0.85x | 1.00x | 1.01x | 1.01x | 0.85x | - | cap4k
1 MB | 16 | 16 MB | 248.6 us | 0.99x | 0.99x | 1.00x | 1.00x | 1.01x | 0.99x | - | cap64k
1 B | 268435456 | 256 MB | 330.6 ms | 73.72x | 75.28x | 77.36x | 71.86x | 75.49x | 78.03x | 166.40x | memset
10 B | 26843545 | 256 MB | 23.5 ms | 5.59x | 5.57x | 5.58x | 5.19x | 6.30x | 5.57x | - | cap64k
100 B | 2684354 | 256 MB | 5.2 ms | 1.24x | 1.24x | 1.25x | 0.74x | 1.57x | 1.24x | - | cap64k
1 KB | 262144 | 256 MB | 4.3 ms | 1.04x | 1.04x | 1.00x | 0.98x | 1.06x | 1.04x | - | cap64k
64 KB | 4096 | 256 MB | 4.6 ms | 1.09x | 1.08x | 0.99x | 1.00x | 1.00x | 1.08x | - | doubling
1 MB | 256 | 256 MB | 4.1 ms | 0.96x | 0.96x | 1.00x | 1.02x | 1.02x | 0.96x | - | cap64k
1 B | 1000 | 1000 B | 1.3 us | 71.09x | 77.07x | 71.40x | 71.72x | 72.04x | 72.86x | 168.72x | memset
1 B | 1000000 | 976.6 KB | 1.3 ms | 99.40x | 99.71x | 99.40x | 98.46x | 137.32x | 104.02x | 174.90x | memset

System 2
=====

jan2=# SELECT * from repeat_bench();
source | repeats | output | master | doubling | nobranch | cap512 | cap4k | cap64k | nocap | memset | fastest
--------+-----------+----------+-----------+----------+----------+---------+---------+--------+--------+---------+----------
32 B | 2 | 64 B | 17 ns | 0.85x | 1.09x | 0.85x | 0.85x | 0.85x | 0.85x | - | nobranch
32 B | 4 | 128 B | 26 ns | 1.09x | 1.40x | 1.20x | 1.20x | 1.20x | 1.13x | - | nobranch
32 B | 6 | 192 B | 31 ns | 0.94x | 1.21x | 1.07x | 1.07x | 1.07x | 1.07x | - | nobranch
32 B | 7 | 224 B | 36 ns | 0.94x | 1.41x | 1.25x | 1.25x | 1.25x | 1.25x | - | nobranch
32 B | 8 | 256 B | 40 ns | 1.57x | 1.59x | 1.41x | 1.41x | 1.41x | 1.41x | - | nobranch
32 B | 9 | 288 B | 45 ns | 1.47x | 1.48x | 1.34x | 1.34x | 1.34x | 1.34x | - | nobranch
32 B | 12 | 384 B | 59 ns | 1.81x | 1.83x | 1.67x | 1.67x | 1.67x | 1.67x | - | nobranch
32 B | 16 | 512 B | 84 ns | 2.26x | 2.28x | 2.10x | 2.10x | 2.10x | 2.10x | - | nobranch
32 B | 64 | 2 KB | 316 ns | 3.71x | 3.81x | 3.36x | 3.59x | 3.59x | 3.59x | - | nobranch
32 B | 4096 | 128 KB | 19.1 us | 2.05x | 2.06x | 2.78x | 2.79x | 2.05x | 2.06x | - | cap4k
1 B | 16777216 | 16 MB | 114.8 ms | 43.10x | 44.06x | 51.79x | 49.64x | 47.36x | 42.30x | 56.57x | memset
10 B | 1677721 | 16 MB | 12.8 ms | 4.68x | 4.85x | 5.89x | 5.58x | 5.29x | 4.71x | - | cap512
100 B | 167772 | 16 MB | 2.2 ms | 0.80x | 0.84x | 1.04x | 0.94x | 0.90x | 0.80x | - | cap512
1 KB | 16384 | 16 MB | 2.3 ms | 0.83x | 0.87x | 1.00x | 0.99x | 0.95x | 0.83x | - | cap512
64 KB | 256 | 16 MB | 2.4 ms | 0.85x | 0.89x | 1.00x | 0.97x | 0.97x | 0.85x | - | cap512
1 MB | 16 | 16 MB | 2.2 ms | 0.82x | 0.81x | 0.96x | 1.03x | 1.03x | 0.82x | - | cap4k
1 B | 268435456 | 256 MB | 1846.6 ms | 39.75x | 39.73x | 48.84x | 48.89x | 46.66x | 39.76x | 64.38x | memset
10 B | 26843545 | 256 MB | 207.6 ms | 4.30x | 4.31x | 5.08x | 5.12x | 4.89x | 4.28x | - | cap4k
100 B | 2684354 | 256 MB | 41.3 ms | 0.86x | 0.86x | 1.00x | 1.02x | 0.98x | 0.86x | - | cap4k
1 KB | 262144 | 256 MB | 37.4 ms | 0.81x | 0.81x | 1.00x | 0.99x | 0.94x | 0.81x | - | cap512
64 KB | 4096 | 256 MB | 42.0 ms | 0.87x | 0.87x | 1.00x | 1.00x | 1.00x | 0.87x | - | master
1 MB | 256 | 256 MB | 32.5 ms | 0.70x | 0.70x | 1.00x | 1.01x | 1.01x | 0.70x | - | cap4k
1 B | 1000 | 1000 B | 6.8 us | 75.86x | 76.51x | 74.68x | 73.84x | 73.86x | 73.86x | 264.24x | memset
1 B | 1000000 | 976.6 KB | 6.8 ms | 90.97x | 91.42x | 110.20x | 123.82x | 91.22x | 91.57x | 134.49x | memset

Discussion
=====

* An early return for source_len = 0 is a clear win.
* Using memset for source_len = 1 also seems to be the best strategy.
* The 'count < 8' branch slows down the operation on my hardware.
* The pure doubling strategy can cause regressions, probably because it
reads back from the destination buffer rather than from the source.
Once the output no longer fits in cache, that adds a full pass of
memory reads that master's loop does not do.
* Capping the doubling helps for larger sources, but costs ~10-15% at
very small counts because of the extra loop structure it needs. The
'nocap' column isolates this: it pays for the structure without ever
engaging the cap, and is still slower than 'nobranch'.
* The best capping value differs between my two systems: the M5 prefers
cap64k, the J5005 prefers a smaller cap.

So, I'd suggest keeping the slen=0 shortcut, adding a memset for slen=1,
dropping the 'count < 8' branch, and using capped doubling with the cap
value still to be settled.

I am curious what numbers other people get from these tests.

Best regards
Jan

--
Jan Nidzwetzki
PlanetScale Postgres Core Team

Attachments:

Makefiletext/plain; charset=UTF-8; name=MakefileDownload
repeat_bench.sqltext/plain; charset=UTF-8; name=repeat_bench.sqlDownload
repeatbench--1.0.sqltext/plain; charset=UTF-8; name=repeatbench--1.0.sqlDownload
repeatbench.ctext/plain; charset=UTF-8; name=repeatbench.cDownload
repeatbench.controltext/plain; charset=UTF-8; name=repeatbench.controlDownload
#7ChenhuiMo
chenhuimo.mch@qq.com
In reply to: ChenhuiMo (#1)
Re:[PATCH] Speed up repeat() for larger counts

Hi Jan,

Thanks for the detailed benchmark and for sharing the repeatbench extension.

I reproduced the same general behavior on my machines. In particular, removing
the small-count branch seems to be a good idea, and the single-byte memset() fast
path gives a clear improvement. I have updated the patch with these two changes.

I also extended your benchmark to test a wider range of block-size caps, from 512 bytes up to 16 MB.

While testing this, I noticed that benchmark ordering can have a noticeable effect on
some of the results. To reduce this effect, I changed the benchmark to rotate the strategy
order between iterations. Each strategy is measured 25 times. The table reports the best
timing, and when the strategy selected by the median differs from the best one, it is shown in parentheses.

I tested on the following two systems:

Intel Core i5-13500H, 32 GB LPDDR5
Intel Core i9-12900H, 64 GB DDR5-4800

I ran the benchmark five times on each system and observed the same overall trends. The tables below show the third run from each system.

On the i5-13500H, the larger-output cases generally prefer a block size around 1-2 MB.
For example, for 256 MB outputs, cap2m is consistently among the best choices for
most source lengths.

source | repeats | output | master | doubling | nocap | cap512 | cap1k | cap2k | cap4k | cap8k | cap16k | cap32k | cap64k | cap128k | cap256k | cap512k | cap1m | cap2m | cap4m | cap8m | cap16m | fastest
--------+-----------+----------+----------+----------+--------+--------+--------+--------+--------+--------+--------+--------+--------+---------+---------+---------+--------+--------+--------+--------+--------+---------------------------
32 B | 2 | 64 B | 3 ns | 1.07x | 0.88x | 0.85x | 0.87x | 0.88x | 0.85x | 0.85x | 0.85x | 0.85x | 0.88x | 0.85x | 0.85x | 0.85x | 0.85x | 0.85x | 0.90x | 0.85x | 0.85x | doubling (median: master)
32 B | 4 | 128 B | 6 ns | 1.16x | 1.09x | 1.12x | 1.12x | 1.12x | 1.12x | 1.08x | 1.12x | 1.12x | 1.08x | 1.12x | 1.12x | 1.12x | 1.12x | 1.08x | 1.12x | 1.12x | 1.12x | doubling
32 B | 6 | 192 B | 9 ns | 1.24x | 1.16x | 1.16x | 1.16x | 1.16x | 1.16x | 1.16x | 1.16x | 1.16x | 1.16x | 1.16x | 1.16x | 1.16x | 1.16x | 1.16x | 1.16x | 1.16x | 1.16x | doubling
32 B | 7 | 224 B | 9 ns | 1.36x | 1.28x | 1.28x | 1.28x | 1.28x | 1.28x | 1.28x | 1.28x | 1.28x | 1.28x | 1.28x | 1.28x | 1.28x | 1.28x | 1.28x | 1.28x | 1.28x | 1.28x | doubling
32 B | 8 | 256 B | 10 ns | 1.51x | 1.42x | 1.42x | 1.42x | 1.42x | 1.42x | 1.42x | 1.42x | 1.42x | 1.42x | 1.42x | 1.42x | 1.42x | 1.42x | 1.42x | 1.42x | 1.42x | 1.42x | doubling
32 B | 9 | 288 B | 13 ns | 1.37x | 1.30x | 1.30x | 1.30x | 1.30x | 1.30x | 1.30x | 1.30x | 1.30x | 1.30x | 1.30x | 1.30x | 1.30x | 1.30x | 1.30x | 1.30x | 1.30x | 1.30x | doubling
32 B | 12 | 384 B | 16 ns | 1.56x | 1.49x | 1.49x | 1.49x | 1.49x | 1.49x | 1.49x | 1.49x | 1.49x | 1.49x | 1.49x | 1.49x | 1.49x | 1.48x | 1.49x | 1.49x | 1.46x | 1.49x | doubling
32 B | 16 | 512 B | 22 ns | 2.18x | 2.12x | 2.12x | 2.05x | 2.12x | 2.12x | 2.12x | 2.12x | 2.12x | 2.12x | 2.12x | 2.12x | 2.13x | 2.12x | 2.12x | 2.12x | 2.12x | 2.12x | doubling
32 B | 64 | 2 KB | 71 ns | 3.62x | 3.56x | 3.28x | 3.57x | 3.57x | 3.56x | 3.57x | 3.58x | 3.56x | 3.57x | 3.57x | 3.57x | 3.57x | 3.57x | 3.57x | 3.57x | 3.57x | 3.56x | doubling (median: cap1k)
32 B | 4096 | 128 KB | 5.7 us | 2.92x | 2.91x | 2.24x | 2.89x | 3.18x | 3.37x | 3.70x | 3.73x | 2.94x | 2.93x | 2.91x | 2.92x | 2.91x | 2.92x | 2.92x | 2.91x | 2.92x | 2.91x | cap16k
1 B | 16777216 | 16 MB | 18.3 ms | 30.41x | 28.14x | 25.33x | 34.68x | 41.80x | 27.21x | 33.67x | 35.85x | 32.59x | 34.50x | 40.08x | 38.09x | 34.90x | 27.07x | 38.68x | 36.72x | 29.04x | 28.75x | cap2k (median: cap2m)
10 B | 1677721 | 16 MB | 1.6 ms | 2.61x | 2.69x | 2.09x | 2.04x | 2.13x | 2.42x | 2.91x | 3.40x | 3.32x | 2.96x | 3.12x | 3.08x | 2.81x | 2.35x | 3.17x | 2.87x | 2.69x | 2.78x | cap16k (median: cap2m)
100 B | 167772 | 16 MB | 899.9 us | 1.43x | 1.53x | 1.21x | 1.27x | 1.04x | 1.36x | 1.77x | 1.81x | 1.71x | 1.82x | 1.83x | 1.78x | 1.45x | 1.96x | 1.83x | 1.55x | 1.50x | 1.50x | cap1m
1 KB | 16384 | 16 MB | 719.9 us | 1.09x | 1.10x | 0.98x | 1.04x | 1.52x | 0.94x | 1.19x | 1.36x | 1.21x | 1.34x | 1.21x | 1.37x | 1.07x | 0.99x | 1.47x | 1.36x | 1.09x | 1.09x | cap2k (median: cap2m)
64 KB | 256 | 16 MB | 608.4 us | 0.92x | 0.93x | 0.85x | 0.80x | 1.03x | 1.28x | 1.22x | 1.16x | 0.97x | 1.04x | 1.01x | 1.09x | 1.10x | 0.88x | 1.21x | 1.18x | 0.95x | 0.93x | cap4k (median: cap2m)
1 MB | 16 | 16 MB | 931.1 us | 1.41x | 1.39x | 1.06x | 1.03x | 1.23x | 1.35x | 1.32x | 1.40x | 1.39x | 1.54x | 1.52x | 1.43x | 1.46x | 1.39x | 1.85x | 1.73x | 1.44x | 1.41x | cap2m
1 B | 268435456 | 256 MB | 303.9 ms | 22.57x | 25.11x | 17.18x | 17.80x | 17.10x | 16.71x | 19.49x | 21.68x | 20.89x | 22.71x | 21.99x | 22.84x | 21.38x | 17.81x | 40.98x | 41.67x | 36.89x | 26.75x | cap4m (median: cap2m)
10 B | 26843545 | 256 MB | 28.4 ms | 2.30x | 2.28x | 1.66x | 1.63x | 1.37x | 1.80x | 2.08x | 2.08x | 1.95x | 2.21x | 2.12x | 2.14x | 2.09x | 1.67x | 3.88x | 3.54x | 3.29x | 2.32x | cap2m
100 B | 2684354 | 256 MB | 17.9 ms | 1.48x | 1.45x | 1.05x | 1.04x | 1.01x | 1.06x | 1.32x | 1.22x | 1.33x | 1.34x | 1.24x | 1.21x | 1.11x | 2.57x | 2.47x | 2.33x | 1.87x | 1.72x | cap1m
1 KB | 262144 | 256 MB | 16.5 ms | 1.41x | 1.35x | 0.96x | 0.94x | 0.95x | 0.95x | 1.05x | 1.23x | 1.16x | 1.20x | 1.26x | 1.21x | 1.11x | 1.00x | 2.29x | 2.25x | 2.11x | 1.51x | cap2m
64 KB | 4096 | 256 MB | 15.4 ms | 1.31x | 1.28x | 1.04x | 1.05x | 0.99x | 1.05x | 1.01x | 1.10x | 1.12x | 1.09x | 1.01x | 1.11x | 1.03x | 0.91x | 2.22x | 2.11x | 1.80x | 1.33x | cap2m
1 MB | 256 | 256 MB | 16.9 ms | 1.41x | 1.38x | 1.02x | 1.08x | 1.05x | 1.04x | 1.06x | 1.05x | 1.04x | 1.04x | 1.11x | 1.04x | 1.07x | 1.05x | 2.49x | 2.32x | 2.14x | 1.42x | cap2m
1 B | 1000 | 1000 B | 1.1 us | 178.24x | 30.42x | 30.57x | 30.64x | 30.61x | 30.63x | 30.64x | 30.66x | 30.71x | 30.61x | 30.40x | 30.38x | 30.00x | 30.04x | 30.02x | 30.03x | 30.00x | 30.02x | doubling
1 B | 1000000 | 976.6 KB | 1.1 ms | 82.97x | 70.75x | 59.01x | 72.08x | 74.37x | 75.58x | 79.78x | 80.32x | 69.05x | 70.56x | 71.15x | 71.37x | 71.60x | 71.53x | 71.55x | 71.41x | 71.64x | 71.81x | doubling (median: cap8k)

The results on the i9-12900H are quite different. For 16 MB outputs, much smaller blocks,
generally from a few KB to a few hundred KB, tend to perform better. However, for 256 MB outputs,
the 16 MB cap is consistently the fastest among the capped variants I tested.

source | repeats | output | master | doubling | nocap | cap512 | cap1k | cap2k | cap4k | cap8k | cap16k | cap32k | cap64k | cap128k | cap256k | cap512k | cap1m | cap2m | cap4m | cap8m | cap16m | fastest
--------+-----------+----------+----------+----------+--------+--------+--------+--------+--------+--------+--------+--------+--------+---------+---------+---------+--------+--------+--------+--------+--------+---------------------------
32 B | 2 | 64 B | 3 ns | 0.96x | 0.82x | 0.77x | 0.77x | 0.77x | 0.77x | 0.77x | 0.77x | 0.77x | 0.77x | 0.77x | 0.77x | 0.77x | 0.79x | 0.81x | 0.77x | 0.79x | 0.77x | master
32 B | 4 | 128 B | 5 ns | 1.09x | 0.96x | 1.00x | 1.00x | 1.00x | 1.00x | 1.00x | 1.00x | 1.00x | 1.00x | 1.00x | 1.00x | 1.00x | 1.00x | 1.00x | 1.00x | 1.00x | 1.00x | doubling
32 B | 6 | 192 B | 7 ns | 1.21x | 1.16x | 1.16x | 1.16x | 1.16x | 1.16x | 1.16x | 1.16x | 1.16x | 1.16x | 1.16x | 1.16x | 1.16x | 1.16x | 1.16x | 1.16x | 1.16x | 1.16x | doubling
32 B | 7 | 224 B | 8 ns | 1.37x | 1.28x | 1.28x | 1.28x | 1.28x | 1.28x | 1.28x | 1.28x | 1.28x | 1.28x | 1.28x | 1.28x | 1.28x | 1.28x | 1.28x | 1.28x | 1.28x | 1.28x | doubling
32 B | 8 | 256 B | 9 ns | 1.54x | 1.44x | 1.44x | 1.44x | 1.44x | 1.44x | 1.44x | 1.44x | 1.44x | 1.44x | 1.44x | 1.44x | 1.44x | 1.44x | 1.44x | 1.44x | 1.44x | 1.44x | doubling
32 B | 9 | 288 B | 17 ns | 1.09x | 1.04x | 1.04x | 1.04x | 1.04x | 1.04x | 1.04x | 1.04x | 1.04x | 1.04x | 1.04x | 1.04x | 1.04x | 1.04x | 1.04x | 1.04x | 1.04x | 1.04x | doubling
32 B | 12 | 384 B | 19 ns | 1.76x | 1.68x | 1.68x | 1.68x | 1.68x | 1.68x | 1.68x | 1.68x | 1.68x | 1.68x | 1.68x | 1.68x | 1.68x | 1.68x | 1.68x | 1.68x | 1.68x | 1.68x | doubling
32 B | 16 | 512 B | 21 ns | 1.82x | 1.82x | 1.80x | 1.80x | 1.80x | 1.82x | 1.80x | 1.80x | 1.82x | 1.80x | 1.82x | 1.80x | 1.80x | 1.80x | 1.80x | 1.80x | 1.82x | 1.80x | cap8m (median: doubling)
32 B | 64 | 2 KB | 68 ns | 2.87x | 2.86x | 2.85x | 2.86x | 2.85x | 2.86x | 2.86x | 2.86x | 2.86x | 2.86x | 2.86x | 2.86x | 2.86x | 2.86x | 2.85x | 2.86x | 2.86x | 2.86x | doubling
32 B | 4096 | 128 KB | 5.5 us | 2.92x | 2.93x | 2.64x | 2.97x | 3.18x | 3.37x | 3.64x | 3.74x | 2.95x | 2.92x | 2.92x | 2.92x | 2.92x | 2.92x | 2.92x | 2.91x | 2.92x | 2.92x | cap16k
1 B | 16777216 | 16 MB | 17.5 ms | 47.30x | 30.19x | 42.27x | 44.17x | 42.53x | 37.90x | 42.22x | 45.73x | 38.79x | 39.85x | 38.99x | 38.71x | 38.13x | 31.69x | 31.59x | 30.84x | 30.10x | 31.77x | doubling (median: cap2k)
10 B | 1677721 | 16 MB | 1.4 ms | 2.51x | 2.50x | 3.61x | 3.65x | 3.03x | 3.40x | 3.49x | 3.75x | 3.24x | 3.23x | 3.25x | 3.02x | 2.85x | 2.59x | 2.64x | 2.56x | 2.57x | 2.64x | cap16k (median: cap1k)
100 B | 167772 | 16 MB | 611.2 us | 1.04x | 1.10x | 1.50x | 1.51x | 1.25x | 1.32x | 1.33x | 1.41x | 1.31x | 1.33x | 1.32x | 1.31x | 1.11x | 1.07x | 1.06x | 1.04x | 1.00x | 1.03x | cap1k
1 KB | 16384 | 16 MB | 394.3 us | 0.70x | 0.68x | 0.92x | 0.94x | 1.00x | 0.89x | 0.94x | 0.99x | 0.85x | 0.88x | 0.87x | 0.86x | 0.83x | 0.68x | 0.67x | 0.67x | 0.71x | 0.71x | cap2k (median: master)
64 KB | 256 | 16 MB | 456.4 us | 0.84x | 0.82x | 0.96x | 0.97x | 0.97x | 0.98x | 0.98x | 0.98x | 0.98x | 1.02x | 1.05x | 1.05x | 0.95x | 0.81x | 0.81x | 0.78x | 0.78x | 0.80x | cap256k (median: cap128k)
1 MB | 16 | 16 MB | 621.1 us | 1.05x | 1.07x | 1.01x | 1.02x | 1.01x | 1.00x | 1.02x | 1.05x | 1.02x | 1.07x | 1.09x | 1.05x | 1.04x | 1.04x | 1.08x | 1.08x | 1.04x | 1.09x | cap128k (median: cap64k)
1 B | 268435456 | 256 MB | 290.5 ms | 21.37x | 23.29x | 20.26x | 20.73x | 20.32x | 18.96x | 19.77x | 20.46x | 20.84x | 20.64x | 20.74x | 21.04x | 20.18x | 18.12x | 18.36x | 18.30x | 18.38x | 31.81x | cap16m
10 B | 26843545 | 256 MB | 26.1 ms | 1.98x | 2.02x | 1.82x | 1.81x | 1.52x | 1.75x | 1.81x | 1.86x | 1.86x | 1.88x | 1.88x | 1.87x | 1.80x | 1.69x | 1.67x | 1.61x | 1.54x | 2.30x | cap16m
100 B | 2684354 | 256 MB | 15.8 ms | 1.25x | 1.23x | 1.08x | 1.08x | 0.93x | 1.04x | 1.09x | 1.11x | 1.11x | 1.14x | 1.10x | 1.10x | 1.04x | 1.00x | 1.00x | 0.98x | 0.90x | 1.27x | cap16m
1 KB | 262144 | 256 MB | 14.3 ms | 1.12x | 1.11x | 0.98x | 0.96x | 0.98x | 0.92x | 0.97x | 0.99x | 0.99x | 1.01x | 1.03x | 1.03x | 1.00x | 0.90x | 0.89x | 0.89x | 0.87x | 1.55x | cap16m
64 KB | 4096 | 256 MB | 14.2 ms | 1.08x | 1.10x | 0.99x | 1.00x | 0.99x | 0.99x | 0.99x | 1.00x | 0.99x | 0.98x | 1.01x | 1.02x | 1.00x | 0.89x | 0.88x | 0.87x | 0.86x | 1.42x | cap16m
1 MB | 256 | 256 MB | 15.4 ms | 1.22x | 1.22x | 0.99x | 0.98x | 0.98x | 0.98x | 1.00x | 0.99x | 1.00x | 0.97x | 0.99x | 0.99x | 0.99x | 0.99x | 0.97x | 0.98x | 0.94x | 1.68x | cap16m
1 B | 1000 | 1000 B | 1.0 us | 188.01x | 25.89x | 26.12x | 25.94x | 25.97x | 25.94x | 25.94x | 25.93x | 25.94x | 25.94x | 25.98x | 25.97x | 25.98x | 25.99x | 25.92x | 25.92x | 25.99x | 26.11x | doubling
1 B | 1000000 | 976.6 KB | 1.0 ms | 80.26x | 64.00x | 61.04x | 66.58x | 69.91x | 70.56x | 74.70x | 77.56x | 63.71x | 65.65x | 65.98x | 66.34x | 66.17x | 66.59x | 66.89x | 67.00x | 67.19x | 67.30x | doubling (median: cap16k)

So it seems that the performance-optimal block size depends not only on the machine,
but also on the total output size on the same machine. This makes me hesitant to
choose a fixed cap primarily as a performance optimization.

If you have time, could you also run the wider cap sweep on your M5 Max and J5005 systems?
It would be very useful to see whether the preferred block size changes with the output size there as well.

This also makes me wonder whether the cap should mainly be chosen to bound the interval
between CHECK_FOR_INTERRUPTS() calls, rather than to optimize memcpy() throughput. Any
performance improvement from the cap could then be treated as a secondary benefit.

The attached patch contains the memset() fast path and removes the small-count branch. I have
not added a block-size cap yet, since I would like to get more data and feedback before choosing one.

I have also attached the updated repeatbench extension used for the tests above.

Thanks,
Chenhui

Attachments:

t253193_7
0001-Speed-up-repeat-for-larger-counts.patchapplication/octet-stream; charset=utf-8; name=0001-Speed-up-repeat-for-larger-counts.patchDownload+31-5
Makefileapplication/octet-stream; charset=utf-8; name=MakefileDownload
repeat_bench.sqlapplication/octet-stream; charset=utf-8; name=repeat_bench.sqlDownload
repeatbench.capplication/octet-stream; charset=utf-8; name=repeatbench.cDownload
repeatbench.controlapplication/octet-stream; charset=utf-8; name=repeatbench.controlDownload
repeatbench--1.0.sqlapplication/octet-stream; charset=utf-8; name=repeatbench--1.0.sqlDownload
#8Jan Nidzwetzki
jan@planetscale.com
In reply to: ChenhuiMo (#7)
Re: Re:[PATCH] Speed up repeat() for larger counts

Hello Chenhui,

On 26.08.26 17:54, ChenhuiMo wrote:
[...]> I also extended your benchmark to test a wider range of block-size caps, from 512 bytes up to 16 MB.

While testing this, I noticed that benchmark ordering can have a noticeable effect on
some of the results. To reduce this effect, I changed the benchmark to rotate the strategy
order between iterations. Each strategy is measured 25 times. The table reports the best
timing, and when the strategy selected by the median differs from the best one, it is shown in parentheses.

[...]>

If you have time, could you also run the wider cap sweep on your M5 Max and J5005 systems?
It would be very useful to see whether the preferred block size changes with the output size there as well.

Thanks for the numbers and the updated benchmark. I looked into the
benchmark and saw that strat_double() has the "if (slen == 1)" -> memset
fast path. However, capped() is missing this, making the results for
slen == 1 hard to compare.

However, to get a comparable result with your data, I reran your
benchmark version on my machines without any modification. I also
took the results of the third run to do the same as you did.
Here are the numbers:

# Debian 13.5 VM on an Apple M5 Max, 32MB of L2 cache / 24 MB of L3
# cache, 36 GB RAM, gcc 14.2.0 -O2
jan=# select * from repeat_bench();
source | repeats | output | master | doubling | nocap | cap512 | cap1k | cap2k | cap4k | cap8k | cap16k | cap32k | cap64k | cap128k | cap256k | cap512k | cap1m | cap2m | cap4m | cap8m | cap16m | fastest
--------+-----------+----------+----------+----------+--------+--------+---------+---------+---------+--------+--------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+----------------------------
32 B | 2 | 64 B | 3 ns | 0.95x | 0.86x | 0.86x | 0.86x | 0.86x | 0.86x | 0.86x | 0.86x | 0.86x | 0.86x | 0.86x | 0.86x | 0.86x | 0.86x | 0.86x | 0.86x | 0.86x | 0.86x | master
32 B | 4 | 128 B | 5 ns | 1.31x | 1.13x | 1.06x | 1.05x | 1.09x | 1.11x | 1.05x | 1.06x | 1.13x | 1.13x | 1.13x | 1.12x | 1.11x | 1.08x | 1.13x | 1.13x | 1.13x | 1.13x | doubling
32 B | 6 | 192 B | 7 ns | 1.56x | 1.42x | 1.42x | 1.42x | 1.42x | 1.42x | 1.42x | 1.42x | 1.42x | 1.42x | 1.42x | 1.42x | 1.42x | 1.42x | 1.42x | 1.42x | 1.42x | 1.42x | doubling
32 B | 7 | 224 B | 7 ns | 1.65x | 1.50x | 1.50x | 1.50x | 1.50x | 1.50x | 1.50x | 1.50x | 1.49x | 1.50x | 1.50x | 1.50x | 1.50x | 1.50x | 1.50x | 1.50x | 1.50x | 1.50x | doubling
32 B | 8 | 256 B | 7 ns | 1.47x | 1.63x | 1.64x | 1.64x | 1.64x | 1.65x | 1.64x | 1.64x | 1.64x | 1.64x | 1.65x | 1.64x | 1.65x | 1.65x | 1.64x | 1.65x | 1.65x | 1.65x | cap1m (median: cap512k)
32 B | 9 | 288 B | 8 ns | 1.58x | 1.53x | 1.54x | 1.55x | 1.53x | 1.56x | 1.55x | 1.55x | 1.55x | 1.56x | 1.55x | 1.55x | 1.55x | 1.55x | 1.56x | 1.55x | 1.55x | 1.55x | doubling
32 B | 12 | 384 B | 9 ns | 1.25x | 1.42x | 1.50x | 1.45x | 1.50x | 1.50x | 1.47x | 1.50x | 1.50x | 1.50x | 1.50x | 1.50x | 1.50x | 1.50x | 1.50x | 1.47x | 1.50x | 1.50x | cap32k (median: cap512k)
32 B | 16 | 512 B | 12 ns | 1.43x | 1.23x | 1.24x | 1.24x | 1.29x | 1.23x | 1.29x | 1.37x | 1.29x | 1.30x | 1.32x | 1.32x | 1.56x | 1.29x | 1.32x | 1.31x | 1.28x | 1.27x | cap512k (median: cap8m)
32 B | 64 | 2 KB | 49 ns | 2.28x | 2.14x | 2.13x | 2.14x | 2.25x | 2.26x | 2.26x | 2.25x | 2.26x | 2.26x | 2.26x | 2.26x | 2.27x | 2.26x | 2.26x | 2.25x | 2.26x | 2.27x | doubling
32 B | 4096 | 128 KB | 2.7 us | 2.55x | 2.93x | 2.26x | 2.43x | 2.52x | 2.48x | 2.69x | 2.57x | 2.58x | 2.61x | 2.61x | 2.58x | 2.57x | 2.57x | 2.58x | 2.55x | 2.57x | 2.57x | nocap (median: cap8k)
1 B | 16777216 | 16 MB | 20.3 ms | 162.97x | 81.08x | 88.45x | 92.71x | 93.29x | 85.72x | 55.57x | 81.64x | 101.95x | 131.04x | 128.11x | 82.96x | 83.51x | 82.80x | 81.45x | 80.97x | 80.68x | 81.09x | doubling
10 B | 1677721 | 16 MB | 1.5 ms | 5.85x | 5.86x | 6.60x | 6.72x | 5.52x | 5.05x | 4.74x | 6.25x | 8.29x | 9.84x | 5.90x | 5.99x | 6.04x | 5.90x | 5.86x | 5.84x | 5.85x | 5.86x | cap64k
100 B | 167772 | 16 MB | 324.1 us | 1.30x | 1.30x | 1.46x | 1.51x | 1.42x | 0.76x | 1.21x | 1.61x | 2.01x | 2.26x | 1.32x | 1.33x | 1.33x | 1.30x | 1.30x | 1.30x | 1.30x | 1.30x | cap64k
1 KB | 16384 | 16 MB | 266.7 us | 1.06x | 1.07x | 1.04x | 1.04x | 1.21x | 0.57x | 0.75x | 1.06x | 1.34x | 1.72x | 1.68x | 1.09x | 1.10x | 1.09x | 1.07x | 1.07x | 1.07x | 1.07x | cap64k
64 KB | 256 | 16 MB | 211.6 us | 0.85x | 0.85x | 1.00x | 1.01x | 1.01x | 1.00x | 1.01x | 1.00x | 1.00x | 1.01x | 1.34x | 0.86x | 0.87x | 0.86x | 0.85x | 0.85x | 0.85x | 0.85x | cap128k
1 MB | 16 | 16 MB | 249.0 us | 0.99x | 0.99x | 1.00x | 1.01x | 1.01x | 1.01x | 1.01x | 1.01x | 1.01x | 1.01x | 1.01x | 1.01x | 1.01x | 1.01x | 0.99x | 0.99x | 0.99x | 0.99x | cap2k (median: cap64k)
1 B | 268435456 | 256 MB | 314.8 ms | 127.27x | 70.07x | 69.82x | 70.78x | 72.29x | 68.54x | 49.93x | 38.73x | 49.94x | 72.26x | 97.35x | 73.74x | 79.98x | 78.85x | 77.65x | 77.60x | 77.37x | 70.09x | doubling (median: cap128k)
10 B | 26843545 | 256 MB | 23.6 ms | 5.38x | 5.42x | 5.54x | 5.53x | 5.34x | 4.96x | 3.91x | 2.90x | 4.22x | 6.26x | 5.74x | 5.84x | 5.90x | 5.88x | 5.84x | 5.84x | 5.76x | 5.42x | cap64k
100 B | 2684354 | 256 MB | 5.3 ms | 1.22x | 1.23x | 1.24x | 1.25x | 1.18x | 0.73x | 0.84x | 0.72x | 1.04x | 1.55x | 1.25x | 1.32x | 1.35x | 1.31x | 1.31x | 1.30x | 1.18x | 1.20x | cap64k
1 KB | 262144 | 256 MB | 4.8 ms | 1.11x | 1.11x | 1.08x | 1.06x | 1.11x | 1.06x | 0.76x | 0.60x | 0.76x | 1.10x | 1.40x | 1.12x | 1.18x | 1.19x | 1.17x | 1.18x | 1.18x | 1.07x | cap128k
64 KB | 4096 | 256 MB | 4.7 ms | 1.08x | 1.09x | 1.00x | 1.02x | 1.01x | 1.02x | 1.03x | 1.00x | 1.03x | 1.02x | 1.39x | 1.10x | 1.16x | 1.17x | 1.15x | 1.16x | 1.15x | 1.11x | cap128k
1 MB | 256 | 256 MB | 4.2 ms | 0.97x | 0.97x | 1.00x | 1.03x | 1.03x | 1.03x | 1.05x | 1.07x | 1.06x | 1.04x | 1.04x | 1.03x | 1.03x | 1.04x | 1.04x | 1.03x | 1.03x | 0.94x | cap16k (median: cap1m)
1 B | 1000 | 1000 B | 1.3 us | 165.55x | 75.87x | 75.00x | 75.00x | 75.18x | 76.58x | 75.70x | 76.94x | 76.41x | 76.94x | 77.30x | 77.67x | 77.49x | 77.49x | 78.23x | 77.49x | 77.12x | 75.00x | doubling
1 B | 1000000 | 976.6 KB | 1.3 ms | 176.89x | 99.08x | 99.07x | 102.31x | 103.32x | 100.34x | 69.11x | 97.84x | 123.74x | 140.39x | 141.03x | 107.21x | 106.84x | 100.67x | 100.34x | 100.03x | 100.67x | 100.67x | doubling
(24 rows)

# Debian 13.5 on an Intel Pentium Silver J5005, 4 MB L2 cache, no L3
# cache, 16 GB RAM, gcc 14.2.0 -O2
jan=# select * from repeat_bench();
source | repeats | output | master | doubling | nocap | cap512 | cap1k | cap2k | cap4k | cap8k | cap16k | cap32k | cap64k | cap128k | cap256k | cap512k | cap1m | cap2m | cap4m | cap8m | cap16m | fastest
--------+-----------+----------+-----------+----------+--------+---------+---------+---------+---------+---------+---------+--------+--------+---------+---------+---------+--------+--------+--------+--------+--------+-------------------------
32 B | 2 | 64 B | 12 ns | 1.03x | 0.83x | 0.83x | 0.83x | 0.83x | 0.83x | 0.83x | 0.83x | 0.83x | 0.83x | 0.83x | 0.83x | 0.83x | 0.83x | 0.83x | 0.83x | 0.83x | 0.83x | doubling
32 B | 4 | 128 B | 21 ns | 1.18x | 1.00x | 1.00x | 1.00x | 1.00x | 1.00x | 1.00x | 1.00x | 1.00x | 1.00x | 1.00x | 1.00x | 1.00x | 1.00x | 1.00x | 1.00x | 1.00x | 1.00x | doubling
32 B | 6 | 192 B | 31 ns | 1.23x | 1.07x | 1.07x | 1.07x | 1.07x | 1.07x | 1.07x | 1.07x | 1.07x | 1.07x | 1.07x | 1.07x | 1.07x | 1.07x | 1.07x | 1.07x | 1.07x | 1.07x | doubling
32 B | 7 | 224 B | 35 ns | 1.43x | 1.25x | 1.25x | 1.25x | 1.25x | 1.25x | 1.25x | 1.25x | 1.25x | 1.25x | 1.25x | 1.25x | 1.25x | 1.25x | 1.25x | 1.25x | 1.25x | 1.25x | doubling
32 B | 8 | 256 B | 40 ns | 1.62x | 1.42x | 1.42x | 1.42x | 1.42x | 1.42x | 1.42x | 1.42x | 1.42x | 1.42x | 1.42x | 1.42x | 1.42x | 1.42x | 1.42x | 1.42x | 1.42x | 1.42x | doubling
32 B | 9 | 288 B | 45 ns | 1.52x | 1.34x | 1.34x | 1.34x | 1.34x | 1.34x | 1.34x | 1.34x | 1.34x | 1.34x | 1.34x | 1.34x | 1.34x | 1.34x | 1.34x | 1.34x | 1.34x | 1.34x | doubling
32 B | 12 | 384 B | 59 ns | 1.89x | 1.67x | 1.67x | 1.67x | 1.67x | 1.67x | 1.67x | 1.67x | 1.67x | 1.67x | 1.67x | 1.67x | 1.67x | 1.67x | 1.67x | 1.67x | 1.67x | 1.67x | doubling
32 B | 16 | 512 B | 83 ns | 2.31x | 2.08x | 2.08x | 2.08x | 2.08x | 2.08x | 2.08x | 2.08x | 2.08x | 2.08x | 2.08x | 2.08x | 2.08x | 2.08x | 2.08x | 2.08x | 2.08x | 2.08x | doubling
32 B | 64 | 2 KB | 316 ns | 3.79x | 3.59x | 3.36x | 3.59x | 3.59x | 3.59x | 3.59x | 3.59x | 3.59x | 3.59x | 3.59x | 3.59x | 3.59x | 3.59x | 3.59x | 3.59x | 3.59x | 3.59x | doubling
32 B | 4096 | 128 KB | 19.5 us | 2.11x | 2.12x | 2.83x | 3.00x | 3.09x | 2.88x | 2.93x | 2.63x | 2.15x | 2.12x | 2.11x | 2.11x | 2.11x | 2.11x | 2.11x | 2.11x | 2.11x | 2.11x | cap2k
1 B | 16777216 | 16 MB | 114.9 ms | 56.25x | 43.67x | 51.54x | 50.64x | 41.78x | 49.36x | 49.53x | 48.76x | 47.56x | 47.26x | 47.26x | 47.51x | 47.52x | 52.33x | 56.72x | 46.89x | 43.29x | 43.32x | cap2m
10 B | 1677721 | 16 MB | 12.8 ms | 4.67x | 4.83x | 5.89x | 5.76x | 5.48x | 5.50x | 5.51x | 5.38x | 5.31x | 5.27x | 5.27x | 5.28x | 5.33x | 6.06x | 6.14x | 4.93x | 4.87x | 4.84x | cap2m
100 B | 167772 | 16 MB | 2.2 ms | 0.84x | 0.85x | 1.04x | 1.00x | 0.94x | 0.93x | 0.93x | 0.90x | 0.89x | 0.90x | 0.90x | 0.90x | 0.90x | 1.02x | 1.00x | 0.85x | 0.85x | 0.85x | cap512
1 KB | 16384 | 16 MB | 2.3 ms | 0.87x | 0.87x | 1.00x | 1.00x | 0.84x | 0.99x | 0.99x | 0.99x | 0.95x | 0.94x | 0.95x | 0.95x | 0.95x | 1.03x | 1.14x | 0.93x | 0.87x | 0.87x | cap2m
64 KB | 256 | 16 MB | 2.4 ms | 0.87x | 0.89x | 1.00x | 1.00x | 0.97x | 0.97x | 0.96x | 0.96x | 0.97x | 0.97x | 0.98x | 0.97x | 0.98x | 1.05x | 1.15x | 0.95x | 0.89x | 0.89x | cap2m
1 MB | 16 | 16 MB | 2.3 ms | 0.87x | 0.86x | 1.01x | 1.08x | 1.08x | 1.08x | 1.08x | 1.08x | 1.08x | 1.08x | 1.08x | 1.08x | 1.08x | 1.08x | 1.07x | 0.91x | 0.83x | 0.85x | cap64k (median: cap1m)
1 B | 268435456 | 256 MB | 1849.7 ms | 63.78x | 39.96x | 48.86x | 42.71x | 34.30x | 49.08x | 48.94x | 48.57x | 46.89x | 46.73x | 46.64x | 46.83x | 46.89x | 56.84x | 56.49x | 45.49x | 40.60x | 40.35x | doubling
10 B | 26843545 | 256 MB | 207.7 ms | 4.48x | 4.52x | 5.46x | 5.24x | 5.50x | 5.49x | 5.47x | 5.38x | 5.25x | 5.25x | 5.24x | 5.24x | 5.25x | 6.39x | 6.39x | 4.73x | 4.53x | 4.48x | cap1m
100 B | 2684354 | 256 MB | 38.6 ms | 0.83x | 0.83x | 1.01x | 0.97x | 1.02x | 1.02x | 1.01x | 0.98x | 0.98x | 0.98x | 0.97x | 0.98x | 0.97x | 1.19x | 1.18x | 0.85x | 0.85x | 0.83x | cap1m
1 KB | 262144 | 256 MB | 37.4 ms | 0.80x | 0.81x | 1.00x | 0.99x | 0.70x | 0.99x | 0.99x | 0.98x | 0.95x | 0.94x | 0.95x | 0.95x | 0.95x | 1.15x | 1.15x | 1.03x | 0.82x | 0.82x | cap2m
64 KB | 4096 | 256 MB | 39.1 ms | 0.85x | 0.85x | 1.00x | 1.00x | 1.00x | 1.00x | 1.00x | 1.00x | 1.00x | 1.00x | 0.99x | 0.99x | 0.99x | 1.20x | 1.20x | 1.03x | 0.86x | 0.86x | cap2m
1 MB | 256 | 256 MB | 32.6 ms | 0.68x | 0.68x | 1.00x | 1.01x | 1.01x | 1.01x | 1.01x | 1.01x | 1.01x | 1.01x | 1.01x | 1.01x | 1.01x | 1.01x | 1.00x | 0.88x | 0.67x | 0.67x | cap512k (median: cap1m)
1 B | 1000 | 1000 B | 6.8 us | 240.81x | 74.12x | 74.42x | 74.14x | 74.15x | 74.15x | 74.15x | 74.15x | 74.15x | 74.15x | 74.15x | 74.14x | 74.15x | 74.15x | 74.15x | 74.15x | 74.15x | 74.15x | doubling
1 B | 1000000 | 976.6 KB | 6.8 ms | 134.60x | 91.03x | 113.81x | 126.83x | 131.21x | 125.34x | 127.56x | 117.56x | 94.91x | 91.75x | 91.65x | 91.60x | 91.57x | 91.57x | 91.53x | 91.53x | 91.45x | 91.57x | doubling
(24 rows)

Regarding your question whether the preferred block size changes with
the output size: in my measurements, for source lengths from 10 B to 64
KB, it changes with the machine rather than with the output size.

16 MB output 256 MB output
M5 Max 64k, 64k, 64k, 128k 64k, 64k, 128k, 128k
J5005 2m, 512, 2m, 2m 1m, 1m, 2m, 2m

The M5 prefers something around 64 KB - 128 KB at both output sizes,
and the J5005 prefers 1 MB - 2 MB. So the preference looks roughly
stable across output sizes here. However, the two machines are a
factor of 16 apart from each other. Together with your i5 and i9
numbers, the optimum now spans 64 KB to 16 MB.

I am curious what other people see when repeating these benchmarks. So
far, I don't see a good recommendation for a particular block size.

Best regards
Jan

--
Jan Nidzwetzki
PlanetScale Postgres Core Team

#9ChenhuiMo
chenhuimo.mch@qq.com
In reply to: Jan Nidzwetzki (#8)
Re:[PATCH] Speed up repeat() for larger counts

Hi Jan,

Thanks for pointing this out.

I updated the benchmark so that the capped strategies also use memset() for
single-byte inputs. In that path, the memset() calls are split according to
the selected block size, with CHECK_FOR_INTERRUPTS() between chunks.

I reran the benchmark three times on the same system and saw the same overall
trend in each run. The table below shows the second run.

source | repeats | output | master | doubling | nocap | cap512 | cap1k | cap2k | cap4k | cap8k | cap16k | cap32k | cap64k | cap128k | cap256k | cap512k | cap1m | cap2m | cap4m | cap8m | cap16m | fastest
--------+-----------+----------+----------+----------+--------+--------+--------+--------+--------+--------+--------+--------+--------+---------+---------+---------+--------+--------+--------+--------+--------+---------------------------
32 B | 2 | 64 B | 3 ns | 0.97x | 0.89x | 0.83x | 0.83x | 0.83x | 0.83x | 0.83x | 0.86x | 0.89x | 0.89x | 0.89x | 0.89x | 0.89x | 0.89x | 0.89x | 0.89x | 0.89x | 0.89x | master (median: doubling)
32 B | 4 | 128 B | 13 ns | 1.73x | 1.56x | 1.56x | 1.56x | 1.56x | 1.56x | 1.56x | 1.56x | 1.56x | 1.56x | 1.56x | 1.56x | 1.56x | 1.56x | 1.56x | 1.56x | 1.56x | 1.56x | doubling
32 B | 6 | 192 B | 15 ns | 1.67x | 1.59x | 1.59x | 1.59x | 1.59x | 1.59x | 1.59x | 1.59x | 1.59x | 1.59x | 1.59x | 1.59x | 1.59x | 1.59x | 1.59x | 1.59x | 1.59x | 1.59x | doubling
32 B | 7 | 224 B | 16 ns | 1.17x | 1.12x | 1.12x | 1.12x | 1.19x | 1.19x | 1.19x | 1.19x | 1.19x | 1.19x | 1.19x | 1.19x | 1.19x | 1.19x | 1.19x | 1.19x | 1.12x | 1.14x | cap8k (median: doubling)
32 B | 8 | 256 B | 15 ns | 1.19x | 1.15x | 1.15x | 1.15x | 1.15x | 1.15x | 1.15x | 1.15x | 1.15x | 1.15x | 1.15x | 1.15x | 1.15x | 1.15x | 1.15x | 1.15x | 1.15x | 1.15x | doubling
32 B | 9 | 288 B | 17 ns | 1.19x | 1.16x | 1.16x | 1.16x | 1.16x | 1.16x | 1.16x | 1.16x | 1.16x | 1.16x | 1.16x | 1.16x | 1.16x | 1.16x | 1.16x | 1.16x | 1.16x | 1.16x | doubling
32 B | 12 | 384 B | 19 ns | 1.27x | 1.25x | 1.25x | 1.25x | 1.25x | 1.25x | 1.25x | 1.25x | 1.25x | 1.25x | 1.25x | 1.25x | 1.25x | 1.25x | 1.25x | 1.25x | 1.25x | 1.25x | doubling
32 B | 16 | 512 B | 21 ns | 1.37x | 1.35x | 1.35x | 1.35x | 1.35x | 1.35x | 1.35x | 1.35x | 1.35x | 1.35x | 1.35x | 1.35x | 1.35x | 1.35x | 1.35x | 1.35x | 1.35x | 1.35x | doubling
32 B | 64 | 2 KB | 65 ns | 2.45x | 2.19x | 2.19x | 2.28x | 2.19x | 2.19x | 2.19x | 2.19x | 2.20x | 2.19x | 2.19x | 2.19x | 2.19x | 2.19x | 2.19x | 2.19x | 2.19x | 2.19x | doubling
32 B | 4096 | 128 KB | 5.7 us | 2.90x | 2.90x | 2.52x | 2.83x | 3.06x | 3.36x | 3.69x | 3.73x | 2.94x | 2.91x | 2.91x | 2.91x | 2.91x | 2.91x | 2.91x | 2.91x | 2.91x | 2.90x | cap16k
1 B | 16777216 | 16 MB | 17.3 ms | 37.90x | 42.60x | 39.76x | 46.97x | 47.77x | 43.82x | 45.25x | 47.21x | 49.36x | 50.08x | 49.54x | 50.40x | 50.46x | 49.79x | 49.15x | 50.24x | 50.88x | 50.12x | cap8m (median: cap4m)
10 B | 1677721 | 16 MB | 1.4 ms | 2.47x | 2.64x | 3.39x | 3.27x | 2.87x | 3.36x | 3.54x | 3.64x | 3.22x | 3.29x | 3.22x | 3.30x | 2.88x | 2.25x | 2.28x | 2.55x | 2.30x | 2.67x | cap16k
100 B | 167772 | 16 MB | 557.6 us | 1.08x | 0.97x | 1.36x | 1.41x | 1.31x | 1.36x | 1.47x | 1.34x | 1.34x | 1.34x | 1.31x | 1.30x | 1.15x | 1.01x | 1.02x | 1.04x | 1.09x | 1.06x | cap8k (median: cap16k)
1 KB | 16384 | 16 MB | 374.2 us | 0.72x | 0.73x | 1.02x | 1.02x | 1.05x | 0.96x | 1.00x | 1.04x | 0.89x | 0.88x | 0.89x | 0.91x | 0.87x | 0.70x | 0.73x | 0.72x | 0.71x | 0.72x | cap2k (median: cap512)
64 KB | 256 | 16 MB | 525.7 us | 0.88x | 0.77x | 0.96x | 1.02x | 1.19x | 1.02x | 1.21x | 1.18x | 1.20x | 1.12x | 1.04x | 1.09x | 1.03x | 0.85x | 0.86x | 0.78x | 0.83x | 0.88x | cap8k (median: cap128k)
1 MB | 16 | 16 MB | 577.6 us | 1.01x | 1.01x | 1.00x | 1.03x | 1.00x | 1.03x | 1.02x | 1.01x | 1.04x | 1.05x | 1.03x | 1.05x | 1.06x | 1.03x | 1.09x | 1.07x | 1.09x | 1.09x | cap2m (median: cap8m)
1 B | 268435456 | 256 MB | 296.9 ms | 20.72x | 22.10x | 18.76x | 18.77x | 18.95x | 17.11x | 18.43x | 19.68x | 20.18x | 20.26x | 20.40x | 20.56x | 20.57x | 20.40x | 20.82x | 20.58x | 20.50x | 20.96x | nocap (median: cap16m)
10 B | 26843545 | 256 MB | 27.1 ms | 1.99x | 1.98x | 1.75x | 1.80x | 1.61x | 1.70x | 1.81x | 1.81x | 1.83x | 1.82x | 1.84x | 1.82x | 1.74x | 1.59x | 1.59x | 1.64x | 1.61x | 2.67x | cap16m
100 B | 2684354 | 256 MB | 16.7 ms | 1.14x | 1.19x | 1.04x | 1.09x | 0.92x | 1.02x | 1.07x | 1.10x | 1.12x | 1.14x | 1.13x | 1.12x | 1.01x | 0.99x | 0.98x | 0.99x | 0.90x | 1.36x | cap16m
1 KB | 262144 | 256 MB | 14.7 ms | 1.11x | 1.10x | 0.96x | 0.94x | 0.94x | 0.89x | 0.93x | 0.97x | 0.96x | 0.98x | 1.00x | 0.98x | 0.98x | 0.89x | 0.88x | 0.90x | 0.86x | 1.56x | cap16m
64 KB | 4096 | 256 MB | 15.3 ms | 1.14x | 1.16x | 0.98x | 0.98x | 0.98x | 1.00x | 0.98x | 0.99x | 1.03x | 0.98x | 1.04x | 1.02x | 1.00x | 0.92x | 0.94x | 0.94x | 0.89x | 1.66x | cap16m
1 MB | 256 | 256 MB | 16.6 ms | 1.25x | 1.22x | 0.98x | 1.00x | 1.00x | 0.98x | 0.99x | 0.99x | 1.01x | 0.99x | 0.99x | 0.99x | 1.00x | 0.99x | 0.97x | 0.97x | 0.97x | 1.95x | cap16m
1 B | 1000 | 1000 B | 1.1 us | 97.38x | 93.94x | 77.97x | 93.95x | 93.91x | 93.93x | 93.95x | 93.91x | 93.94x | 93.95x | 93.95x | 93.92x | 93.95x | 93.91x | 93.96x | 93.93x | 93.95x | 93.94x | doubling
1 B | 1000000 | 976.6 KB | 1.1 ms | 78.84x | 79.60x | 52.97x | 60.48x | 69.16x | 71.61x | 75.43x | 78.24x | 79.51x | 79.80x | 80.01x | 80.62x | 80.66x | 80.86x | 80.95x | 80.92x | 81.06x | 80.86x | cap8m (median: cap16m)
(24 rows)

The corrected single-byte results do not change the overall conclusion. For
multi-byte inputs, the preferred cap on this machine still varies noticeably
with the total output size. In particular, the 16 MB cases tend to prefer
relatively small caps, while the 256 MB cases consistently prefer cap16m.

For the single-byte cases, splitting the memset() into capped chunks also does
not seem to introduce a significant performance penalty. In some cases, the
larger capped memset variants are even slightly faster than a single large
memset().

I have attached the updated repeatbench.c. The other extension files are
unchanged.

I am also curious to see what results others get on different systems.

Thanks,
Chenhui

Attachments:

repeatbench.capplication/octet-stream; charset=utf-8; name=repeatbench.cDownload
#10Jan Nidzwetzki
jan@planetscale.com
In reply to: ChenhuiMo (#9)
Re: Re:[PATCH] Speed up repeat() for larger counts

Hello Chenhui,

On 03.09.26 17:20, ChenhuiMo wrote:
[...]

I updated the benchmark so that the capped strategies also use memset() for
single-byte inputs. In that path, the memset() calls are split according to
the selected block size, with CHECK_FOR_INTERRUPTS() between chunks

Thanks for the updated benchmark. For comparison, here are my numbers:

# Debian 13.5 VM on an Apple M5 Max, 32MB of L2 cache / 24 MB of L3
# cache, 36 GB RAM, gcc 14.2.0 -O2

jan=# select * from repeat_bench();
source | repeats | output | master | doubling | nocap | cap512 | cap1k | cap2k | cap4k | cap8k | cap16k | cap32k | cap64k | cap128k | cap256k | cap512k | cap1m | cap2m | cap4m | cap8m | cap16m | fastest
--------+-----------+----------+----------+----------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------------------------
32 B | 2 | 64 B | 3 ns | 0.93x | 0.82x | 0.82x | 0.82x | 0.82x | 0.82x | 0.82x | 0.82x | 0.82x | 0.82x | 0.82x | 0.82x | 0.82x | 0.82x | 0.82x | 0.82x | 0.82x | 0.82x | master
32 B | 4 | 128 B | 5 ns | 1.31x | 1.13x | 1.13x | 1.13x | 1.13x | 1.13x | 1.13x | 1.13x | 1.13x | 1.13x | 1.13x | 1.13x | 1.13x | 1.13x | 1.13x | 1.13x | 1.13x | 1.13x | doubling
32 B | 6 | 192 B | 7 ns | 1.59x | 1.42x | 1.42x | 1.42x | 1.42x | 1.42x | 1.42x | 1.42x | 1.42x | 1.42x | 1.42x | 1.42x | 1.42x | 1.42x | 1.42x | 1.42x | 1.42x | 1.42x | doubling
32 B | 7 | 224 B | 7 ns | 1.67x | 1.50x | 1.50x | 1.50x | 1.50x | 1.50x | 1.50x | 1.50x | 1.50x | 1.50x | 1.50x | 1.50x | 1.50x | 1.50x | 1.50x | 1.50x | 1.50x | 1.50x | doubling
32 B | 8 | 256 B | 7 ns | 1.56x | 1.65x | 1.65x | 1.65x | 1.65x | 1.65x | 1.65x | 1.65x | 1.65x | 1.65x | 1.65x | 1.65x | 1.65x | 1.65x | 1.65x | 1.65x | 1.65x | 1.65x | cap2k (median: nocap)
32 B | 9 | 288 B | 8 ns | 1.71x | 1.57x | 1.56x | 1.57x | 1.57x | 1.57x | 1.56x | 1.57x | 1.57x | 1.57x | 1.56x | 1.57x | 1.56x | 1.56x | 1.57x | 1.57x | 1.57x | 1.57x | doubling
32 B | 12 | 384 B | 9 ns | 1.27x | 1.52x | 1.44x | 1.55x | 1.40x | 1.56x | 1.55x | 1.56x | 1.55x | 1.46x | 1.40x | 1.46x | 1.43x | 1.55x | 1.40x | 1.56x | 1.55x | 1.56x | cap16k (median: cap1k)
32 B | 16 | 512 B | 12 ns | 1.43x | 1.24x | 1.24x | 1.24x | 1.25x | 1.24x | 1.25x | 1.25x | 1.25x | 1.24x | 1.25x | 1.25x | 1.24x | 1.25x | 1.25x | 1.25x | 1.24x | 1.25x | doubling (median: cap4m)
32 B | 64 | 2 KB | 49 ns | 2.29x | 2.25x | 2.24x | 2.27x | 2.26x | 2.27x | 2.26x | 2.27x | 2.26x | 2.26x | 2.26x | 2.27x | 2.27x | 2.26x | 2.26x | 2.27x | 2.26x | 2.27x | doubling (median: cap16m)
32 B | 4096 | 128 KB | 2.7 us | 2.55x | 2.84x | 2.35x | 2.44x | 2.44x | 2.47x | 2.58x | 2.50x | 2.51x | 2.51x | 2.54x | 2.54x | 2.51x | 2.53x | 2.51x | 2.52x | 2.50x | 2.50x | nocap (median: cap8k)
1 B | 16777216 | 16 MB | 14.8 ms | 119.90x | 120.23x | 71.77x | 73.30x | 74.02x | 67.44x | 39.32x | 61.44x | 71.05x | 93.60x | 105.89x | 112.77x | 116.40x | 118.54x | 119.58x | 120.19x | 120.19x | 120.35x | cap16m
10 B | 1677721 | 16 MB | 1.5 ms | 5.84x | 5.86x | 6.60x | 6.66x | 6.62x | 5.07x | 6.14x | 7.72x | 9.60x | 10.65x | 5.95x | 6.02x | 6.05x | 5.89x | 5.86x | 5.84x | 5.85x | 5.86x | cap64k
100 B | 167772 | 16 MB | 316.4 us | 1.27x | 1.27x | 1.42x | 1.47x | 1.37x | 1.11x | 1.47x | 1.90x | 2.26x | 2.38x | 1.30x | 1.30x | 1.31x | 1.28x | 1.27x | 1.27x | 1.27x | 1.27x | cap64k
1 KB | 16384 | 16 MB | 258.5 us | 1.04x | 1.04x | 1.01x | 1.01x | 1.20x | 1.09x | 0.95x | 1.18x | 1.45x | 1.80x | 1.68x | 1.06x | 1.07x | 1.06x | 1.04x | 1.04x | 1.03x | 1.04x | cap64k
64 KB | 256 | 16 MB | 204.0 us | 0.82x | 0.82x | 1.00x | 1.00x | 1.01x | 1.01x | 1.01x | 1.02x | 1.02x | 1.01x | 1.35x | 0.84x | 0.84x | 0.83x | 0.82x | 0.82x | 0.82x | 0.82x | cap128k
1 MB | 16 | 16 MB | 249.3 us | 0.99x | 0.99x | 1.00x | 1.01x | 1.01x | 1.01x | 1.01x | 1.01x | 1.01x | 1.01x | 1.01x | 1.01x | 1.01x | 1.01x | 0.99x | 0.99x | 0.99x | 0.99x | cap4k (median: cap256k)
1 B | 268435456 | 256 MB | 272.1 ms | 137.59x | 137.80x | 62.59x | 62.03x | 63.57x | 62.09x | 31.27x | 33.47x | 47.84x | 67.43x | 89.15x | 108.70x | 127.66x | 134.98x | 136.54x | 137.14x | 137.42x | 137.55x | nocap (median: cap16m)
10 B | 26843545 | 256 MB | 23.5 ms | 5.65x | 5.66x | 5.74x | 5.77x | 5.64x | 5.21x | 4.19x | 2.95x | 4.26x | 6.20x | 5.29x | 5.72x | 5.94x | 5.86x | 5.84x | 5.84x | 5.74x | 5.73x | cap64k
100 B | 2684354 | 256 MB | 5.2 ms | 1.25x | 1.25x | 1.26x | 1.28x | 1.24x | 0.74x | 0.97x | 0.74x | 1.08x | 1.57x | 1.29x | 1.31x | 1.32x | 1.29x | 1.29x | 1.29x | 1.27x | 1.26x | cap64k
1 KB | 262144 | 256 MB | 4.5 ms | 1.08x | 1.09x | 1.06x | 1.05x | 1.11x | 1.04x | 0.73x | 0.53x | 0.72x | 1.05x | 1.39x | 1.11x | 1.14x | 1.15x | 1.12x | 1.13x | 1.13x | 1.12x | cap128k
64 KB | 4096 | 256 MB | 4.5 ms | 1.08x | 1.09x | 1.02x | 1.00x | 1.00x | 1.01x | 1.01x | 0.99x | 1.00x | 0.99x | 1.34x | 1.08x | 1.14x | 1.14x | 1.12x | 1.13x | 1.12x | 1.12x | cap128k
1 MB | 256 | 256 MB | 4.1 ms | 0.97x | 0.98x | 1.02x | 1.04x | 1.04x | 1.04x | 1.04x | 1.04x | 1.04x | 1.04x | 1.06x | 1.04x | 1.04x | 1.04x | 1.02x | 1.02x | 1.02x | 1.01x | cap128k (median: cap64k)
1 B | 1000 | 1000 B | 1.3 us | 166.38x | 157.59x | 153.17x | 157.57x | 156.83x | 156.83x | 157.57x | 157.57x | 157.57x | 157.59x | 157.57x | 157.57x | 156.83x | 157.57x | 157.59x | 156.83x | 156.83x | 156.83x | doubling
1 B | 1000000 | 976.6 KB | 870.5 us | 112.32x | 112.94x | 72.54x | 75.42x | 76.25x | 75.42x | 49.63x | 70.82x | 87.05x | 99.48x | 107.70x | 113.55x | 112.32x | 112.94x | 112.92x | 112.94x | 112.94x | 113.54x | cap256k (median: nocap)
(24 rows)

# Debian 13.5 on an Intel Pentium Silver J5005, 4 MB L2 cache, no L3
# cache, 16 GB RAM, gcc 14.2.0 -O2

jan=# select * from repeat_bench();
source | repeats | output | master | doubling | nocap | cap512 | cap1k | cap2k | cap4k | cap8k | cap16k | cap32k | cap64k | cap128k | cap256k | cap512k | cap1m | cap2m | cap4m | cap8m | cap16m | fastest
--------+-----------+----------+-----------+----------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+-------------------------
32 B | 2 | 64 B | 13 ns | 1.06x | 0.81x | 0.81x | 0.81x | 0.81x | 0.81x | 0.81x | 0.81x | 0.81x | 0.81x | 0.81x | 0.81x | 0.81x | 0.81x | 0.81x | 0.81x | 0.81x | 0.81x | doubling
32 B | 4 | 128 B | 22 ns | 1.15x | 1.02x | 1.02x | 1.02x | 1.02x | 1.02x | 1.02x | 1.02x | 1.02x | 1.02x | 1.02x | 1.02x | 1.02x | 1.02x | 1.02x | 1.02x | 1.02x | 1.02x | doubling
32 B | 6 | 192 B | 31 ns | 1.19x | 1.09x | 1.09x | 1.09x | 1.09x | 1.09x | 1.09x | 1.09x | 1.09x | 1.09x | 1.09x | 1.09x | 1.09x | 1.09x | 1.09x | 1.09x | 1.09x | 1.09x | doubling
32 B | 7 | 224 B | 36 ns | 1.39x | 1.27x | 1.27x | 1.27x | 1.27x | 1.27x | 1.27x | 1.27x | 1.27x | 1.27x | 1.27x | 1.27x | 1.27x | 1.27x | 1.27x | 1.27x | 1.27x | 1.27x | doubling
32 B | 8 | 256 B | 42 ns | 1.57x | 1.43x | 1.43x | 1.43x | 1.43x | 1.43x | 1.43x | 1.43x | 1.43x | 1.43x | 1.43x | 1.43x | 1.43x | 1.43x | 1.43x | 1.43x | 1.43x | 1.43x | doubling
32 B | 9 | 288 B | 47 ns | 1.46x | 1.36x | 1.35x | 1.35x | 1.35x | 1.35x | 1.35x | 1.36x | 1.36x | 1.35x | 1.35x | 1.35x | 1.36x | 1.36x | 1.36x | 1.36x | 1.36x | 1.36x | doubling
32 B | 12 | 384 B | 61 ns | 1.81x | 1.68x | 1.68x | 1.68x | 1.68x | 1.68x | 1.68x | 1.68x | 1.68x | 1.68x | 1.68x | 1.68x | 1.68x | 1.68x | 1.68x | 1.68x | 1.68x | 1.68x | doubling
32 B | 16 | 512 B | 84 ns | 2.26x | 2.06x | 2.06x | 2.06x | 2.06x | 2.06x | 2.06x | 2.06x | 2.06x | 2.06x | 2.06x | 2.06x | 2.06x | 2.06x | 2.06x | 2.06x | 2.06x | 2.06x | doubling
32 B | 64 | 2 KB | 316 ns | 3.77x | 3.56x | 3.29x | 3.57x | 3.56x | 3.55x | 3.55x | 3.56x | 3.55x | 3.56x | 3.55x | 3.55x | 3.55x | 3.55x | 3.55x | 3.55x | 3.55x | 3.55x | doubling
32 B | 4096 | 128 KB | 19.1 us | 2.06x | 2.06x | 2.65x | 2.83x | 2.97x | 2.80x | 2.86x | 2.57x | 2.10x | 2.06x | 2.07x | 2.07x | 2.07x | 2.06x | 2.06x | 2.07x | 2.06x | 2.07x | cap2k
1 B | 16777216 | 16 MB | 115.4 ms | 62.63x | 62.87x | 48.66x | 48.17x | 46.47x | 46.12x | 46.15x | 45.99x | 46.26x | 46.42x | 45.77x | 46.02x | 45.57x | 53.15x | 62.82x | 62.85x | 64.64x | 63.37x | cap8m (median: cap16m)
10 B | 1677721 | 16 MB | 12.8 ms | 4.69x | 4.84x | 5.81x | 5.43x | 5.56x | 5.51x | 5.50x | 5.44x | 5.29x | 5.29x | 5.28x | 5.29x | 5.31x | 6.08x | 6.31x | 4.92x | 4.92x | 4.87x | cap2m
100 B | 167772 | 16 MB | 2.2 ms | 0.82x | 0.85x | 1.01x | 0.98x | 0.91x | 0.93x | 0.93x | 0.90x | 0.90x | 0.89x | 0.89x | 0.90x | 0.90x | 1.01x | 1.03x | 0.86x | 0.85x | 0.85x | cap2m
1 KB | 16384 | 16 MB | 2.2 ms | 0.83x | 0.85x | 1.00x | 0.98x | 0.76x | 0.96x | 0.96x | 0.95x | 0.92x | 0.91x | 0.92x | 0.92x | 0.91x | 1.01x | 1.10x | 0.92x | 0.87x | 0.85x | cap2m
64 KB | 256 | 16 MB | 2.4 ms | 0.90x | 0.91x | 1.00x | 1.00x | 0.97x | 0.97x | 0.97x | 0.97x | 0.97x | 0.97x | 0.98x | 0.98x | 0.98x | 1.07x | 1.18x | 0.98x | 0.92x | 0.91x | cap2m
1 MB | 16 | 16 MB | 2.3 ms | 0.87x | 0.85x | 0.98x | 1.06x | 1.06x | 1.06x | 1.06x | 1.06x | 1.06x | 1.06x | 1.06x | 1.06x | 1.06x | 1.06x | 1.05x | 0.91x | 0.83x | 0.85x | cap1k (median: cap16k)
1 B | 268435456 | 256 MB | 1863.4 ms | 63.24x | 64.65x | 49.51x | 49.87x | 49.36x | 49.32x | 48.93x | 49.09x | 48.95x | 49.08x | 49.03x | 48.93x | 49.00x | 61.96x | 63.08x | 62.97x | 63.22x | 63.51x | nocap
10 B | 26843545 | 256 MB | 207.6 ms | 4.45x | 4.47x | 5.43x | 5.22x | 5.49x | 5.50x | 5.44x | 5.38x | 5.25x | 5.20x | 5.24x | 5.22x | 5.21x | 6.37x | 6.38x | 4.60x | 4.53x | 4.47x | cap2m (median: cap1m)
100 B | 2684354 | 256 MB | 38.7 ms | 0.82x | 0.83x | 1.01x | 0.97x | 1.02x | 1.02x | 1.01x | 0.98x | 0.98x | 0.97x | 0.98x | 0.98x | 0.97x | 1.18x | 1.16x | 0.85x | 0.84x | 0.83x | cap1m
1 KB | 262144 | 256 MB | 37.8 ms | 0.81x | 0.81x | 1.00x | 1.00x | 0.70x | 1.00x | 0.99x | 0.99x | 0.95x | 0.95x | 0.95x | 0.95x | 0.95x | 1.16x | 1.16x | 1.03x | 0.82x | 0.82x | cap2m (median: cap1m)
64 KB | 4096 | 256 MB | 39.1 ms | 0.83x | 0.84x | 1.00x | 0.99x | 0.99x | 0.99x | 0.99x | 0.99x | 0.99x | 0.99x | 0.99x | 0.98x | 0.98x | 1.20x | 1.20x | 1.00x | 0.84x | 0.85x | cap2m (median: cap1m)
1 MB | 256 | 256 MB | 32.6 ms | 0.70x | 0.69x | 1.01x | 1.01x | 1.01x | 1.01x | 1.01x | 1.01x | 1.01x | 1.01x | 1.01x | 1.01x | 1.01x | 1.01x | 1.00x | 0.82x | 0.71x | 0.71x | cap128k
1 B | 1000 | 1000 B | 6.8 us | 240.97x | 225.32x | 201.53x | 225.38x | 225.38x | 225.38x | 225.38x | 225.38x | 225.38x | 225.38x | 225.38x | 225.38x | 225.38x | 225.38x | 225.38x | 225.38x | 225.38x | 225.38x | doubling
1 B | 1000000 | 976.6 KB | 6.8 ms | 134.42x | 134.55x | 124.58x | 126.56x | 132.95x | 128.51x | 131.56x | 133.10x | 133.85x | 134.27x | 134.37x | 134.51x | 134.58x | 134.53x | 134.55x | 134.57x | 134.52x | 134.54x | cap512k (median: nocap)
(24 rows)

To understand the different cap behavior on our systems better, could
you share the output of the following command from your systems?

ld.so --list-tunables | grep non_temporal

On my J5005, it looks as follows:

$ ld.so --list-tunables | grep non_temporal
glibc.cpu.x86_memset_non_temporal_threshold: 0x100000 (min: 0x4040, max: 0xffffffffffffffff)
glibc.cpu.x86_non_temporal_threshold: 0x100000 (min: 0x4040, max: 0xfffffffffffffff)

The preferred cap on the J5005 is the first one at or above this
threshold (0x100000 = 1 MB). Does this match the behavior on your
systems as well?

On ARM, this tunable isn't present.

The corrected single-byte results do not change the overall conclusion. For
multi-byte inputs, the preferred cap on this machine still varies noticeably
with the total output size. In particular, the 16 MB cases tend to prefer
relatively small caps, while the 256 MB cases consistently prefer cap16m.

For the single-byte cases, splitting the memset() into capped chunks also does
not seem to introduce a significant performance penalty. In some cases, the
larger capped memset variants are even slightly faster than a single large
memset().

Unfortunately, on the M5 Max it does. Comparing the worst chunked
variant (cap8k in all three cases) against nocap, which does the same
work in a single memset():

output cap8k nocap ratio
16 MB 39.32x 120.23x 3.1x
256 MB 31.27x 137.80x 4.4x
976.6 KB 49.63x 112.94x 2.3x

To move this patch forward, would you consider proposing the
single-byte memset() as a separate, smaller patch first? It is a large
win with no regression in the benchmarks we have executed. It is also
independent of the block size question. So, it could go in while the
doubling strategy needs more investigation. Also, the early return
for count == 0 || slen == 0 is a second change that could be committed
independently of the cap discussion.

Best regards
Jan

--
Jan Nidzwetzki
PlanetScale Postgres Core Team

#11Jan Nidzwetzki
jan@planetscale.com
In reply to: Jan Nidzwetzki (#10)
Re: Re:[PATCH] Speed up repeat() for larger counts

Hello Chenhui,

I tried to find the reason why the preferred cap in the benchmark
differs so much between our machines, and I came up with a theory
that explains the behavior.

On 04.09.26 15:51, Jan Nidzwetzki wrote:

The preferred cap on the J5005 is the first one at or above this
threshold (0x100000 = 1 MB). Does this match the behavior on your
systems as well?

glibc tunable
=============

On x86, glibc switches memcpy() to non-temporal stores above
x86_non_temporal_threshold (bypassing the CPU caches and writing
data directly to the system memory).

This value is dynamically calculated based on the characteristics
of the machine. However, it can be adjusted manually, like:

GLIBC_TUNABLES="glibc.cpu.x86_non_temporal_threshold=$value\
:glibc.cpu.x86_memset_non_temporal_threshold=$value"

I did an experiment (outside of PostgreSQL) where I forced
certain thresholds and repeated the benchmark. Changing that
value moves the preferred cap with it. On the J5005, with a
256 MB output:

set threshold 1 KB source 100 B source
64 KB cap64k cap64k
256 KB cap256k cap256k
1 MB cap1m cap1m
4 MB none none
disabled none none

The speedup always starts at the first cap at or above the threshold,
when non-temporal stores are used and the cache is bypassed.

At a 4 MB threshold, the effect disappears entirely. A 4 MB block
is the whole L2 on this machine, so no size is both large enough
for non-temporal stores and small enough to stay cached.

With the non-temporal path disabled, the row is flat as well, and
uncapped doubling falls below the current implementation.

Since this is adjustable through GLIBC_TUNABLES, the best cap is not
only machine-dependent; it is settable from the environment.

We could potentially use something like __builtin_nontemporal_store()
to avoid the glibc threshold and force non-temporal stores. However,
this is compiler- and architecture-specific, and I don't think it is
worth optimizing this particular code path that way.

What do you think about the theory?

Personally, I would go with the early return and memset() optimization
as outlined in my last mail, since they show clear improvements in all
tested environments. The doubling / capping approach shows good
results in many situations, but is harder to implement, since picking
a wrong cap leads to regressions.

However, I am still curious what other people will see when they
repeat the benchmarks.

Best regards
Jan

--
Jan Nidzwetzki
PlanetScale Postgres Core Team

#12ChenhuiMo
chenhuimo.mch@qq.com
In reply to: Jan Nidzwetzki (#11)
Re: [PATCH] Speed up repeat() for larger counts

Hi Jan,

Thanks for the additional results and for the suggestion.

I agree that it makes sense to separate the changes that are independent
of the block-size question. I have split them into two patches:

0001 returns early when count == 0 or the source string is empty.

0002 uses memset() when the source string is one byte long.

The doubling change is left out for now while the block-size behavior is
investigated further.

I also checked the glibc tunables on both of my x86 systems.

On the i5-13500H system:

glibc.cpu.x86_shared_cache_size: 0x1c0000
glibc.cpu.x86_non_temporal_threshold: 0x150000
glibc.cpu.x86_data_cache_size: 0xc000

The non-temporal threshold is 1.3125 MiB, so the first tested cap at or
above it is 2 MiB. This matches the range that performed well in my
earlier measurements.

On the i9-12900H system:

glibc.cpu.x86_shared_cache_size: 0x1940000
glibc.cpu.x86_non_temporal_threshold: 0xca0000
glibc.cpu.x86_data_cache_size: 0xc000

Here the non-temporal threshold is 12.625 MiB, so the first tested cap
at or above it is 16 MiB. This also matches the earlier measurements,
where cap16m was often the best choice for the larger-output cases.

So the glibc non-temporal threshold appears to correlate well with the
preferred cap on both of my x86 systems.

I have attached the two smaller patches. The doubling change is not
included in this version.

Thanks,
Chenhui

Attachments:

t253193_12
0001-Return-early-from-repeat-for-empty-results.patchapplication/octet-stream; charset=utf-8; name=0001-Return-early-from-repeat-for-empty-results.patchDownload+4-1
0002-Use-memset-in-repeat-for-single-byte-strings.patchapplication/octet-stream; charset=utf-8; name=0002-Use-memset-in-repeat-for-single-byte-strings.patchDownload+7-1
#13Jan Nidzwetzki
jan@planetscale.com
In reply to: ChenhuiMo (#12)
Re: [PATCH] Speed up repeat() for larger counts

Hi Chenhui,

Thanks for the new version and for splitting the patch set. It applies
to the current master branch, and check-world passes.

On 12.09.26 15:12, ChenhuiMo wrote:

[...]

Here the non-temporal threshold is 12.625 MiB, so the first tested cap
at or above it is 16 MiB. This also matches the earlier measurements,
where cap16m was often the best choice for the larger-output cases.

So the glibc non-temporal threshold appears to correlate well with the
preferred cap on both of my x86 systems.

That settles it for me. Collecting the three x86 systems measured so far:

system non-temporal threshold first cap at or above it
J5005 1 MiB cap1m
i5-13500H 1.3125 MiB cap2m
i9-12900H 12.625 MiB cap16m

In every case, the preferred cap is the first one at or above the
threshold. So the 64 KB to 16 MB spread we started with is a glibc
setting with three different values, not a hardware property.

Review
======
In patch 2, CHECK_FOR_INTERRUPTS() is called after memset(). Usually,
CHECK_FOR_INTERRUPTS() is called periodically to check whether a
long-running task has to be aborted. After memset() returns, the actual
heavy work is already done. Not sure if we actually need the check.

In addition, I recommend a short comment for the new 'if (slen == 1)'
path that describes that memset() is faster than the loop below.

strings.sql offers only partial test coverage for the added conditions.
I recommend adding at least these three tests to the existing ones to
test the early return and the memset() fast path:

SELECT repeat('', 10);
SELECT repeat('x', 0);
SELECT repeat('x', 4);

Additional comments
===================
Your new patch set has a different author than the previous ones. Please
double-check that it contains the actual name you want visible in the
commit.

When you generate a new patch set, it is common to version it to make it
easier for others to understand which version is most recent. You can do
this by specifying the "-v" option when exporting the patch. For
example, "git format-patch -v3 -2" generates version 3 of the patch
series consisting of the last two commits.

Best regards
Jan

--
Jan Nidzwetzki
PlanetScale Postgres Core Team

#14ChenhuiMo
chenhuimo.mch@qq.com
In reply to: Jan Nidzwetzki (#13)
Re: [PATCH] Speed up repeat() for larger counts

Hi Jan,

Thanks for the review.

I have updated the patch set based on your comments:

- removed CHECK_FOR_INTERRUPTS() after memset()
- added a short comment for the single-byte fast path
- added regression tests for the early-return cases and the memset() path
- versioned the patch series as v2

I also confirmed that the author information in the patches is the name
and email address I want to use.

The updated v2 patch set is attached.

Thanks,
Chenhui

Attachments:

t253193_14
v2-0001-Return-early-from-repeat-for-empty-results.patchapplication/octet-stream; charset=utf-8; name=v2-0001-Return-early-from-repeat-for-empty-results.patchDownload+18-1
v2-0002-Use-memset-in-repeat-for-single-byte-strings.patchapplication/octet-stream; charset=utf-8; name=v2-0002-Use-memset-in-repeat-for-single-byte-strings.patchDownload+14-1
#15Jan Nidzwetzki
jan@planetscale.com
In reply to: ChenhuiMo (#14)
Re: [PATCH] Speed up repeat() for larger counts

Hi Chenhui,

On 15.09.26 10:06, ChenhuiMo wrote:

Thanks for the review.

I have updated the patch set based on your comments:

Thanks for version 2 of the patch set. It applies to the current master
branch, and check-world passes. Also, pgindent reports no changes.

So, the patch set looks good to me. I will mark it as ready for committer.

Best regards
Jan

--
Jan Nidzwetzki
PlanetScale Postgres Core Team

#16Sehrope Sarkuni
sehrope@jackdb.com
In reply to: Jan Nidzwetzki (#15)
Re: [PATCH] Speed up repeat() for larger counts

FYI, the lpad/rpad patch now also uses the same helper for repeat(), so
this overlaps with the work here:

/messages/by-id/CAH7T-apj+pFg9bRkXVGfGejS2Uu4WzdMd7KoLKTW11mdsrt1Ew@mail.gmail.com

I've credited this thread for the earlier repeat() doubling work and
the discussion around capping the copy size.

Regards,
-- Sehrope Sarkuni
Founder & CEO | JackDB, Inc. | https://www.jackdb.com/