Skip to content

The block codec compresses the whole encoded region per chunk and discards it when it declines: +25% write CPU on incompressible data #1075

Description

@OffgridwithJD

PgColumnarFsstHelpsCompressed runs two full block compressions per verdict and pfrees both, using them only for their lengths. On data the codec cannot compress, every byte of that work is discarded, and it is measurable: lz4 costs +24.8% write CPU on the incompressible shape.

From #890's phase 1 (@jdatcmd's measurement), filed separately because #890 closes as "change nothing about the default" and this is a cost that stands whatever the default is.

Verified in source

src/columnar_encoding.c, PgColumnarFsstHelpsCompressed:

PgColumnarCompressValueStream(corpus, corpusLen, compressionType,
                            compressionLevel, &plainComp, &plainCompLen, ...);
PgColumnarCompressValueStream(codes, codesLen, compressionType,
                            compressionLevel, &codesComp, &codesCompLen, ...);

helps = (((uint64) codesCompLen + tableLen) * 100
         < (uint64) plainCompLen * (uint64) (100 - pgcolumnar_fsst_min_gain_percent));

pfree(codes);
if (plainComp) pfree(plainComp);
if (codesComp) pfree(codesComp);

Two compressions, both freed, only plainCompLen and codesCompLen used.

Measured

write cost ratio, incompressible shape
  lz4   W = 1.248      <- against a NET COST threshold of 1.25 in #890's rule
  zstd  W = 0.92

lz4 misses #890's pre-registered NET COST threshold by 0.002. It is not the default, so it did not bear on that decision — but a user who sets compression = lz4 on high-entropy text pays it.

The zstd arm being cheaper than none is the same mechanism from the other side and is explained by the sibling issue: with compression = none the function returns true unconditionally and FSST is paid for every vector with no decision to make, so none is not doing less work.

Why fsst_verdict_reuse does not already cover it

It was proposed as the explanation and measured to be zero:

none  (FSST always kept, every chunk)        13,001,225,971
zstd, verdict_reuse=16 (default)             11,982,999,646
zstd, verdict_reuse=1  (decide every chunk)  11,982,901,432    identical
zstd, min_gain=0       (force FSST kept)     11,984,172,254    identical

reuse=1 against reuse=16 differs by 0.0008%, so amortising the verdict is not where the cost is. The cost is inside each verdict.

Suggested shapes, cheapest first

  1. Sample rather than compress the whole stream. The verdict needs a length ratio, not the bytes. Compressing a bounded prefix would give the same decision for a fraction of the work, at the cost of being an estimate — which it already is, since the verdict is reused across up to 16 chunks by default.
  2. Short-circuit on a cheap incompressibility estimate before paying either compression.
  3. Reuse one of the two compressions where the chosen branch needs it again downstream, instead of freeing both. Worth checking whether the winning stream is recompressed immediately afterwards; if so this is pure duplicate work and the fix is plumbing rather than estimation.

I have not measured which of these pays, and the fixture that would show it is the one in #890's write-side table.

Related

#1074 — the same function's keep test never evaluates FSST stored uncompressed. If that candidate is added, it is computable without a compression at all, which may make ordering the comparisons cheapest-first worthwhile.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions