[PATCH v2 02/14] smb: client: compress: rework compression heuristics

Enzo Matsumiya <[email protected]> Mon, 20 Jul 2026 16:49:13 -0300
Newsgroups org.kernel.vger.linux-cifs
Message-ID <[email protected]>
- add enum with *COMPRESSIBLE values
- is_compressible() is now check_compressible(), and is called from
  smb_compress() instead of should_compress();  the latter only does
  the basic checks (write >= 4K)
- compressibility check now also may return 'MAYBE_COMPRESSIBLE',
  for cases when even with deeper analysis, it can't return a
  decisive result
- rewrite Shannon entropy computation (original code was copied over
  from btrfs, so it carelessly used their parameters)
- divide heuristics checks in stages:
  1. check if buffer is filled with same byte (RLE)
  2. check if partial RLE (first ~1% of buffer repeats same byte)
  3. check whole buffer entropy
  4. split buffer in 64k chunks and check entropy and byte distribution
     (based on hardcoded thresholds)
- remove collect_sample() and collect_step() helpers, heuristics checks
  are run on the entire buffer now
- remove calc_byte_distribution(), as it's a redundant entropy check

Signed-off-by: Enzo Matsumiya <[email protected]>
---
 fs/smb/client/compress.c | 456 ++++++++++++++++++++++-----------------
 1 file changed, 259 insertions(+), 197 deletions(-)

diff --git a/fs/smb/client/compress.c b/fs/smb/client/compress.c
index 5591a8d85037..925b01a4d0a5 100644
--- a/fs/smb/client/compress.c
+++ b/fs/smb/client/compress.c
@@ -1,6 +1,6 @@
 // SPDX-License-Identifier: GPL-2.0-only
 /*
- * Copyright (C) 2024, SUSE LLC
+ * Copyright (C) 2024-2026, SUSE LLC
  *
  * Authors: Enzo Matsumiya <[email protected]>
  *
@@ -24,242 +24,291 @@
 #include "../common/compress/compress.h"
 #include "compress.h"
 
-/*
- * The heuristic_*() functions below try to determine data compressibility.
+/**
+ * freq_count() - Count symbols (bytes) frequencies in a buffer.
+ * @buf:	uncompressed buffer
+ * @len:	size of @buf
+ * @mem:	(caller-allocated) auxiliary memory for parallel accumulators
+ * @count:	on exit, set to unique byte count (alphabet size) in @buf
  *
- * Derived from fs/btrfs/compression.c, changing coding style, some parameters, and removing
- * unused parts.
+ * This function counts all bytes' unique count and frequencies.
+ * Expect @mem to be 1024 * sizeof(u32) long and zeroed.
+ * Final frequencies will be stored on first 256 elements of @mem.
  *
- * Read that file for better and more detailed explanation of the calculations.
+ * Function is optimized for OoO CPUs (straightforward iterative count is about 2-3x slower).
  *
- * The algorithms are ran in a collected sample of the input (uncompressed) data.
- * The sample is formed of 2K reads in PAGE_SIZE intervals, with a maximum size of 4M.
+ * Return: max frequency found in @buf.
  *
- * Parsing the sample goes from "low-hanging fruits" (fastest algorithms, likely compressible)
- * to "need more analysis" (likely uncompressible).
+ * (*) This is an adapted version of FSE's https://github.com/Cyan4973/FiniteStateEntropy
  */
