Hi Hackers,

On 28.07.26 04:15, Jeevan Chalke wrote:
> On Mon, Jul 27, 2026 at 7:11 PM David Rowley <[email protected] 
> <mailto:[email protected]>> wrote:
>     On Mon, 27 Jul 2026 at 22:18, Heikki Linnakangas <[email protected] 
> <mailto:[email protected]>> 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
MODULES = repeatbench
EXTENSION = repeatbench
DATA = repeatbench--1.0.sql
PGFILEDESC = "repeatbench - verify repeat() copy strategies"

PG_CONFIG = pg_config
PGXS := $(shell $(PG_CONFIG) --pgxs)
include $(PGXS)
CREATE OR REPLACE FUNCTION repeat_bench_sql(max_output_bytes bigint DEFAULT 
268435456,
                                                                                
iterations int DEFAULT 10,
                                                                                
empty_source_count int DEFAULT 10000000)
RETURNS TABLE (output_mb numeric,
                           source_len int,
                           repeat_count bigint,
                           best_ms numeric,
                           ns_per_byte numeric)
LANGUAGE plpgsql AS $$
DECLARE
        source_lengths  int[] := ARRAY[1, 10, 100, 1024, 4096, 16384, 65536,
                                                                   262144, 
1048576];
        size_divisors   int[] := ARRAY[16, 4, 1];
        divisor                 int;
        cur_len                 int;
        cur_count               bigint;
        target_bytes    bigint;
        source                  text;
        sink                    text;
        started_at              timestamptz;
        elapsed_ms              numeric;
        best_elapsed    numeric;
BEGIN
        -- repeat() rejects any result larger than MaxAllocSize (1GB - 1) 
counting
        -- the varlena header, so asking for more than that only produces 
errors.
        -- Clamping here also keeps every repeat count comfortably inside int4.
        max_output_bytes := least(max_output_bytes, 1073741819);

        -- Zero-length source: no bytes are ever copied.
        source := '';
        best_elapsed := NULL;
        FOR i IN 1 .. iterations LOOP
                started_at := clock_timestamp();
                sink := repeat(source, empty_source_count);
                elapsed_ms := extract(epoch FROM clock_timestamp() - 
started_at) * 1000;
                IF best_elapsed IS NULL OR elapsed_ms < best_elapsed THEN
                        best_elapsed := elapsed_ms;
                END IF;
        END LOOP;
        output_mb := 0.0;
        source_len := 0;
        repeat_count := empty_source_count;
        best_ms := round(best_elapsed, 4);
        ns_per_byte := NULL;            -- no bytes produced, so the ratio is 
undefined
        RETURN NEXT;

        FOREACH divisor IN ARRAY size_divisors LOOP
                target_bytes := greatest(max_output_bytes / divisor, 1048576);

                FOREACH cur_len IN ARRAY source_lengths LOOP
                        cur_count := greatest(target_bytes / cur_len, 1);
                        source := repeat('x', cur_len);

                        best_elapsed := NULL;
                        FOR i IN 1 .. iterations LOOP
                                started_at := clock_timestamp();
                                sink := repeat(source, cur_count::int);
                                elapsed_ms := extract(epoch FROM 
clock_timestamp() - started_at) * 1000;
                                IF best_elapsed IS NULL OR elapsed_ms < 
best_elapsed THEN
                                        best_elapsed := elapsed_ms;
                                END IF;
                        END LOOP;
                        sink := NULL;

                        output_mb := round((cur_len::numeric * cur_count) / 
1048576, 1);
                        source_len := cur_len;
                        repeat_count := cur_count;
                        best_ms := round(best_elapsed, 4);
                        ns_per_byte := round((best_elapsed * 1000000) /
                                                                 
(cur_len::numeric * cur_count), 4);
                        RETURN NEXT;
                END LOOP;
        END LOOP;
END $$;

