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:
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:
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.
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
0is 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:
So if the encoder cannot find useful backrefs, the stream expands by about one bit per byte:
Possible encoding
One possible format change would be to reserve the otherwise useless zero-length backref as a raw-run escape.
For example:
Interpreted as a normal backref,
length = 0does 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
offsetfield could then carry the raw-copy length instead:Conceptually:
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 to100%.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 roughly100%.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 = 0as 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.