Output formats - YadeWira/ultra-fast-lzma2 GitHub Wiki

Output formats

The same LZMA2 data can leave the library in three wrappings. The encoder runs identically in all three; only the framing around its output differs.

format how read by integrity
native (default) nothing to set this library xxhash32, flagged in the property byte
raw LZMA2 UF2_p_omitProperties = 1 any LZMA2 decoder, given the property byte the container's
.xz UF2_p_format = UF2_format_xz xz, 7-Zip, any .xz decoder CRC64 (default), CRC32 or none

Native

fast-lzma2's framing, unchanged: a property byte, the LZMA2 chunks, and by default a 32-bit xxhash of the input. The hash is flagged in the top bit of the property byte. A standard LZMA2 property is at most 40, so that bit is never set in a standard stream, and a standard decoder rejects this output at its first byte unless it is written with UF2_p_doXXHash = 0.

Raw LZMA2, for containers

For a container that stores the property byte itself, as 7-Zip does in its archive header:

UF2_CCtx_setParameter(cctx, UF2_p_omitProperties, 1);
size_t n = UF2_compressCCtx(cctx, dst, cap, src, srcSize, level);
unsigned char prop = UF2_getCCtxDictProp(cctx);   /* store this in the container */

Omitting the property byte also drops the hash, because the flag has nowhere to go. This is how 7-Zip-zstd uses the library. It is why the default format has to stay native: an integrator that sets only omitProperties must keep receiving raw LZMA2.

.xz

UF2_CCtx_setParameter(cctx, UF2_p_format, UF2_format_xz);
UF2_CCtx_setParameter(cctx, UF2_p_xzCheck, 4);        /* 4 = CRC64, 1 = CRC32, 0 = none */
UF2_CCtx_setParameter(cctx, UF2_p_xzBlockSize, 0);    /* 0 = one block per dictionary reset */
size_t n = UF2_compressCCtx(cctx, dst, cap, src, srcSize, level);   /* or UF2_compressMt() */

A standard .xz file ("The .xz File Format" 1.2.1) with the LZMA2 data inside, in one or more blocks. The dictionary each Block Header declares is capped at the block size, the same way the native format caps its own, so a small input does not make its decoder reserve the whole dictionary of a high level. UF2_compressBound() covers both formats.

Blocks, and decoding on several threads

Each block is an ordinary compression of its slice of the input, starting with a fresh dictionary, so blocks are independent and decode in parallel. UF2_p_xzBlockSize sets their uncompressed size; the default, 0, ends a block wherever the encoder resets its dictionary anyway, every dictionary size times UF2_p_resetInterval (4 by default) bytes. That split costs only the block framing: an input no larger than one reset interval gives exactly the bytes of 1.2.0, and level 6 already writes Silesia as 4 blocks at the same size. An explicit size must be at least UF2_XZ_BLOCKSIZE_MIN (1 MiB); smaller blocks decode on more threads and compress worse.

A multi-threaded context (UF2_decompressMt(), UF2_createDCtxMt()) decodes the blocks of a stream side by side. It needs both sizes in every Block Header to place each block before decoding the one before it; this library always writes them, and so does xz when it compresses on several threads. Files without them, such as those of xz -T1, are decoded in order. xz 5.4 and later also decode this library's multi-block files on several threads.

Silesia (211 MB, one file), x86_64, assembler decoder, CRC64, best of 3 in round robin:

level block size 1 thread 2 4 8 16
10 default (1 block) 22.966% 107 MB/s 105 104 109 109
10 64 MiB (4 blocks) 22.998% 104 157 276 281 278
10 32 MiB (7) 23.171% 107 198 295 505 542
10 16 MiB (13) 23.363% 102 187 325 542 859
10 8 MiB (26) 23.673% 102 187 337 557 803
6 default (64 MiB, 4) 23.670% 101 151 274 272 271
6 32 MiB (7) 23.723% 104 203 285 510 547
6 16 MiB (13) 23.758% 107 184 316 553 893
6 8 MiB (26) 24.022% 101 191 332 548 810

Speed stops rising once every block has a thread: 4 blocks top out near 280 MB/s on any thread count. Past 16 MiB, halving the block costs more ratio than the extra threads give back (8 MiB is slower than 16 MiB on 16 threads). xz 5.8.3 decodes the 16 MiB level-10 file at 557 MB/s on 16 threads and 94 MB/s on one, through a pipe.