\timing on
SELECT * FROM repeat_bench_sql();
CREATE FUNCTION repeat_bench()
RETURNS TABLE (source text,
                           repeats int,
                           output text,
                           master text,
                           doubling text,
                           nobranch text,
                           cap512 text,
                           cap4k text,
                           cap64k text,
                           nocap text,
                           memset text,
                           fastest text)
AS 'MODULE_PATHNAME', 'repeat_bench'
LANGUAGE C STRICT VOLATILE;

/*
 * repeatbench.c -- compare candidate repeat() copy strategies.
 * SELECT * FROM repeat_bench();  Not for commit.
 */
#include "postgres.h"

#include "fmgr.h"
#include "funcapi.h"
#include "miscadmin.h"
#include "portability/instr_time.h"
#include "utils/builtins.h"
#include "utils/fmgrprotos.h"
#include "utils/memutils.h"
#include "utils/tuplestore.h"
#include "varatt.h"

PG_MODULE_MAGIC;

#define ITERS 25

/* master */
static void
strat_linear(char *dst, const char *src, int slen, int count)
{
        char       *cp = dst;

        for (int i = 0; i < count; i++)
        {
                memcpy(cp, src, slen);
                cp += slen;
                CHECK_FOR_INTERRUPTS();
        }
}

/* the posted patch */
static void
strat_double(char *dst, const char *src, int slen, int count)
{
        char       *cp = dst;

        if (count < 8)
        {
                strat_linear(dst, src, slen, count);
                return;
        }

        memcpy(cp, src, slen);
        cp += slen;
        CHECK_FOR_INTERRUPTS();

        for (int curcount = 1; curcount < count;)
        {
                int                     chunk = Min(curcount, count - curcount);

                memcpy(cp, dst, (size_t) chunk * slen);
                cp += (size_t) chunk * slen;
                curcount += chunk;
                CHECK_FOR_INTERRUPTS();
        }
}

/* the posted patch with its count < 8 fallback deleted */
static void
strat_nobranch(char *dst, const char *src, int slen, int count)
{
        char       *cp = dst;

        memcpy(cp, src, slen);
        cp += slen;
        CHECK_FOR_INTERRUPTS();

        for (int curcount = 1; curcount < count;)
        {
                int                     chunk = Min(curcount, count - curcount);

                memcpy(cp, dst, (size_t) chunk * slen);
                cp += (size_t) chunk * slen;
                curcount += chunk;
                CHECK_FOR_INTERRUPTS();
        }
}

/*
 * Double until the block reaches blocksz bytes, then repeat that block.
 * chunk <= curcount, so every memcpy has disjoint source and destination.
 * The block limit is computed once rather than multiplied per iteration.
 * A one-copy block holds the same bytes as src, and reading it there keeps
 * large sources identical to master.
 */
static void
capped(char *dst, const char *src, int slen, int count, int blocksz)
{
        char       *cp = dst;
        const char *from;
        int                     curcount = 1;
        int                     blockcount;
        int                     blocklimit = (blocksz - 1) / slen + 1;  /* 
ceil, no overflow */

        memcpy(cp, src, slen);
        cp += slen;
        CHECK_FOR_INTERRUPTS();

        while (curcount < count && curcount < blocklimit)
        {
                int                     chunk = Min(curcount, count - curcount);

                memcpy(cp, dst, (size_t) chunk * slen);
                cp += (size_t) chunk * slen;
                curcount += chunk;
                CHECK_FOR_INTERRUPTS();
        }

        blockcount = curcount;
        from = (blockcount == 1) ? src : dst;

        while (curcount < count)
        {
                int                     chunk = Min(blockcount, count - 
curcount);

                memcpy(cp, from, (size_t) chunk * slen);
                cp += (size_t) chunk * slen;
                curcount += chunk;
                CHECK_FOR_INTERRUPTS();
        }
}

static void
strat_cap512(char *dst, const char *src, int slen, int count)
{
        capped(dst, src, slen, count, 512);
}

