1
0
mirror of https://github.com/lz4/lz4.git synced 2026-10-24 01:48:57 +03:00
Commit Graph

4111 Commits

Author SHA1 Message Date
Yann Collet
35d74f85f7 lz4hc: LZ4_saveDictHC() before any compression no longer corrupts the next block
On a new or reset stream (prefixStart == NULL), LZ4_saveDictHC() installed @safeBuffer as prefix,
which skipped the initialization done by the next compression (LZ4HC_init_internal()).
The stream then started at index 0, which the hash table uses for empty entries :
positions matched themselves (offset 0), and the block decoded into wrong data, at all HC levels,
also with an attached dictionary. Present in v1.10.0.
It now saves nothing, leaving the stream to be initialized by the next compression.
Found by lz4frame with uncompressed then compressed segments in linked blocks.
tests : fuzzer.c saveDictHC test #926 compresses after LZ4_saveDictHC(), without and with an attached dictionary.
2026-10-22 12:58:19 -07:00
Yann Collet
124e0d4da8 lz4frame: exact bounds, the job is the unit with nbWorkers >= 1 ; uncompressed segments are jobs ; CLI output buffer independent of nb of threads (checkpoint)
LZ4F_compressBound(), without autoFlush :
- single-threaded : (srcSize / blockSize + 1) blocks, stored uncompressed in worst case, plus the frame footer.
  It covers up to (blockSize-1) buffered bytes, a buffered block emitted separately (mode switch),
  or remaining from an incomplete LZ4F_flush(). srcSize == 0 : one block + footer, so that LZ4F_flush() and
  LZ4F_compressEnd() complete in a single invocation, strictly below the bound (v1.10.0 guarantee kept).
- nbWorkers >= 1 : the job replaces the block : ceil(srcSize / jobSize) jobs (1 for srcSize == 0) + footer,
  independently of nbWorkers. Updates defer completed jobs not fitting into dstBuffer : output is only required
  to start a job while all slots are busy. Flush and end emit jobs in flight in several invocations.
  nbWorkers is new : no existing user depends on a single invocation there.
Multi-threaded LZ4F_uncompressedUpdate() : uncompressed segments are jobs storing blocks uncompressed ;
a mode switch submits the job being filled (same output as single-threaded mode). A single LZ4F_mt_update()
serves both updates : uncTail, LZ4F_mt_emitUncTail() and LZ4F_uncompressedUpdate_MT() are removed.
lz4 : output buffer sized by LZ4F_compressBound(chunk) (~4 MB, instead of 72 MB with 8 threads),
end of frame in a loop ; more threads within a memory budget (--memory=300M : 13 -> 23 threads) ;
releaseLag = (2*nbWorkers+1) jobs + 64 KB.
tests : frameMT (bound structure, flush / end loops), stream fuzzer (reference loops with nbWorkers).
2026-10-22 12:58:19 -07:00
Yann Collet
fa086fd0d6 lz4frame: LZ4F_flush() and LZ4F_compressEnd() fill dstBuffer or complete ; CLI releaseLag from LZ4F_compressBound(0) (checkpoint)
LZ4F_flush() and LZ4F_compressEnd() : each invocation fills dstBuffer, or completes :
while it returns dstCapacity, output may remain, and the operation is invoked again, with any dstCapacity.
Output remains strictly below LZ4F_compressBound(0) (+1 for autoFlush), so that callers sizing
dstBuffer with the bound see no change : a single invocation completes.
- the first invocation sets the flush point : buffered input is compressed (all jobs waited for),
  so input is no longer read after it returns (LZ4F_srcStableUntilFlush lifetime unchanged) ;
- single-threaded : a block not fitting is compressed into stageBuff (allocated on first need), same output ;
  multi-threaded : the oldest job can be emitted partially (mt->emitted) ; frame footer staged (8 bytes) ;
- compressUpdate() / uncompressedUpdate() may follow an incomplete flush : remaining output comes first ;
  until an end completes, only LZ4F_compressEnd() continues it (LZ4F_compressBegin() abandons it) ;
