On Saturday, October 24th, 2026 at 3:32 PM, Manu <[email protected]> 
wrote:

> Hi Greg,

Hi Manu,

I confirmed your encoding analysis in my own tree, replies inline.

> It is the database encoding. bench.sh runs on a SQL_ASCII database on
> purpose. With a multibyte encoding, text_substring() asks detoast for
> bytes [0, (start + len - 1) * eml) (varlena.c:666-718), and a request
> at least as long as the raw size becomes a full decompression
> (detoast.c:529); lines as of master 42e96cf2fe0, and v10 changes
> neither.

Confirmed, same lines in my checkout. text_substring() takes the eml > 1
branch (varlena.c, "we can't get LC without detoasting, so we'll grab a
conservatively large slice"): it sets slice_start = 0 and asks for a
slice up to the end byte, (start + len) * eml. detoast then hits

  if ((uint32) slicelength >= VARDATA_COMPRESSED_GET_EXTSIZE(attr))
      return toast_decompress_datum(attr);

and decodes the whole value. On a single-byte encoding eml == 1 and the
request stays a real slice. So this is a property of the caller and the
encoding, not of the patch, and v11 does not touch either path.

> - UTF8: substr(v, 500000, 100) enters zstd_decompress_datum() 20 times
> and the slice function never. - SQL_ASCII: the same query enters
> zstd_decompress_datum_slice() 20 times, with slicelength 500099.

That is the clean way to settle it, and it matches the code above. My box
was UTF8, so my "slice" rows were calling the full-decode path. I should
have run under SQL_ASCII as you did, or set a breakpoint instead of
trusting the query shape.

> So, if your database is UTF8, your 1MB slice@500k (1.001) is a full
> read, like our 1 MB full read (1.000 [0.985, 1.006]); your 32kB
> slice@20k (0.953) sits with your full read (0.956); and your 32kB
> prefix (0.614) is the one that stays a slice.

Agreed, line by line. Relabeled, my numbers are: full reads 0.956 and
1.001, which sit on your 0.956 and 1.000; and one true slice, the 32kB
prefix at 0.614. There was never a disagreement, just one genuine slice
measured two ways.

> The 172 us per value that the reuse saves on our 1 MB slice track page
> faults. On SQL_ASCII the backend takes 416 page faults per value on
> that slice with a fresh context and none with the reused one [...] The
> 1 MB prefix (0 and 0 faults) and full read (631 and 631) show no fault
> difference, and their timings barely move.

This is the mechanism I was missing, and it is the right one. A fresh
ZSTD_DCtx faults in its working buffers on every value; the reused
context keeps them resident, so the saving is the fault cost, not decode
work. That explains why the win lands only on a true slice of a large
value: the full read touches the buffers either way (your 631 vs 631),
and the small prefix never grows them (0 vs 0). The 0003 commit message
says "small slices and prefixes of large values," which is exactly this
case, so I'll leave the wording as it stands; if you think it is worth
naming the page-fault cause in the message I'm happy to add a line.

> The 32 kB rows are not the same test: ours on 32,000-byte docs, json
> and random values. Size and content both differ, so I would not read
> 0.614 vs our 0.70-0.76 as a disagreement.

Right, those are not comparable. Mine were 80k x 46kB docs; yours are
32,000-byte json and random values, so both the size and the
compressibility differ, and the prefix fraction that stays a slice
differs with them. I'll stop citing the two side by side.

> Our setup: Ryzen AI 9 HX 370 laptop, 8 CPUs pinned, [...] SQL_ASCII,
> locale C.

Noted; mine was c6id.metal, UTF8, locale C, which is the one variable
that mattered here. Thanks for the scripts and the per-run file; the
nocfbot-v9n-without-reuse.diff control is the same shape as the true-v7
build I used, so between the two we have the reuse isolated on both a
laptop and bare metal, and the page-fault counts explain the one number
that moves.

best.

-greg



Reply via email to