static void
strat_cap4k(char *dst, const char *src, int slen, int count)
{
        capped(dst, src, slen, count, 4096);
}

static void
strat_cap64k(char *dst, const char *src, int slen, int count)
{
        capped(dst, src, slen, count, 65536);
}

/* cap large enough that it never engages (repeat() tops out at 1GB) --
 * isolates the cost of the structure from the cost of capping */
static void
strat_nocap(char *dst, const char *src, int slen, int count)
{
        capped(dst, src, slen, count, PG_INT32_MAX);
}

/* Heikki's suggestion */
static void
strat_memset(char *dst, const char *src, int slen, int count)
{
        if (slen == 1)
        {
                memset(dst, src[0], count);
                CHECK_FOR_INTERRUPTS();
                return;
        }
        capped(dst, src, slen, count, 512);
}

static void (*const strategies[]) (char *dst, const char *src,
                                                                   int slen, 
int count) = {
        strat_linear, strat_double, strat_nobranch, strat_cap512, strat_cap4k,
        strat_cap64k, strat_nocap, strat_memset
};

static const char *const names[] = {"master", "doubling", "nobranch", "cap512",
"cap4k", "cap64k", "nocap", "memset"};



/* every strategy must match the built-in repeat() before any timing is
 * reported, otherwise a broken strategy just looks fast */
static void
verify(void)
{
        static const int slens[] = {0, 1, 2, 3, 7, 8, 16, 100, 1000};
        static const int counts[] = {PG_INT32_MIN, -1, 0, 1, 2, 3, 7, 8, 9,
        15, 16, 17, 64, 65, 1000};

        for (int a = 0; a < lengthof(slens); a++)
        {
                int                     slen = slens[a];
                text       *src = (text *) palloc(VARHDRSZ + slen);

                SET_VARSIZE(src, VARHDRSZ + slen);
                for (int k = 0; k < slen; k++)
                        VARDATA(src)[k] = (char) ('a' + (k % 26));

                for (int b = 0; b < lengthof(counts); b++)
                {
                        int                     eff = Max(counts[b], 0);
                        int                     tlen = VARHDRSZ + slen * eff;
                        text       *want;

                        want = DatumGetTextPP(DirectFunctionCall2(repeat,
                                                                                
                          PointerGetDatum(src),
                                                                                
                          Int32GetDatum(counts[b])));

                        for (int s = 0; s < lengthof(strategies); s++)
                        {
                                text       *got = (text *) palloc(tlen);

                                SET_VARSIZE(got, tlen);
                                if (slen > 0 && eff > 0)
                                        strategies[s] (VARDATA(got), 
VARDATA(src), slen, eff);

                                if (VARSIZE(want) != VARSIZE(got) ||
                                        memcmp(VARDATA_ANY(want), VARDATA(got), 
tlen - VARHDRSZ) != 0)
                                        elog(ERROR, "strategy \"%s\" differs 
from repeat() at "
                                                 "slen=%d count=%d", names[s], 
slen, counts[b]);
                                pfree(got);
                        }
                        pfree(want);
                }
                pfree(src);
        }
}

/* "1 B", "64 KB", "256 MB" */
static char *
human(int64 bytes)
{
        if (bytes >= 1024 * 1024)
                return psprintf("%.4g MB", (double) bytes / (1024 * 1024));
        if (bytes >= 1024)
                return psprintf("%.4g KB", (double) bytes / 1024);
        return psprintf(INT64_FORMAT " B", bytes);
}

/* "339.2 ms", "185.0 us", "7 ns" */
static char *
duration(double ms)
{
        if (ms >= 1.0)
                return psprintf("%.1f ms", ms);
        if (ms >= 0.001)
                return psprintf("%.1f us", ms * 1000.0);
        return psprintf("%.0f ns", ms * 1000000.0);
}