- dstCapacity 0 is rejected when output remains, without modifying the frame ;
- contentSize mismatch is detected when the end starts, before writing anything ;
- LZ4F_compressFrame*() keep failing with dstCapacity_tooSmall when the frame doesn't fit.
lz4file : LZ4F_writeClose() loops on LZ4F_compressEnd().
lz4 : releaseLag = LZ4F_compressBound(0) + 64 KB (bound of input retained within cctx).
tests : frametest (#1817 streaming probes, fuzzer), frameMT (validatePartialFlush, small dst, strict bound),
stream fuzzer : flush and end in pieces.
2026-10-22 12:58:19 -07:00
Yann Collet
a88e3a7311 review round 11: allocations within workers documented, CLI nb of threads only grows across files, single-job files without workers, compressBound() with one job slot without multithreading (checkpoint)
lz4hc levels >= 10 allocate a temporary buffer (LZ4HC_HEAPMODE) within worker threads :
documented in lz4frame.h (nbWorkers), lz4frame.c (pool) and lz4.c (LZ4_USER_MEMORY_FUNCTIONS
must then be thread-safe).
lz4 : the automatic nb of workers no longer shrinks from one file to the next
(cRess_t.nbWorkers high-water mark) : worker threads and buffers are kept
(-m on 300 files of 64 KB - 8 MB : 1156 -> 21 thread creations).
lz4 : with independent blocks, a file fitting into a single job is compressed by the calling
thread (nbWorkers 0, same output). Not with linked blocks : single-threaded output then depends
on stableSrc (last partial block referencing history in place), so memory-mapped input would differ.
lz4frame : without LZ4F_MULTITHREAD, LZ4F_compressBound() covers a single job slot
(jobs processed serially) : 4 MB instead of 68 MB with linked blocks and nbWorkers 8.
tests : frameMT validateCompressBound ; test-lz4-multithread : single-job files and -m.
2026-10-22 12:58:19 -07:00
Yann Collet
88ed0b630e api: LZ4F_getMaxNbWorkers() reports multi-threading support ; CLI thread cap follows liblz4, lz4 -V reports multithreading (checkpoint)
LZ4F_getMaxNbWorkers() (static linking only, experimental like nbWorkers) :
0 when liblz4 is built without LZ4F_MULTITHREAD, otherwise the cap of nbWorkers,
now 1024 on 64-bit systems and 128 on 32-bit ones (was 200).
lz4 : build macro LZ4_NBWORKERS_MAX removed ; worker threads of frame and legacy
formats are capped by LZ4F_getMaxNbWorkers(), 1 without liblz4 multi-threading
(restores a cap for the legacy format on POSIX). lz4 -V reports "multithread"
only when both lz4 and liblz4 support it ; -T# warning and -v note accordingly.
VS2022 _test.bat checks the version line, catching a CLI built without LZ4F_MULTITHREAD
(same output, so no other observable difference).
frameMT : validateMaxNbWorkers ; test builds use LZ4F_NBWORKERS_MAX=128.
2026-10-22 12:58:19 -07:00
Yann Collet
d4a0782b47 review round 10: failed LZ4F_compressBegin() invalidates on allocation only, deferred emission of completed jobs, VS2022 x64 CLI multithreaded (checkpoint)
LZ4F_compressBegin*() validates its arguments first (dictSize included) :
rejecting them leaves a frame in progress untouched, jobs in flight included
(contract of #1817, tested by frametest). Past validation, the frame in progress
is abandoned before cctx is modified : an allocation failure invalidates it
(previously, cStage remained 1 with an inconsistent cctx, also with nbWorkers == 0).
lz4frame.h : LZ4F_srcStableUntilFlush lifetime and LZ4F_compressBegin() failures clarified.
Without autoFlush, LZ4F_compressUpdate() defers completed jobs not fitting into dst,
instead of failing : it only fails when output must be emitted (all job slots busy).
VS2022 : lz4.vcxproj x64 configurations define LZ4F_MULTITHREAD=1.
frameMT : validateDeferredEmission, validateBeginFailures.
2026-10-22 12:58:19 -07:00
Yann Collet
c6e5a8cf53 review round 9: destination too small invalidates MT frames (test), --rm keeps a renamed or replaced source, multithreaded CLI tests, MSan step (checkpoint) 2026-10-22 12:58:19 -07:00
Yann Collet
826f9bd6c8 cleanup: legacy helpers without frame leftovers, nbWorkers == 1 comments (checkpoint)
lz4io.c : the legacy compression pipeline no longer carries frame-only code
(ReadTracker.prefix and .xxh32, prefixSize parameter, CompressJobDesc.lastBlock).
xxhash.h remains included, for the content checksum of a memory-mapped source.
lz4frame.c, frameMT.c : nbWorkers == 1 means one worker thread, in addition to
the calling thread. Comments only, behavior unchanged.
2026-10-22 12:58:19 -07:00
Yann Collet
125fdf4baa NEWS: multithreaded liblz4 (checkpoint)
Prototype, not intended for a PR as is.

v1.10.1 entries for LZ4F_preferences_t.nbWorkers, LZ4F_srcStableUntilFlush, scalar reserved
fields, the CLI relying on liblz4 (memory-mapped input), -T# semantics, memory budget
(--memory, LZ4_MEMORY), detection of sources modified during compression, build policy
(lib-mt / lib-nomt, cmake and meson options) and make install.
The former "multithreaded compression honors custom block sizes" becomes : custom block
sizes are honored, and compressed single-threaded.
2026-10-22 12:58:19 -07:00
Yann Collet
bfd8019c02 memory: CLI memory budget (--memory, LZ4_MEMORY), no fixed nb of workers, single-threaded path without threads, no tmpBuff for MT frames, uncompressed segments as single-threaded (checkpoint)
Prototype, not intended for a PR as is.

- lz4 CLI : the automatic nb of workers (no -T, or -T0) is limited by a memory budget
  (4 GB, 1 GB on 32-bit systems), estimated from liblz4's job slots, input buffers and
  LZ4 states, plus the CLI's buffers (within ~5% of actual allocations when all are used).
  It's also limited to the nb of jobs of each file, when its size is known
  (ex: 3 threads for a 10 MB file, instead of 145 on a 166-core machine).
  -9 on 844 MB, 166 cores : unchanged auto (145 workers, ~3 GB budget estimate).
- --memory=# (suffixes K, M, G) and environment variable LZ4_MEMORY set the budget ;
  an explicitly set budget also limits an explicit -T#, as does the default one on 32-bit
  systems. A budget too small for 1 worker triggers a warning.
- lz4 CLI : no more fixed maximum nb of workers for frames (LZ4_NBWORKERS_MAX now only
  bounds the legacy format's thread pool) ; liblz4 keeps a sanity limit, raised to 1024
  (200 on 32-bit systems, where it keeps LZ4F_compressBound() within address space),
  overridable at compile time (tests : 200, keeping the capped test case affordable).
- liblz4 without LZ4F_MULTITHREAD (ex: default static library) : independent blocks take
  the single-threaded path (same output, no job buffers, no extra copies) ; linked blocks
  keep jobs processed serially (same output as multi-threaded, ~3% slower at level 1).
- liblz4 : frames cut into jobs no longer allocate tmpBuff (-4.1 MB, LZ4F_cctx_size()).
- liblz4 : LZ4F_uncompressedUpdate() with workers now cuts blocks as single-threaded mode :
  an incomplete last block waits for next uncompressed input (it was written immediately,
  producing different blocks for consecutive uncompressed invocations). With independent
  blocks, output is now identical to nbWorkers == 0 in all cases, and doesn't depend on
  multi-threading support. tests/frameMT.c compares uncompressed segments with
  single-threaded mode too. Full digest reference : 899dd7e3d21cad9a.
- Documented in man (-T#, --memory, LZ4_MEMORY, LZ4_NBWORKERS), programs/README.md, lz4frame.h.

Validated : full tests MT / no-MT (same digest), ASAN+UBSAN, TSAN, make test, wine,
CLI == dev CLI, lz4-nomt == lz4, memory budget scenarios, modification scenarios,
build policies, ~88k libFuzzer executions.
2026-10-22 04:26:50 -07:00
Yann Collet
6781c41631 lz4 CLI: memory-mapped source modified during compression is accepted when its content checksum still matches (checkpoint)
Prototype, not intended for a PR as is.

Since the previous checkpoint, any modification of a memory-mapped source (default mode)
was an error, so compressing a log file growing meanwhile, which used to work, failed.

A file only grown leaves its mapped part unchanged, and the frame is consistent.
When a modification is detected, the mapped content is hashed again (XXH32) and compared
with the frame's content checksum, computed while reading it :
- equal : the content read is unchanged (ex: log file growing meanwhile) : the output is
  valid, holding the file as it was when opened : warning only, source kept with --rm ;
- different, or can't be verified (--no-frame-crc, or file shrunk) : error 54, output removed.
Unmodified files cost nothing more.

Documented in man (--[no-]mmap), -H help and lz4io.h.

Validated : growing log (default mode : valid output), rewrite of content already read
(error 54), rewrite ahead of reading (consistent : accepted), --no-frame-crc (error 54),
--no-mmap (always valid), also with a CLI built with ASAN+UBSAN ; make test, CLI == dev CLI.
2026-10-22 04:26:50 -07:00
Yann Collet
b54cc29c78 MT robustness: source modified during compression detected, pool retried, queued jobs cancelled, longer mmap release lag (checkpoint)
Prototype, not intended for a PR as is.

- lz4 CLI : a source file modified during its compression is detected, comparing its size
  and times (mtime, ctime) before and after, like tar ("file changed as we read it").
  The source is kept, even with --rm.
  - memory-mapped input is read several times (checksum, compression, history of next job) :
    the frame can be inconsistent : error 54, output removed.
  - otherwise (--no-mmap), input is read once : the frame holds the content read,
    ex: up to the end of a log file growing meanwhile : warning only.
  Documented in man (--[no-]mmap, --rm), -H help and lz4io.h.
- lz4frame.c : a missing or incomplete worker pool (ex: transient allocation failure) is
  created again at next frame, instead of staying serial for the context's lifetime.
- lz4frame.c : abandoning a frame (error, LZ4F_compressBegin(), LZ4F_freeCompressionContext())
  cancels jobs not started yet, instead of compressing them (and the calling thread helping) :
  only running jobs are waited for. Level 12, 4 MB jobs : CPU spent abandoning a frame
  1.58 s -> 0.80 s (2 workers), 4.03 s -> 3.50 s (8 workers).
- lz4 CLI : memory-mapped input released behind (2*nbWorkers+1) jobs of 4 MB + 64 KB of history,
  instead of nbWorkers+2 chunks (fewer refaults : ~5-8% fewer minor faults at -9 -BD).
- tests/frameMT.c : validatePoolRetry (each absorbed allocation failure, then next frame
  creates worker threads again ; fails with the previous lib).

Validated : full tests MT / no-MT, ASAN+UBSAN, TSAN, make test, wine, CLI == dev CLI,
in-place rewrite / growing log / truncation scenarios, ~88k libFuzzer executions.
2026-10-22 04:26:50 -07:00
Yann Collet
869ed8083f build: shared pthread probe within a private directory, also for programs (checkpoint)
Prototype, not intended for a PR as is.

programs/Makefile probed pthread with a fixed have_pthread.c in its directory
(pre-existing in dev) : concurrent invocations collided (24 / 192 concurrent probes
wrongly reported no threads, producing an lz4 CLI without multi-threading),
and read-only source trees made it fail.

- build/make/lz4defs.make : PTHREAD_PROBE, evaluated with the CC and ALLFLAGS of
  the invoking Makefile, within a private temporary directory (mktemp -d).
- lib/Makefile and programs/Makefile employ it (lib/'s copy from previous commit removed).

Validated : probes of lib/ and programs/ (192 concurrent, read-only trees), full tests
MT / no-MT, ASAN+UBSAN, TSAN, make test, wine, CLI build (lz4 multithread, lz4-nomt not),
build policies, install scenarios + test_install.sh.
2026-10-22 04:26:50 -07:00
Yann Collet
96be2ab78d build: mingw _WIN32_WINNT, pthread probe in a private directory, install installs what was built (checkpoint)
Prototype, not intended for a PR as is.

- lz4frame.c (Win32) : _WIN32_WINNT raised to 0x0600 (condition variables) when lower,
  instead of only when undefined : <_mingw.h>, included by standard headers, defines it
  to 0x502 on older mingw-w64, where CONDITION_VARIABLE was then undeclared.
  A higher user-defined value is preserved.
- lib/Makefile : the pthread probe runs within a private temporary directory (mktemp -d),
  instead of a fixed have_pthread.c in the source tree : concurrent invocations no longer
  collide (35 / 320 concurrent probes wrongly reported no threads, producing liblz4.so
  without multi-threading), and read-only source trees are supported.
  The build message mentions "(threads not detected)" when the probe fails.
- make install, as zstd : installs the libraries already built (lib, lib-mt, lib-nomt),
  and only builds them, with current settings, when absent :
  `make lib-mt && make install` now installs the multi-threaded libraries.
  liblz4.pc declares -pthread (Libs.private) when the installed liblz4.a requires it,
  deduced from its symbols (nm) ; when they can't be listed, whenever threads are available.
  Root Makefile : install is ordered after lib goals of the same command line
  (make -j all install, make -j lib-mt install), instead of always rebuilding lib.
- lib/README.md documents it.

Validated : full tests MT / no-MT, ASAN+UBSAN, TSAN, make test, wine, build policies,
Windows DLL through lib/Makefile (imports CreateThread), mingw with _WIN32_WINNT=0x0502,
10 install scenarios + test_install.sh, concurrent and read-only probes.
2026-10-22 04:26:50 -07:00
Yann Collet
d9c9fe23a2 lz4frame: explicit reserved fields ; LZ4F_srcStableUntilFlush includes LZ4F_srcStable (checkpoint)
Prototype, not intended for a PR as is.

- LZ4F_preferences_t : reserved array replaced by explicit scalar fields
  (nbWorkers, reserved1, reserved0), and LZ4F_compressOptions_t likewise
  (stableSrc, reserved2, reserved1, reserved0), as LZ4F_decompressOptions_t.
  Same sizes and offsets (ABI unchanged). A future field takes the place of a
  reserved one, without changing positional initializers.
  One-time source change : the former positional form { ..., { 0, 0, 0 } }
  no longer compiles in C++ (warnings in C) ; examples/frameCompress.c updated.
  LZ4F_INIT_PREFERENCES : { LZ4F_INIT_FRAMEINFO, 0, 0u, 0u, 0u, 0u, 0u }.
- LZ4F_srcStableUntilFlush (2) is stronger than LZ4F_srcStable (1) : in addition
  to the last 64 KB of input (which remain, including after LZ4F_flush()),
  all of src remains until LZ4F_flush() / LZ4F_compressEnd() returns
  (with autoFlush : until the invocation returns). Docs only : the code already
  relied on this (history read in place after a flush, single-threaded path).
- tests/frameMT.c : validateFlushContract : one buffer, 2 invocations separated by
  a flush, everything but the last 64 KB scrambled after the flush with content
  resembling upcoming data ; nbWorkers 0 and 2, levels 1 and 9, with and without
  autoFlush. Scrambling the last 64 KB too is detected (contentChecksum_invalid).
  Streaming test : LZ4F_srcStableUntilFlush slices come from a working copy,
  scrambled the same way after each flush.

Output unchanged (same digests). Validated : full tests MT / no-MT, ASAN+UBSAN, TSAN,
make test, wine, examples (C, C++), cxxtest, CLI == dev CLI.
2026-10-22 04:26:50 -07:00
Yann Collet
84507cb3d6 MT: synchronous signals remain deliverable in workers ; CLI reports SIGBUS on memory-mapped input (checkpoint)
Prototype, not intended for a PR as is.

A memory-mapped file truncated during its compression, or unreadable (I/O error),
raises SIGBUS in the thread reading it. Worker threads were blocking all signals,
so a fault in a worker killed the process, bypassing any handler.

- lz4frame.c : worker threads still block signals intended for the application,
  but not synchronous ones (SIGBUS, SIGSEGV, SIGFPE, SIGILL) : the application's
  handlers are invoked on faults (documented in lz4frame.h, nbWorkers).
- lz4 CLI : while a file is memory-mapped, a SIGBUS handler reports
  "Error 53 : <file> : file truncated or unreadable during compression (memory-mapped) ;
  retry with --no-mmap", then terminates the process (async-signal-safe : write, _exit).
  The message is prepared beforehand ; when several threads fault simultaneously,
  only the first one reports. Previous handler restored once unmapped.
- -H help, man page (--[no-]mmap) and lz4io.h updated.

Truncation test (silesia x4 truncated during -9 -T4) : error 53 with one message,
including --no-frame-crc (workers read the mapping first) ; with the former signal
mask, the process was killed by SIGBUS. --no-mmap : unchanged.
Output and speed unchanged. Validated : full tests MT / no-MT, ASAN+UBSAN, TSAN,
make test, wine (frameMT, lz4.exe == Linux CLI), CLI == dev CLI.
2026-10-22 04:26:50 -07:00
Yann Collet
c283199226 lz4frame: LZ4F_stableSrc_t, MT jobs read input in place only with LZ4F_srcStableUntilFlush (checkpoint)
Prototype, not intended for a PR as is.

Historically, stableSrc only guarantees that the last 64 KB of input remain
present (linked blocks reference them instead of copying them). The MT engine
was reading whole jobs in place after LZ4F_compressUpdate() returns, silently
extending this contract : code correct with nbWorkers == 0 (buffer reused with
independent blocks, alternating buffers with linked blocks) became racy once
nbWorkers is enabled.

- lz4frame.h : enum LZ4F_stableSrc_t, the field remains unsigned (C++ compatibility) :
  - LZ4F_srcUnstable (0) : default ;
  - LZ4F_srcStable (1) : historical meaning, last 64 KB of input ;
  - LZ4F_srcStableUntilFlush (2) : all of src remains until LZ4F_flush() / LZ4F_compressEnd()
    returns, or the frame is abandoned : MT jobs read it in place.
- single-threaded behavior is unchanged : any non-zero value means stable.
- MT : only exactly LZ4F_srcStableUntilFlush enables in-place reading after return ;
  otherwise job content is copied. autoFlush still reads in place (jobs complete within the call).
- one-shot compression and the CLI (memory-mapped input) employ LZ4F_srcStableUntilFlush.
- tests/frameMT.c : the three values ; with LZ4F_srcStable and workers, input is scrambled
  after each invocation (a mutant reading it in place is detected, also by TSan).
- compress_frame_stream_fuzzer : stableSrc in 0..2.

Output unchanged (same digests), CLI mmap speed unchanged.
Validated : full tests MT / no-MT, ASAN+UBSAN, TSAN, make test, wine, CLI == dev CLI,
~88k libFuzzer executions.
2026-10-22 04:26:50 -07:00
Yann Collet
ebbdb4e807 MT hardening : allocation failure injection, lz4file test, fuzzers with nbWorkers, CI sanitizer steps (checkpoint)
Prototype, not intended for a PR as is.

- FORWARD_IF_ERROR() used to evaluate its argument twice ; applied to LZ4F_mt_begin(),
  an allocation failure re-ran it, and LZ4F_compressBegin() reported success without
  starting the frame. Found by allocation failure injection.
  Fixed in dev by #1877 (single evaluation) ; FORWARD_IF_ERROR_MT() now follows the same pattern.
- tests/frameMT.c :
  - each allocation fails in turn (one-shot, streaming copy / stableSrc, levels 1 and 9) :
    clean allocation_failed or correct output, no leak, context remains usable ;
  - LZ4F_write() / LZ4F_read() with workers : same frame as one-shot ;
  - quick mode (-q), step labels, FRAMEMT_DUMP diagnostics ;
  - CDict now created after its dictionary is filled (found by ASAN).
- make test runs test-frameMT-quick ; CI sanitizers : ASan and TSan steps.
- ossfuzz : compress_frame_stream_fuzzer employs different nbWorkers for reference and test,
  comparing whole frames when output lags behind input ; round_trip_frame_fuzzer : random nbWorkers.
- LZ4F_MT_JOBSIZE_* can be overridden (testing only), to cross job boundaries with small inputs.
- lz4frame.h : per-invocation output depends on scheduling (without autoFlush).

Validated : full tests MT / no-MT (same digest), ASAN+UBSAN, TSAN, make test,
mingw-w64 build under wine (same digest as Linux), ~160k libFuzzer executions.
2026-10-22 04:26:50 -07:00
Yann Collet
bb777cdb47 lz4 CLI: memory-mapped input for frame compression, --[no-]mmap ; document -T# semantics (checkpoint)
Prototype, not intended for a PR as is.

- regular files are memory-mapped (POSIX 64-bit, frames cut into jobs only) and given to
  LZ4F_compressUpdate() with stableSrc : compression jobs read input in place, removing
  two copies from the main thread (fread, then copy into job buffers).
- on Linux, input far behind jobs in flight is released (madvise MADV_DONTNEED) :
  only a memory hint, released pages are read again from the file if still needed.
- --no-mmap disables it : a file truncated while being compressed makes lz4 crash (SIGBUS).
- output is identical with and without mmap.
- -T# : # worker threads ; the main thread also compresses while waiting (up to #+1 cores).
- custom block sizes (-B# other than 64KB / 256KB / 1MB / 4MB) remain single-threaded :
  documented in -H help, man page (-T#, -B#), and a note with -v.

Level 1, -T16, silesia.tar : 82 ms (previous CLI 118 ms, --no-mmap 162 ms).
2026-10-22 04:26:50 -07:00
Yann Collet
ae0012969a lz4frame: reduce multi-threading memory (checkpoint)
Prototype, not intended for a PR as is.

- input buffers form a pool (nbWorkers+1), shared by job slots :
  a buffer is reclaimed as soon as its job is compressed, instead of when its output is emitted;
  when none is free, the calling thread waits for any job to complete (job durations vary).
- job slots : 2*nbWorkers+1 (was +2), which also shrinks LZ4F_compressBound().
- stableSrc : when an input directly follows the previous one, history is read in place,
  so jobs starting at an invocation boundary aren't copied anymore.
- the calling thread runs queued jobs while waiting only if enough jobs remain queued
  to keep worker threads busy (it can't feed them while compressing), or once input is over.

CLI max RSS, level 1 : -T1 44 -> 29 MB, -T8 129 -> 80 MB, -T64 280 -> 107 MB (previous CLI : 13 / 55 / 212 MB);
HC speed back to previous CLI's level (-9 -T16 : 583 ms vs 582 ms).
2026-10-22 04:26:50 -07:00
Yann Collet
0852fddadc lz4frame: streaming multi-threaded compression, persistent pool, CLI uses it (checkpoint)
Prototype, not intended for a PR as is.

- persistent thread pool within LZ4F_cctx (pthread / Win32), replaces the per-call fork-join;
  nbWorkers is now the nb of worker threads (the calling thread also runs jobs while waiting).
- streaming : LZ4F_compressUpdate() / LZ4F_flush() / LZ4F_compressEnd() cut input into jobs
  (4 MB linked, 1 MB independent), at fixed positions of the content; outputs emitted in order;
  input read in place with stableSrc or autoFlush, copied otherwise; autoFlush respected;
  LZ4F_compressBound() covers jobs in flight; LZ4F_uncompressedUpdate() drains pending jobs.
- one-shot compression goes through the same engine.
- CLI : frame compression goes through LZ4F streaming with nbWorkers (output identical to
  previous CLI, and lz4 == lz4-nomt); custom block sizes fall back to single-thread.
- build systems compile the CLI's liblz4 sources with LZ4F_MULTITHREAD.
- tests/frameMT.c : streaming validation (slicing, stableSrc, flushes, autoFlush,
  uncompressed segments, exact bounds, abandoned frames).
2026-10-22 04:26:50 -07:00
Yann Collet
02916432ee lz4frame: experimental multi-threaded one-shot compression (checkpoint)
Prototype, not intended for a PR as is.

- LZ4F_preferences_t.nbWorkers (replaces reserved[0], ABI preserved):
  LZ4F_compressFrame() and LZ4F_compressFrame_usingCDict() cut input into jobs,
  compressed by up to nbWorkers threads (inline fork-join, threads created per call).
  Output only depends on parameters, never on nbWorkers (>= 1) nor scheduling;
  identical to single-threaded with independent blocks, identical to the CLI -T output.
- LZ4F_MULTITHREAD build macro (default 0) : without it, jobs run serially, same output.
- build policy (zstd-like) : dynamic library multi-threaded by default, static library not
  (make lib-mt / lib-nomt / MULTITHREAD=, cmake LZ4_SHARED/STATIC_MULTITHREAD,
  meson shared-/static-multithread, VS2022 liblz4-dll).
- internal LZ4F_compressBlocks_internal() extracted from LZ4F_compressUpdateImpl().
- tests/frameMT.c (make -C tests test-frameMT) : validation + bench mode.

Known gaps : Win32 thread path never compiled; no persistent thread pool; no streaming.
2026-10-22 04:26:50 -07:00
Yann Collet
e5ab519f10 Merge pull request #1925 from lz4/shortcut_context_candidate
fast levels : first test the position following the previous occurrence of ip-1
2026-10-21 21:12:21 -07:00
Yann Collet
4b45bb5e32 fast levels : first test the position following the previous occurrence of ip-1
After each match, the fast parser only tested the most recent occurrence
of the sequence at ip. On periodic data with long runs, it is a short
match, and such matches can chain until the end of the input :
an 8-byte dictionary made a 4 KB page compress to 1543 bytes instead
of 49 (#1805). It also happens without dictionary.

The position following the previous occurrence of ip-1 is now tested
first, also within the dictionary, and taken if it matches 8+ bytes.
The table is filled with ip-1 instead of ip-2.
Both candidates share the same dictionary / current-block selection.

Silesia, compressed size : -0.6% (256 KB blocks), -0.3% (4 KB blocks),
-0.5% (4 KB pages with a 64 KB dictionary).
Speed, AMD EPYC Genoa (Zen 4), x86-64, gcc 11 / gcc 15 / clang 22 :
-0.8 to -1.2% (256 KB blocks), -1.7 to +1.5% (4 KB blocks),
-6 to +1% (4 KB pages with a dictionary).
x86 (gcc 12 -m32) : +1% (4 KB blocks) to -11% (dictionary).

Closes #1805
2026-10-21 20:54:37 -07:00
Yann Collet
bb93da808f Merge pull request #1924 from lz4/dictctx_copy_threshold
Copy the tables of an attached dictionary from 2 KB inputs
2026-10-21 16:06:06 -07:00
Yann Collet
082944fa53 Merge pull request #1922 from lz4/cli_main_helpers
cli: main() : clearer check of the .lz4 extension for automatic output names
2026-10-21 15:59:56 -07:00
Yann Collet
da7b7ebdda copy the tables of an attached dictionary from 2 KB inputs
LZ4_compress_fast_continue() copied the tables of an attached dictionary
(one table to look into) only above 4 KB, and used the dictCtx loop
(two tables) below. The copy is now faster from 1/8 of the table size:
from 2 KB with the default table, capped at 4 KB for large tables.
The compressed output is unchanged.

AMD EPYC Genoa (Zen 4), x86-64, 2-4 KB inputs:
gcc 11 +3-5%, gcc 15 +5-8%, clang 22 +16-31%.
2026-10-21 15:30:40 -07:00
Yann Collet
6739cc9d10 cli: main() : clearer check of the .lz4 extension for automatic output names
The automatic output name of decompression removed the .lz4 extension
with an obscure loop, comparing the last 5 characters (terminating zero
included) while zeroing them : it's now a plain suffix check.
2026-10-21 15:16:34 -07:00
Yann Collet
6a291524be Merge pull request #1921 from lz4/lz4f_compress_dedup
lz4frame: share state initialization, LZ4F_makeBlock() takes the context
2026-10-21 14:55:33 -07:00
Yann Collet
ab5f917365 lz4frame: share state initialization, LZ4F_makeBlock() takes the context
LZ4F_compressBegin_internal() initialized the fast or HC state in 2
places, after an allocation and when changing its type : they now share
LZ4F_initLZ4State(). This also fixes a crash : when the allocation of a
larger state failed, the previous state was freed but still recorded as
allocated, and a following LZ4F_compressBegin() for its type used a NULL
state. frametest checks this case, with a failing custom allocator.

LZ4F_makeBlock() takes the compression context, instead of the same
5 of its fields at each of its 4 calls.
2026-10-21 14:22:18 -07:00
Yann Collet
1c51283d11 Merge pull request #1920 from lz4/cli_multiple_names
cli: share file name building of multiple-files modes
2026-10-21 13:40:48 -07:00
Yann Collet
317389aaff cli: share file name building of multiple-files modes
Frame and legacy compression of multiple files each built the name of
the compressed file, with the same buffer management : initial
allocation, growth, strcpy + strcat of the suffix, and stdout special
case. They now share LZ4IO_compressedFileName(), and decompression of
multiple files shares its buffer management, LZ4IO_reserveFileName().
2026-10-21 12:44:20 -07:00
Yann Collet
9e8bc239b8 Merge pull request #1918 from lz4/cli_legacy_rm
cli: legacy format honors --rm and keeps file attributes
2026-10-21 10:54:36 -07:00
Yann Collet
f11eeb613c cli: legacy format honors --rm and keeps file attributes
Single and multi-threaded frame compression duplicated the opening of
files, the preparation of preferences, and the end of file processing :
closing files, copying owner, permissions and modification time, --rm,
final status. Legacy compression had none of this end : `lz4 -l` ignored
`--rm`, and the compressed file didn't keep the source's attributes.

The end of file processing is now LZ4IO_finishCompressedFile(), shared by
frame and legacy compression, and LZ4IO_compressFilename_extRess() opens
files and prepares preferences for both frame compressors, which only
compress.

Also, LZ4IO_compressFilename_extRess() now reports a processed size of 0
when a file can't be opened, instead of leaving it uninitialized : with
-m, a missing file added a stale or garbage size to the final speed
summary (found by scan-build).
2026-10-21 10:30:59 -07:00
Yann Collet
dcd73e8e4d Merge pull request #1915 from akashdeshmukh-git/cli-reject-malformed-long-args
cli: reject malformed --fast= and --threads arguments
2026-10-20 15:49:17 -07:00
Yann Collet
911d1ae36b Merge pull request #1914 from akashdeshmukh-git/fix-truncated-skippable-frame
lz4io: detect truncated skippable frames in regular files
2026-10-20 11:39:44 -07:00
Yann Collet
2e12728a18 Merge pull request #1912 from lz4/wildcopy64_override
lz4: LZ4_WILDCOPY64 can be set from the build command
2026-10-20 09:55:21 -07:00
Yann Collet
5542d86ef3 Merge branch 'dev' into wildcopy64_override 2026-10-20 08:26:08 -07:00
Yann Collet
1deef0b70c Merge pull request #1913 from lz4/xxh_bad_function_cast
xxhash: no -Wbad-function-cast warning
2026-10-20 08:21:58 -07:00
akashdeshmukh-git
cebeba6e3e cli: reject malformed --fast= and --threads arguments
`--fast=5x` was accepted as `--fast=5`: characters following the
number were silently ignored, unlike `--threads=5x` which is rejected.

`--threadsX 2` was accepted as `--threads 2`, since the long command
was matched as a prefix only.

Both are now rejected with the usage message.
2026-10-20 08:32:54 +00:00
akashdeshmukh-git
26f121584a lz4io: detect truncated skippable frames in regular files
fseek() succeeds beyond end of file, so a skippable frame whose
declared size exceeds the remaining input was silently accepted
when reading from a file (`lz4 -t`, `lz4 -d` and `lz4 --list` all
reported success), while the same input from a pipe correctly failed
with "Error 43 : cannot skip skippable area".

fseek_u32() now seeks to the last byte of the area and reads it,
so truncation is detected the same way for files and pipes.
2026-10-20 08:32:54 +00:00
Yann Collet
060275ab9f Merge pull request #1911 from lz4/lz4f_decompress_gather
lz4frame: LZ4F_decompress() refactor
2026-10-20 00:01:04 -07:00
Yann Collet
8b8c415d79 xxhash: no -Wbad-function-cast warning
XXH_isLittleEndian() returned an int,
and each caller cast this function call to XXH_endianness,
which triggers -Wbad-function-cast (6 warnings, gcc and clang).

It now returns an XXH_endianness.
The casts at call sites are kept:
they still support an XXH_CPU_LITTLE_ENDIAN defined externally as an int,
including in C++ builds, where int to enum requires a cast.
Generated code is unchanged (-O3).

-Wbad-function-cast is added to the lib/Makefile warning flags,
so that a regression is caught by existing builds.

Reported, with a first fix, by @brainy in #1711.
2026-10-19 23:50:05 -07:00
Yann Collet
af4b7386f2 lz4: LZ4_WILDCOPY64 can be set from the build command
Like LZ4_FAST_DEC_LOOP, LZ4_WILDCOPY64 can now be enabled or disabled
at compile time (-DLZ4_WILDCOPY64=0 or 1). Defaults are unchanged.

#1804 proposed enabling both on wasm, measuring +21% (default) and
+25% (HC9) decompression speed on Apple M5 (Chromium 153, wasm32).
But a wasm module runs on any host, and on x86-64 the same change slows
decompression down (AMD Zen 4, Node 24 / V8 13.6, wasm32, Silesia) :
-5.8% / -7.8% on #1804's baseline, -1.5% / -3% on current dev,
with text files losing 10-20% and poorly compressible ones gaining 20-40%.
So it's not enabled by default : a wasm build can opt in with
-DLZ4_FAST_DEC_LOOP=1 -DLZ4_WILDCOPY64=1, which produces the same module
as #1804 applied on dev.

Based on #1804 by @JCash.
2026-10-19 23:31:08 -07:00
Yann Collet
3e337913ce Merge pull request #1910 from lz4/compress_end_limits
lz4: set the compressor's end limits after the input size check
2026-10-19 23:26:13 -07:00
Yann Collet
79d09aa0d9 lz4frame: LZ4F_decompress(): gather input with LZ4F_gatherInput()
Block header, compressed block, block checksum, frame checksum and
skippable frame size each gathered their input bytes their own way,
4 of them with a "get" stage reading directly from src and a "store"
stage buffering it, joined by case labels nested within if statements.
LZ4F_gatherInput() now provides these bytes, directly from src when
possible, otherwise buffered over as many calls as needed: each pair
becomes a single stage, 11 stages instead of 15.

The frame header keeps its 2 stages: LZ4F_getFrameInfo() relies on
dstage_storeFrameHeader, and LZ4F_decodeHeader() may request more bytes.

No behavior change.
2026-10-19 23:02:10 -07:00
Yann Collet
aecc724040 lz4: set the compressor's end limits after the input size check
`mflimitPlusOne` and `matchlimit` pointed before the input buffer
for inputs shorter than `LZ4_minLength`, which is undefined behavior.
They are only used once the input is long enough.
2026-10-19 22:49:18 -07:00
Yann Collet
29f736debc Merge pull request #1909 from lz4/lz4f_decompress_dedup
lz4frame: LZ4F_decompress(): factor block decoding and history move
2026-10-19 22:46:25 -07:00
Yann Collet
f20d44534e Merge pull request #1908 from lz4/cxx_modules_headers
headers: include dependencies outside of extern "C"
2026-10-19 22:46:08 -07:00
Yann Collet
aca5592874 lz4frame: LZ4F_decompress(): factor block decoding and history move
The 2 compressed block decoding paths (directly into dst, or into tmpOut)
duplicated the dictionary size clamp, LZ4_decompress_safe_usingDict(), its
error check, and the content checksum and remaining size updates: they now
share LZ4F_decodeBlock(). The history move in front of tmpOut, written both
in LZ4F_updateDict() and at the end of LZ4F_decompress(), becomes
LZ4F_moveDictBeforeTmpOut().

Also fix a wrong comment and a stray unary plus.

No behavior change.
2026-10-19 21:46:21 -07:00