Streaming

UF2_CStream writes .xz when UF2_p_format is set before UF2_initCStream(), through UF2_compressStream(), the zero-copy UF2_getDictionaryBuffer() / UF2_getNextCompressedBuffer() pair and with a timeout alike. The LZMA2 data is exactly the native stream's; the framing is queued around it. A block starts at each dictionary reset, as in the one-shot default, but its header has to be written before its data, when neither size is known yet, so by default it leaves them out, as xz does on one thread, and such a file decompresses on one thread only.

UF2_p_xzSizedHeaders set to 1 changes that: the encoder holds each block's compressed data in memory until the block ends, then writes its header with both sizes into room kept before the data. The file then decompresses on several threads like one-shot output, here and in xz 5.4 and later. It costs memory up to one block's compressed size, and a block is dictionary size times UF2_p_resetInterval of input (4 dictionaries by default, up to 512 MiB of input at level 10; a reset interval of 1 brings it to one dictionary). UF2_flushStream() writes nothing of the open block in this mode, since its sizes are not known yet. It works with every streaming API, timeouts and dual buffering included.

UF2_DStream recognises .xz by its first byte and decodes it on one thread, checking every CRC of the framing, each block check, and the Index against the blocks decoded. The Index is compared through a summary (count, the sums of both sizes and a CRC32 of the size pairs), so no per-block record is kept however long the file. UF2_decompressStream() returns 0 at the end of each Stream and accepts Stream Padding or another Stream after it, so a concatenated file decodes the same however its input is split; anything else after a Stream is an error. Input that ends inside Stream Padding that is not a multiple of four is left unfinished (1), which is how a streaming caller sees a truncated file.

The framing costs nothing measurable. Silesia at level 6, one thread, 1 MiB input and output chunks, best of 3: native stream 102.0 MB/s, streamed .xz with CRC64 103.4 MB/s; one-shot decoding of the same files 104.9 and 103.4 MB/s.

Reading

Nothing to set: UF2_decompress(), UF2_decompressMt() and UF2_decompressDCtx() recognise .xz by its signature. Its first byte, 0xFD, is never a valid native property byte, so there is no ambiguity. They also read .xz written by xz itself: any number of blocks, with or without sizes in the Block Headers, concatenated streams, Stream Padding, and CRC32, CRC64 or no check. Every CRC of the framing and every block check is verified, and the Index is compared against the blocks actually decoded. UF2_findDecompressedSize() reads the size from the Index.

What the decoder cannot read, it rejects with parameter_unsupported rather than decoding it wrongly: filter chains such as BCJ or delta, and SHA-256.

What the check costs

The check is the one part of .xz that costs decompression speed. Silesia at level 6, one thread, assembler decoder, best of five in round robin, each check against .xz with no check:

check one-shot, tables one-shot, hardware streamed, tables streamed, hardware
CRC32 -4.1% -1.6%, then about -0.8% -3.2% -0.5%
CRC64 -6.4% -1.6%, then about -0.8% -4.9% -0.4%

Since the version after 1.4.0 both CRCs fold the data 64 bytes at a time with a carry-less multiply where the processor has one: PCLMULQDQ on x86 (32 and 64 bit), PMULL on ARM64 Linux and Apple, detected at run time. Elsewhere, and below 64 bytes, they use slicing-by-16 over constant tables. On the E5-2697A v4 both run at 7.2 GB/s against 3.1 (CRC32) and 2.5 GB/s (CRC64) with the tables. tools/gen_xz_tables.py generates the tables and the fold constants, and simulates the fold against the tables before writing anything. The first, byte-at-a-time version cost 22%.

One-shot decompression first kept more of the cost because it checked a block only once the whole block was decoded, when most of it had left the cache, so the CRC ran at memory speed: 7.1 GB/s for CRC64 against 16 GB/s from cache. It now runs the check 256 KiB at a time as the block is decoded, which brought the cost of CRC64 from 1.7% to 0.8% on average (best of 15, three alternated runs each; the runs overlap, so the gain is about a point, not a precise one). The streaming decoder already checked each output buffer while it was in cache. A single block decoded by the multi-threaded LZMA2 decoder, split at its dictionary resets, is still checked at the end. The ARM64 speed is untested: qemu shows only that the results are right.