PG_FUNCTION_INFO_V1(repeat_bench);
Datum
repeat_bench(PG_FUNCTION_ARGS)
{
        static const int cases[][2] = {
                /* small counts, densely either side of the patch's threshold 
of 8 */
                {32, 2}, {32, 4}, {32, 6}, {32, 7}, {32, 8}, {32, 9}, {32, 12},
                {32, 16}, {32, 64}, {32, 4096},
                /* 16 MB of output: usually still cache-resident */
                {1, 16777216}, {10, 1677721}, {100, 167772},
                {1024, 16384}, {65536, 256}, {1048576, 16},
                /* 256 MB of output: far beyond any cache */
                {1, 268435456}, {10, 26843545}, {100, 2684354},
                {1024, 262144}, {65536, 4096}, {1048576, 256},
                /* single-byte sources */
                {1, 1000}, {1, 1000000}
        };
        ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;

        InitMaterializedSRF(fcinfo, 0);
        verify();

        for (int i = 0; i < lengthof(cases); i++)
        {
                int                     slen = cases[i][0];
                int                     count = cases[i][1];
                int                     tlen = VARHDRSZ + slen * count;
                int                     inner = Max(1048576 / tlen, 1);
                /* memset only differs from capped when the source is one byte 
*/
                int                     nstrat = (slen == 1) ? 
lengthof(strategies)
                        : lengthof(strategies) - 1;
                int                     best = 0;
                double          ms[lengthof(strategies)];
                Datum           values[4 + lengthof(strategies)];
                bool            nulls[4 + lengthof(strategies)] = {0};
                MemoryContext cxt,
                                        old;
                char       *buf;
                char       *src = palloc(slen);

                memset(src, 'x', slen);
                cxt = AllocSetContextCreate(CurrentMemoryContext, 
"repeat_bench",
                                                                        
ALLOCSET_DEFAULT_SIZES);
                old = MemoryContextSwitchTo(cxt);
                buf = palloc(tlen);
                memset(buf, 0, tlen);   /* fault the pages in up front */

                for (int s = 0; s < nstrat; s++)
                        ms[s] = -1.0;

                /* interleave the strategies so frequency drift hits them 
equally */
                for (int r = 0; r < ITERS; r++)
                {
                        for (int s = 0; s < nstrat; s++)
                        {
                                instr_time      t0,
                                                        dur;
                                double          el;

                                INSTR_TIME_SET_CURRENT(t0);
                                for (int j = 0; j < inner; j++)
                                        strategies[s] (buf + VARHDRSZ, src, 
slen, count);
                                INSTR_TIME_SET_CURRENT(dur);
                                INSTR_TIME_SUBTRACT(dur, t0);

                                el = INSTR_TIME_GET_MILLISEC(dur) / inner;
                                if (ms[s] < 0.0 || el < ms[s])
                                        ms[s] = el;
                        }
                }
                MemoryContextSwitchTo(old);
                MemoryContextDelete(cxt);

                for (int s = 1; s < nstrat; s++)
                        if (ms[s] < ms[best])
                                best = s;

                values[0] = CStringGetTextDatum(human(slen));
                values[1] = Int32GetDatum(count);
                values[2] = CStringGetTextDatum(human((int64) slen * count));
                values[3] = CStringGetTextDatum(duration(ms[0]));
                for (int s = 1; s < lengthof(strategies); s++)
                {
                        if (s < nstrat)
                                values[3 + s] = 
CStringGetTextDatum(psprintf("%.2fx", ms[0] / ms[s]));
                        else
                                values[3 + s] = CStringGetTextDatum("-");
                }
                values[3 + lengthof(strategies)] = 
CStringGetTextDatum(names[best]);

                tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, 
values, nulls);
        }

        return (Datum) 0;
}

comment = 'verify candidate repeat() copy strategies'
default_version = '1.0'
module_pathname = '$libdir/repeatbench'
relocatable = true

Reply via email to