-
-
Notifications
You must be signed in to change notification settings - Fork 7.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
extmod/moddeflate.c: Add deflate.DeflateIO providing compression and decompression. #11905
Conversation
Code size report:
|
Some quick performance tests: For compression (on the unix port), MicroPython (wbits=8) is about 8x slower than Python (wbits=15) to compress the same file. (4x if you use wbits=6). In terms of bytes/second: MicroPython's decompression performance is impacted slightly by not buffering its input as it reads byte at a time from the underlying stream. On PYBV11 it can be increased to 280kiB/s by adding a 4 byte buffer. Diminishing marginal returns for a bigger buffer. Similar relative gain on Unix (22MiB/s). MicroPython's compression performance is limited by the linear searching in the history window (it's not meant to be fast, it's meant to use less memory). |
I'm really excited to see this coming together, thanks @jimmo ! |
Codecov Report
@@ Coverage Diff @@
## master #11905 +/- ##
========================================
Coverage 98.41% 98.41%
========================================
Files 155 156 +1
Lines 20564 20696 +132
========================================
+ Hits 20238 20368 +130
- Misses 326 328 +2
|
Re performance: compiling relevant |
extmod/moddeflate.c
Outdated
// -----CMF------ ----------FLG--------------- | ||
// CINFO(5) CM(3) FLEVEL(2) FDICT(1) FCHECK(5) | ||
uint8_t buf[] = { 0x08, 0x80 }; // CM=2 (deflate), FLEVEL=2 (default), FDICT=0 (no dictionary) | ||
buf[0] |= MAX(self->window_bits - 8, 1) << 4; // base-2 logarithm of the LZ77 window size, minus eight. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this use wbits
? In case self->window_bits==0
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch. Fixed.
extmod/moddeflate.c
Outdated
static const mp_arg_t allowed_args[] = { | ||
{ MP_QSTR_stream, MP_ARG_REQUIRED | MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL} }, | ||
{ MP_QSTR_format, MP_ARG_INT, {.u_int = DEFLATEIO_FORMAT_NONE} }, | ||
{ MP_QSTR_wbits, MP_ARG_INT, {.u_int = 0} }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps the default here should be DEFLATEIO_DEFAULT_WBITS
, and then the code that checks for wbits==0
elsewhere in this file can be removed?
Edit: or does wbits=0
mean something special?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Edit: or does
wbits=0
mean something special?
Yes.
extmod/moddeflate.c
Outdated
{ MP_QSTR_close, MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_rom_obj = MP_ROM_FALSE} }, | ||
}; | ||
mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; | ||
mp_arg_parse_all_kw_array(n_args, n_kw, all_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Possible make these positional-only args, to reduce code size and allow a natmod version.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
I added a new test for stream errors. |
extmod/moddeflate.c
Outdated
STATIC mp_uint_t deflateio_read(mp_obj_t o_in, void *buf, mp_uint_t size, int *errcode) { | ||
mp_obj_deflateio_t *self = MP_OBJ_TO_PTR(o_in); | ||
|
||
if (!deflateio_init_read(self) || self->stream == MP_OBJ_NULL) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These checks should be swapped.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
ea972d4
to
b67d30e
Compare
Added |
Made compression enabled at the "full" level by default, and updated docs to match. |
680da47
to
a78e5be
Compare
Updated to address comments. Added |
This will be replaced with a new deflate module providing the same functionality, with an optional frozen Python wrapper providing a replacement zlib module. binascii.crc32 is temporarily disabled. This work was funded through GitHub Sponsors. Signed-off-by: Jim Mussared <jim.mussared@gmail.com>
There are enough places that implement __exit__ by forwarding directly to mp_stream_close that this saves code size. For the cases where __exit__ is a no-op, additionally make their MP_STREAM_CLOSE ioctl handled as a no-op. This work was funded through GitHub Sponsors. Signed-off-by: Jim Mussared <jim.mussared@gmail.com>
The compression algorithm implemented in this commit uses much less memory compared to the standard way of implementing it using a hash table and large look-back window. In particular the algorithm here doesn't allocate hash table to store indices into the history of the previously seen text. Instead it simply does a brute-force-search of the history text to find a match for the compressor. This is slower (linear search vs hash table lookup) but with a small enough history (eg 512 bytes) it's not that slow. And a small history does not impact the compression too much. To give some more concrete numbers comparing memory use between the approaches: - Standard approach: inplace compression, all text to compress must be in RAM (or at least memory addressable), and then an additional 16k bytes RAM of hash table pointers, pointing into the text - The approach in this commit: streaming compression, only a limited amount of previous text must be in RAM (user selectable, defaults to 512 bytes). To compress, say, 1k of data, the standard approach requires all that data to be in RAM, plus an additional 16k of RAM for the hash table pointers. With this commit, you only need the 1k of data in RAM. Or if it's streaming from a file (or elsewhere), you could get away with only 256 bytes of RAM for the sliding history and still get very decent compression. In summary: because compression takes such a large amount of RAM (in the standard algorithm) and it's not really suitable for microcontrollers, the approach taken in this commit is to minimise RAM usage as much as possible, and still have acceptable performance (speed and compression ratio). Signed-off-by: Damien George <damien@micropython.org>
Because we only use the streaming source, this is just extra code size. Saves 64 bytes on PYBV11. Signed-off-by: Jim Mussared <jim.mussared@gmail.com>
This commit makes the following changes: - Replace 256-byte reverse-bits-in-byte lookup table with computation. - Replace length and distance code lookup tables with computation. - Remove comp_disabled check (it's unused). - Make the dest_write_cb take the data pointer directly, rather than the Outbuf. Saves 500 bytes on PYBV11. Signed-off-by: Jim Mussared <jim.mussared@gmail.com>
This library used a mix of "tinf" and "uzlib" to refer to itself. Remove all use of "tinf" in the public API. This work was funded through GitHub Sponsors. Signed-off-by: Jim Mussared <jim.mussared@gmail.com>
This supports `wbits` values between +40 to +47. This work was funded through GitHub Sponsors. Signed-off-by: Jim Mussared <jim.mussared@gmail.com>
Saves 68 bytes on PYBV11. This work was funded through GitHub Sponsors. Signed-off-by: Jim Mussared <jim.mussared@gmail.com>
Collapsing the two adjacent calls to outbits saves 32 bytes. Bringing defl_static.c into lz77.c allows better inlining, saves 24 bytes. Merge the Outbuf/uzlib_lz77_state_t structs, a minor simplification that doesn't change code size. This work was funded through GitHub Sponsors. Signed-off-by: Jim Mussared <jim.mussared@gmail.com>
For better abstraction for users of this API. Signed-off-by: Jim Mussared <jim.mussared@gmail.com>
This provides similar functionality to the former zlib.DecompIO and especially CPython's gzip.GzipFile for both compression and decompression. This class can be used directly, and also can be used from Python to implement (via io.BytesIO) zlib.decompress and zlib.compress, as well as gzip.GzipFile. Enable/disable this on all ports/boards that zlib was previously configured for. This work was funded through GitHub Sponsors. Signed-off-by: Jim Mussared <jim.mussared@gmail.com>
Signed-off-by: Jim Mussared <jim.mussared@gmail.com>
Also update zlib & gzip docs to describe the micropython-lib modules. This work was funded through GitHub Sponsors. Signed-off-by: Jim Mussared <jim.mussared@gmail.com>
This replaces the previous zlib version. This work was funded through GitHub Sponsors. Signed-off-by: Jim Mussared <jim.mussared@gmail.com>
Now merged! Thanks @jimmo for all the hard work on this. |
This replaces #11879 -- because there was no way to provide a
gzip.GzipFile
that exactly matched CPython's (e.g. because we need thewbits
parameter), instead we will put this functionality in a separate module/class. I have updated micropython/micropython-lib#694 to also provide a CPython-compatiblegzip
module.Updated copy of the #11879 description below:
Summary: Adds gzip/zlib/raw-deflate compression support. (See issues #7228, #5590, and earlier PRs #8195, #5613).
Historically MicroPython provided a CPython-compatible implementation of
zlib.decompress
as well as a MicroPython-specificzlib.DecompIO
which is similar to CPython'szlib.decompressobj
except more useful as a stream wrapper, for example to decompress a file or socket, because you pull (read) decompressed data out of it, rather than pushing (write) compressed data into it.However, CPython also provides
gzip.GzipFile
which works exactly like MicroPython'sDecompIO
, the only limitation is that it only supports gzip-format data (whereasDecompIO
supported the samewbits
argument thatzlib.decompress
supported).This PR removes the
zlib
module, and replaces it with adeflate
module providing a stream wrapper classDeflateIO
that is essentially DeflateIO with compression support, but also suitable as a building block for implementinggzip.GzipFile
. Because it no longer part of thezlib
module, it also has a simpler API for specifying format and window size.For backwards compatibility, I have written a pure-Python implementation of the functionality of the former
zlib
module (including nowzlib.compress
), as well as thegzip
module (includingcompress
,decompress
,open
, andGzipFile
), which will be published to micropython-lib (micropython/micropython-lib#694) and can be optionally installed via mip (or frozen). Even though they are very small (~450 bytes each), they are not frozen by default, with the justification:This PR includes the following changes:
mp_stream___exit___obj
that avoids each file/stream-like object needing to implement this themselves. (This is a code size optimisation saving ~200 bytes)zlib
module.deflate
module.deflate.DeflateIO
.zlib
and add docs forgzip
to describe the micropython-lib versions.deflate
.Overall this PR adds +1156 bytes to PYBV11 (i.e. adding compression support). This is made up of -2840 (remove zlib), +2928 (add decompression-only DeflateIO), +1068 (add compression support to DeflateIO).
Currently compression support (via
MICROPY_PY_DEFLATE_COMPRESS
) is enabled at the same level ("extra features") as decompression (viaMICROPY_PY_DEFLATE
), but I think we should consider making it either "full features" or "everything". I do think we should enable it on Unix/Windows though (but really I think that means we should make it "full" and move Unix/Windows to "full").Compared to #8195 & #5613 which are quite similar (cc @harbaum, @andrewleech) this PR adds support for streaming as well as non-gzip (i.e. zlib/raw) formats and a code size reduction as well as reduced memory usage in the compressor (see #11879 (comment) for more notes from @dpgeorge).
This work was funded through GitHub Sponsors, by specific request from a supporter.