+static noinline u32 freq_count(const u8 *buf, const u32 len, u32 *mem, u32 *count)
+{
+	const u8 *p = buf, *end = p + len;
+	u32 c = 0, max_freq = 0;
+	u32 v = mem_read32(p);
+	u32 *acc1 = mem;
+	u32 *acc2 = acc1 + 256;
+	u32 *acc3 = acc2 + 256;
+	u32 *acc4 = acc3 + 256;
+
+	p += sizeof(u32);
+
+	/* Count by stripes of 16 bytes. */
+	while (p < end - 15) {
+		u32 cached = v;
+
+		v = mem_read32(p);
+		p += 4;
+		acc1[(u8)cached]++;
+		acc2[(u8)(cached>>8)]++;
+		acc3[(u8)(cached>>16)]++;
+		acc4[(u8)(cached>>24)]++;
+
+		cached = v;
+		v = mem_read32(p);
+		p += 4;
+		acc1[(u8)cached]++;
+		acc2[(u8)(cached>>8)]++;
+		acc3[(u8)(cached>>16)]++;
+		acc4[(u8)(cached>>24)]++;
+
+		cached = v;
+		v = mem_read32(p);
+		p += 4;
+		acc1[(u8)cached]++;
+		acc2[(u8)(cached>>8)]++;
+		acc3[(u8)(cached>>16)]++;
+		acc4[(u8)(cached>>24)]++;
+
+		cached = v;
+		v = mem_read32(p);
+		p += 4;
+		acc1[(u8)cached]++;
+		acc2[(u8)(cached>>8)]++;
+		acc3[(u8)(cached>>16)]++;
+		acc4[(u8)(cached>>24)]++;
+	}
 
-struct bucket {
-	unsigned int count;
-};
+	p -= 4;
+
+	/* Count lefotver symbols. */
+	while (p < end)
+		acc1[*p++]++;
+
+	/*
+	 * Combine accumulators + store.
+	 *
+	 * Avoid dereferencing @count here because performance.
+	 */
+	for (v = 0; v < 256; v++) {
+		acc1[v] += acc2[v] + acc3[v] + acc4[v];
+
+		if (acc1[v]) {
+			c++;
+			if (acc1[v] > max_freq)
+				max_freq = acc1[v];
+		}
+	}
 
-static inline size_t pow4(size_t n)
-{
-	return n * n * n * n;
+	if (count)
+		*count = c;
+
+	return max_freq;
 }
 
-/*
- * has_low_entropy() - Compute Shannon entropy of the sampled data.
- * @bkt:	Bytes counts of the sample.
- * @slen:	Size of the sample.
+/**
+ * calc_entropy() - Compute Shannon entropy of a sample symbols' frequencies.
+ * @freqs:	frequency counts of the sample
+ * @len:	size of the sample
+ * @scale:	scale for final result
  *
- * Return: true if the level (percentage of number of bits that would be required to
- *	   compress the data) is below the minimum threshold.
+ * @freqs may include zero freq symbols.
+ * The final sum is multiplied by @factor to fit callers' precision requirements.
  *
- * Note:
- * There _is_ an entropy level here that's > 65 (minimum threshold) that would indicate a
- * possibility of compression, but compressing, or even further analysing, it would waste so much
- * resources that it's simply not worth it.
+ * Valid results interpretation for @factor == 1:
+ *   < 6: definitely compressible
+ * 6 - 7: probably compressible, but needs other parameters/heuristics for decisive result
+ *   > 7: probably uncompressible -- other parameters/heuristics _might_ evaluate to "somewhat
+ *	  compressible", but usually not much
  *
- * Also Shannon entropy is the last computed heuristic; if we got this far and ended up
- * with uncertainty, just stay on the safe side and call it uncompressible.
+ * Return: approximate number of bits (scaled to @scale) per byte that would be required to
+ * compress the data.
  */
-static bool has_low_entropy(struct bucket *bkt, size_t slen)
+static __always_inline u32 calc_entropy(const u32 *freqs, const u32 len, const u32 scale)
 {
-	const size_t threshold = 65, max_entropy = 8 * ilog2(16);
-	size_t i, p, p2, len, sum = 0;
+	const u32 len2 = ilog2(len);
+	size_t i, p, sum = 0;
 
-	len = ilog2(pow4(slen));
-
-	for (i = 0; i < 256 && bkt[i].count > 0; i++) {
-		p = bkt[i].count;
-		p2 = ilog2(pow4(p));
-		sum += p * (len - p2);
+	for (i = 0; i < 256; i++) {
+		/* 0 freq won't skew the results (ilog2(n) also returns 0 if n < 2) */
+		p = freqs[i];
+		sum += p * (len2 - ilog2(p));
 	}
 
-	sum /= slen;
-
-	return ((sum * 100 / max_entropy) <= threshold);
+	return (sum * scale / len);
 }
 
-#define BYTE_DIST_BAD		0
-#define BYTE_DIST_GOOD		1
-#define BYTE_DIST_MAYBE		2
 /*
- * calc_byte_distribution() - Compute byte distribution on the sampled data.
- * @bkt:	Byte counts of the sample.
- * @slen:	Size of the sample.
- *
- * Return:
- * BYTE_DIST_BAD:	A "hard no" for compression -- a computed uniform distribution of
- *			the bytes (e.g. random or encrypted data).
- * BYTE_DIST_GOOD:	High probability (normal (Gaussian) distribution) of the data being
- *			compressible.
- * BYTE_DIST_MAYBE:	When computed byte distribution resulted in "low > n < high"
- *			grounds.  has_low_entropy() should be used for a final decision.
+ * Heuristic limits based on 64k blocks.
  */
-static int calc_byte_distribution(struct bucket *bkt, size_t slen)
-{
-	const size_t low = 64, high = 200, threshold = slen * 90 / 100;
-	size_t sum = 0;
-	int i;
-
-	for (i = 0; i < low; i++)
-		sum += bkt[i].count;
-
-	if (sum > threshold)
-		return BYTE_DIST_BAD;
-
-	for (; i < high && bkt[i].count > 0; i++) {
-		sum += bkt[i].count;
-		if (sum > threshold)
-			break;
-	}
-
-	if (i <= low)
-		return BYTE_DIST_GOOD;
-
-	if (i >= high)
-		return BYTE_DIST_BAD;
-
-	return BYTE_DIST_MAYBE;
-}
-
-static bool is_mostly_ascii(const struct bucket *bkt)
-{
-	size_t count = 0;
-	int i;
-
-	for (i = 0; i < 256; i++)
-		if (bkt[i].count > 0)
-			/* Too many non-ASCII (0-63) bytes. */
-			if (++count > 64)
-				return false;
-
-	return true;
-}
-
-static bool has_repeated_data(const u8 *sample, size_t len)
-{
-	size_t s = len / 2;
-
-	return (!memcmp(&sample[0], &sample[s], s));
-}
-
-static int cmp_bkt(const void *_a, const void *_b)
-{
-	const struct bucket *a = _a, *b = _b;
-
-	/* Reverse sort. */
-	if (a->count > b->count)
-		return -1;
-
-	return 1;
-}
+#define HEURISTIC_CHUNK_SIZE		SZ_64K
+static const u32 heuristic_freq_hi	= 8000;
+static const u32 heuristic_freq_lo	= 5000;
+static const u32 heuristic_entropy_hi	= 800;
+static const u32 heuristic_entropy_mid	= 700;
+static const u32 heuristic_entropy_lo	= 600;
+static const u32 heuristic_alphabet_hi	= 256;
+static const u32 heuristic_alphabet_lo	= 128;
+
+enum {
+	UNCOMPRESSIBLE = 0,
+	COMPRESSIBLE,
+	MAYBE_COMPRESSIBLE,
+};
 
 /*
- * Collect some 2K samples with 2K gaps between.
+ * Computes entropy and byte dominance of @buf, on a 64k block basis.
+ *
+ * Return: one of the *COMPRESSIBLE values
  */
-static int collect_sample(const struct iov_iter *source, ssize_t max, u8 *sample)
+static int check_compressible_chunks(const u8 *buf, const u32 len, u32 *freqs)
 {
-	struct iov_iter iter = *source;
-	size_t s = 0;
-
-	while (iov_iter_count(&iter) >= SZ_2K) {
-		size_t part = umin(umin(iov_iter_count(&iter), SZ_2K), max);
-		size_t n;
-
-		n = copy_from_iter(sample + s, part, &iter);
-		if (n != part)
-			return -EFAULT;
+	int score = 0, boost = 0, n = 0;
+	const u32 nsamples = (len / HEURISTIC_CHUNK_SIZE);
+	const s32 half_samples = nsamples / 2;
+	const u8 *end = buf + len;
+
+	while (buf < end) {
+		u32 alphabet = 0;
+		const u32 max_freq = freq_count(buf, HEURISTIC_CHUNK_SIZE, freqs, &alphabet);
+		const u32 entropy = calc_entropy(freqs, HEURISTIC_CHUNK_SIZE, 100);
+
+		if (entropy < heuristic_entropy_mid && max_freq > heuristic_freq_hi)
+			score++;
+		else if (entropy >= heuristic_entropy_mid && max_freq < heuristic_freq_lo)
+			score--;
+
+		if (alphabet < heuristic_alphabet_hi) {
+			boost++;
+			if (alphabet < heuristic_alphabet_lo)
+				boost++;
+		}
+
+		/* Reward/penalty for really good/bad entropy. */
+		if (entropy > heuristic_entropy_hi)
+			boost -= 2;
+		else if (entropy < heuristic_entropy_lo)
+			boost += 2;
+
+		memset(freqs, 0, sizeof(u32) * 1024);
+		buf += HEURISTIC_CHUNK_SIZE;
+		n++;
+
+		if (end - buf < HEURISTIC_CHUNK_SIZE)
+			break;
+	}
 
-		s += n;
-		max -= n;
+	/* Check how much the heuristics above impacted (at least) half of @buf. */
+	if (score + boost > 0 && score + boost < half_samples)
+		return MAYBE_COMPRESSIBLE;
 
-		if (iov_iter_count(&iter) < PAGE_SIZE - SZ_2K)
-			break;
+	if (score + boost > -half_samples) {
+		if (score > half_samples || score + boost > half_samples)
+			return COMPRESSIBLE;
 
-		iov_iter_advance(&iter, SZ_2K);
+		return MAYBE_COMPRESSIBLE;
 	}
 
-	return s;
+	return UNCOMPRESSIBLE;
 }
 
 /*
- * is_compressible() - Determines if a chunk of data is compressible.
- * @data: Iterator containing uncompressed data.
+ * Check @buf heuristics (entropy/distribution) to determine its compressibility level.
+ *
+ * Tests shows that this function is quite reliable in predicting data compressibility, matching
+ * very close with the behaviour of LZ77 compression success and failures.
  *
- * Return: true if @data is compressible, false otherwise.
+ * This function allocates memory, callers must check for -ENOMEM.
  *
- * Tests shows that this function is quite reliable in predicting data compressibility,
- * matching close to 1:1 with the behaviour of LZ77 compression success and failures.
+ * Return: one of the *COMPRESSIBLE values on success, -errno otherwise.
  */
-static bool is_compressible(const struct iov_iter *data)
+static __must_check int check_compressible(const u8 *buf, u32 len)
 {
-	const size_t read_size = SZ_2K, bkt_size = 256, max = SZ_4M;
-	struct bucket *bkt = NULL;
-	size_t len;
-	u8 *sample;
-	bool ret = false;
-	int i;
-
-	/* Preventive double check -- already checked in should_compress(). */
-	len = iov_iter_count(data);
-	if (unlikely(len < read_size))
-		return ret;
-
-	if (len - read_size > max)
-		len = max;
-
-	sample = kvzalloc(len, GFP_KERNEL);
-	if (!sample) {
-		WARN_ON_ONCE(1);
-
-		return ret;
-	}
-
-	/* Sample 2K bytes per page of the uncompressed data. */
-	i = collect_sample(data, len, sample);
-	if (i <= 0) {
-		WARN_ON_ONCE(1);
-
-		goto out;
-	}
+	u32 entropy, *freqs, rle = 0, rle_boost = 0;
+	const u32 min_reps = (len / 100); /* ~1% of @len */
+	const u8 *p;
+	int ret;
 
-	len = i;
-	ret = true;
+	if (unlikely(!buf || !len))
+		return -EINVAL;
 
-	if (has_repeated_data(sample, len))
-		goto out;
+	/* Stage 1: RLE (@buf is filled with same byte, a.k.a Run-Length Encoding). */
+	p = memchr_inv(buf, *buf, len);
+	if (!p)
+		/* Full RLE */
+		return COMPRESSIBLE;
 
-	bkt = kzalloc_objs(*bkt, bkt_size);
-	if (!bkt) {
-		WARN_ON_ONCE(1);
-		ret = false;
+	/* Stage 2: partial/starting RLE (repeating bytes cover are at least @min_reps long). */
+	if (p - buf >= min_reps) {
+		rle = (p - buf);
 
-		goto out;
+		buf += rle;
+		len -= rle;
 	}
 
-	for (i = 0; i < len; i++)
-		bkt[sample[i]].count++;
-
-	if (is_mostly_ascii(bkt))
-		goto out;
-
-	/* Sort in descending order */
-	sort(bkt, bkt_size, sizeof(*bkt), cmp_bkt, NULL);
-
-	i = calc_byte_distribution(bkt, len);
-	if (i != BYTE_DIST_MAYBE) {
-		ret = !!i;
-
-		goto out;
+	/*
+	 * This might happen if:
+	 *   1. original @len was <= chunk size
+	 *   2. remaining @len after a long partial RLE
+	 *
+	 * In either case, we call it compressible (for (1), it's just faster to let compressor
+	 * handle it).
+	 */
+	if (len <= HEURISTIC_CHUNK_SIZE)
+		return COMPRESSIBLE;
+
+	freqs = kzalloc_objs(*freqs, 1024);
+	if (!freqs)
+		return -ENOMEM;
+
+	/*
+	 * Stage 3: whole buffer entropy check (decisive results if too high or too low).
+	 *
+	 * Don't care about max freq or alphabet size here.
+	 */
+	(void)freq_count(buf, len, freqs, NULL);
+	entropy = calc_entropy(freqs, len, 1);
+	/*
+	 * Reduce entropy based on @rle coverage.
+	 * Its usage is similar to decrementing @entropy for every 1/4 of @buf covered by
+	 * repeating bytes.
+	 * Apply boost only if @entropy < 8 and @rle_boost > 0.
+	 */
+	rle_boost = umin(rle >> (ilog2(len) - 1), entropy);
+	ret = MAYBE_COMPRESSIBLE;
+
+	if (entropy >= 8)
+		ret = UNCOMPRESSIBLE;
+	else if (entropy < 6 || (rle_boost && entropy - rle_boost <= 6))
+		ret = COMPRESSIBLE;
+
+	/*
+	 * Stage 4: if no decisive result yet, break down buffer into 64k chunks for more detailed
+	 * analysis (requires >= 2 chunks).
+	 *
+	 * Note that it's quite common to go from MAYBE_COMPRESSIBLE -> COMPRESSIBLE after
+	 * per-chunk analysis.
+	 *
+	 * OTOH, getting MAYBE_COMPRESSIBLE again here really means 'uncompressible' for
+	 * compressors that focus on speed (e.g. LZ77).
+	 * Compressors focused on compression ratio might still get good compression though.
+	 */
+	if (ret == MAYBE_COMPRESSIBLE && len >= 2 * HEURISTIC_CHUNK_SIZE) {
+		memset(freqs, 0, sizeof(u32) * 1024);
+		ret = check_compressible_chunks(buf, len, freqs);
 	}
 
-	ret = has_low_entropy(bkt, len);
-out:
-	kvfree(sample);
-	kfree(bkt);
+	kfree(freqs);
 
 	return ret;
 }
@@ -275,7 +324,6 @@ static bool is_compressible(const struct iov_iter *data)
  * - server has enabled compression for the share
  * - it's a read or write request
  * - (write only) request length is >= SMB_COMPRESS_MIN_LEN
- * - (write only) is_compressible() returns 1
  *
  * Return false otherwise.
  */
@@ -295,10 +343,7 @@ bool should_compress(const struct cifs_tcon *tcon, const struct smb_rqst *rq)
 	if (shdr->Command == SMB2_WRITE) {
 		const struct smb2_write_req *wreq = rq->rq_iov->iov_base;
 
-		if (le32_to_cpu(wreq->Length) < SMB_COMPRESS_MIN_LEN)
-			return false;
-
-		return is_compressible(&rq->rq_iter);
+		return (le32_to_cpu(wreq->Length) >= SMB_COMPRESS_MIN_LEN);
 	}
 
 	return (shdr->Command == SMB2_READ);
@@ -332,6 +377,23 @@ int smb_compress(struct TCP_Server_Info *server, struct smb_rqst *rq, compress_s
 		goto err_free;
 	}
 
+	/*
+	 * Even though smb_lz77_compress() runs quite fast on uncompressible data (it ends up
+	 * usually just parsing the input buffer), this is much faster.
+	 *
+	 * So to keep things balanced (compress performance vs prediction accuracy), let's drop the
+	 * uncompressible low-hanging fruits here and let smb_lz77_compress() handle the
+	 * exceptions/rare cases.
+	 */
+	ret = check_compressible(src, slen);
+
+	/* XXX: do something with MAYBE_COMPRESSIBLE */
+	if (ret != COMPRESSIBLE) {
+		if (ret >= 0)
+			ret = send_fn(server, 1, rq);
+		goto err_free;
+	}
+
 	dlen = smb_lz77_compressed_alloc_size(slen);
 	dst = kvzalloc(dlen, GFP_KERNEL);
 	if (!dst) {
-- 
2.54.0