Skip to content

Lost opportunity: batching incompressible literal runs with zero-length backrefs #99

Description

@LordMike

Summary

While benchmarking heatshrink for small embedded targets, I ran into what looks like a missed opportunity in the bitstream format: incompressible data has to be emitted as individual literals, so it trends toward about 112.5% of the original size.

A possible alternative would be to reuse an otherwise meaningless backref length code. A backref with decoded length 0 is not useful as a copy operation, so that code could instead mean "raw bytes follow". This would allow long literal spans to be batched without paying one tag bit per byte.

This is related to #17, which already discusses that some length-code space is effectively wasted and that improving it would require a breaking format change.

Background

Heatshrink is attractive for small streaming decoders because the decoder is simple, deterministic, and has bounded memory usage.

The awkward case is random, noisy, or already-compressed input. As I understand the current format, each literal is encoded as:

1 tag bit + 8 data bits

So if the encoder cannot find useful backrefs, the stream expands by about one bit per byte:

9 bits emitted per 8 bits input = 112.5%

Possible encoding

One possible format change would be to reserve the otherwise useless zero-length backref as a raw-run escape.

For example:

tag = 0
offset = raw_len - 1
length = 0
raw bytes follow

Interpreted as a normal backref, length = 0 does not make sense: copying zero bytes is a no-op, and spending bits to encode a no-op is not useful. That makes it a natural place to piggyback a control marker.

The offset field could then carry the raw-copy length instead:

raw_len = offset + 1

Conceptually:

current:
literal, literal, literal, literal, ...

possible raw-run form:
raw-run(length), byte, byte, byte, byte, ...

The encoder could still use normal literals for short spans where the raw-run header is not worth it.

What it solves

This mainly improves worst-case behavior.

In an experimental variant, long incompressible spans could be emitted as raw runs. That means noisy/random inputs that previously expanded to around 112% could get much closer to 100%.

It does not make heatshrink compete with zlib/DEFLATE on compression ratio. The benefit is more specific: it removes the constant one-bit-per-byte penalty in cases where the encoder already knows the data is not usefully compressible.

Experiment

We tested this while comparing small-memory compression options here:

https://github.com/LordMike/opendisplay-compression-benchmark

The experimental variant is called heatshrinkraw.

In the benchmark, the raw-run variant improved the bad random/noisy cases substantially. Regular heatshrink could expand incompressible inputs to around 112%, while the raw-run variant brought those cases down to roughly 100%.

The average ratio still remained worse than zlib, but the worst-case expansion became much less awkward.

Compatibility

This would be breaking.

Existing decoders would not treat length = 0 as a raw-run marker. So this would need to be a new stream version, explicit variant, or otherwise negotiated format option.

It also consumes one length code that could otherwise be used for a short backref if the format were changed in the direction discussed in #17. Whether that is worth it depends on the workload, but for the tested data the tradeoff was useful for reducing worst-case expansion.

Why I mention it

Heatshrink is otherwise very attractive for tiny streaming decoders. The literal encoding choice leaves avoidable worst-case expansion on the table, and a raw-run escape seems like it could address that while preserving most of the existing format model.

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