Efficient Large-Scale Text Precompression via Approximate LZ77 Parsings
Abstract
The LZ77 [Lempel and Ziv, 1977] compression scheme is ubiquitous: it lies at the core of everyday general-purpose standard compressors such as gzip or zstd, but also behind the scenes of many applications such as the compression of payloads transmitted in networks.
Computing the exact LZ77 parsing is largely solved in theory: it can be done in sublinear time and space, in compressed space and in external memory, to name but some scenarios. However, these approaches are often impractical for everyday use due to their intensive time or space requirements. Standard compressors tackle this issue by introducing heuristics that go hand in hand with sophisticated encoding schemes to achieve very good compression fast and in small space, however, they only have a local view (e.g., a sliding window) on the input, potentially missing out on long-range repetitions that may be located far apart from one another.
In this work, we design and implement – in C++ and leveraging shared-memory parallelism – compression pipelines that first precompress the input using an approximate LZ77 parsing taking care of long-range repetitions. This then serves as an assist to standard compressors for producing a succinct encoding of the remaining short and local repetitions. Similar approaches have been considered by [Kosolobov et al., 2020] and [Nalbach, 2024], respectively using Relative Lempel Ziv [Kuruppu et al. 2010] or the string synchronizing set [Kempa & Kociumaka, 2019].
We fill a gap taking the route via the prefix-free parsing [Boucher et al., 2019], using an intermediate result of [Hong et al., 2023]. On large repetitive inputs of tens of gigabytes, our pipelines are orders of magnitudes faster than the state of the art for computing the exact LZ77 parsing, use space less than the input size and still – despite producing more phrases – achieve the best overall compression in comparison to related work.
Keywords and phrases:
compression, algorithm engineering, parallel computationFunding:
Patrick Dinklage: Funded by the Deutsche Forschungsgemeinschaft (DFG) under the Research Grants programme (project No. 501086801).2012 ACM Subject Classification:
Theory of computation Data compression ; Theory of computation Pattern matching ; Theory of computation Shared memory algorithmsSupplementary Material:
Software (Source Code): https://github.com/pdinklag/alzarchived at
swh:1:dir:3c8c4ad7ec6e2415cfac8ae0eae41aa3cf5cf4f1
Editors:
Martin Aumüller and Irene FinocchiSeries and Publisher:
Leibniz International Proceedings in Informatics, Schloss Dagstuhl – Leibniz-Zentrum für Informatik
1 Introduction
Lempel-Ziv 77 (LZ77) [46] is a fundamental compression scheme that lies at the core of countless compression utilities and is thus ubiquitous in modern computer systems, be it in popular tools such as gzip or the more modern zstd, or behind the scenes in the compression of payloads transmitted from web servers to clients (e.g., Brotli or lz4). However, out of practical requirements and limitations, theory and practice differ greatly.
In theory, the LZ77 parsing can be computed in time and space sublinear in the input size [13, 29] (assuming that multiple characters can be packed into a computer word). When compressing very large files, however, it may be prohibitive to construct the required data structures indexing the entire input. Policriti and Prezza [43] gave an algorithm to compute LZ77 in compressed space , where denotes the number of runs in the Burrows-Wheeler transform [7] of , a well-established measure of compressibility. However, this requires superlinear time. A way to greatly reduce memory usage is doing compression in external memory as proposed by Kärkkäinen et al. [26]. While this, indeed, enables very large-scale compression even on low-memory systems, the detours required to optimize the number of I/O operations result in ultimately impractical running times.
From everyday compression utilities, users expect fast processing even of (reasonably) large inputs, and at the same time a compressed output that is small as possible. Standard compressors, like those named earlier, implement heuristics that yield a reasonable trade-off between those three criteria: running time, memory usage and compression ratio. Apart from sophisticated encoding schemes, to the best of our knowledge, all of them share one common idea that ages back to the original work of Lempel and Ziv themselves: instead of processing the input as a whole, they only consider a window of some controllable and manageable size. While this enables respecting any memory limitation (only information about the window needs to be stored) and improves speed (only the window is considered, independent windows can be processed in parallel), this comes at the cost of being unable to find long-range repetitions that are located possibly far apart from one another in the input.
This constitutes the motivation behind our work: is there a way of getting a global view of the input without fully indexing or even loading it, and fast? Dinklage et al. [12] considered this scenario and found a solution based on heavy hitters, giving an algorithm to maintain – approximately and in online fashion – the most frequent patterns that occur in the (entire) input while streaming and compressing it for some . This is combined with the classic approach of blockwise compression of the input in an attempt to get the best out of both worlds. A limitation of their approach, however, is that this favors short repetitions.
In this work, we conceptually separate the problem. We consider the handling of short and local repetitions solved and focus instead on finding long-range repetitions as a means of precompression. The key to our approach is an intermediate result of Hong et al. [22], who compute the LZ77 parsing using a prefix-free parsing (PFP) [6] of the input. If is repetitive, then the PFP is much smaller than the input, allowing them to compute the parsing even for very large inputs. For general-purpose compression, however, this is impractically slow.
We ask the question: is it necessary to compute the exact LZ77 parsing to achieve competitive compression? To answer this, one could look at approximations, and several approximations of LZ77 have been proposed in the literature: Fischer et al. [15] give algorithms to compute a - and -approximation, respectively, Bille et al. [4] give an -approximation of rightmost LZ77 and the LZ-End scheme due to Kreft and Navarro [33] has been shown to approximate LZ77 [23, 30, 16].
Here, as previously stated, we do not care so much about approximating LZ77 with guarantees, but are more interested in practically efficient precompression. Two recent works also fit this description and are similar to ours. Nalbach [38] implemented a variant of Ellert’s 3-approximation of LZ77 [13] based on the string synchronizing set [28]. However, their approach requires the input to be loaded fully into memory. Kosolobov et al. propose ReLZ [32], using Relative Lempel-Ziv [34] to the LZ77 parsing. To address potential memory limitations, their algorithm can run several recursive rounds of precompression in external memory to then compute an approximate LZ77 parsing in main memory.
Our Contributions
We design and implement – in C++ and using shared-memory parallelism – a pipeline that computes an approximation of LZ77 based on the PFP. We achieve competitive running times and, based on the repetitiveness of the input, use space possibly smaller than the input itself. For a succinct encoding, we then pass the (deliberately naïvely encoded) output to a standard compressor to form a symbiosis: precompression takes care of finding long-range repetitions in the input – a discipline at which standard compressors typically fail due to having only a local view on the input. They are then, however, used for what they excel at: capturing and succinctly encoding remaining local repetitions.
2 Preliminaries
We argue about running times in the word RAM model of computation [19], where we model the memory as a sequence of words of size bits each, where is the size of the problem at hand. Arithmetic and similar operations on words can be done in constant time. By default, we give logarithms to the base of two.
Let be a string of length over an integer alphabet with . For some , we denote by the -th character of . Given additionally some , we denote by the substring , where juxtaposition of strings means their concatenation. For clarity, we sometimes use the explicit operator to concatenate strings or write to denote the concatenation of strings .
VByte Code
Let be a natural number that we want to store in -bit words. If , we can naturally store in exactly one word. For larger , we can use the following variable-length code to store in words. Let denote the binary representation of . We partition into blocks of size . In case does not divide , we pad the final block with 0-bits. For every block from least to highest significance, we now store a word that contains the bits of the respective block, as well as an extra bit that indicates whether the block is the last block. As an example, we can encode the number with using -bit words as (indicator bits underlined).
This code is commonly referred to as the VByte code and used in contexts where larger numbers are to be encoded using a variable number of bytes (8-bit words).
Rabin-Karp Fingerprinting
Let be an integer (the base) chosen uniformly at random and a prime. We call
the fingerprint of [27]. Let be a window size and let denote the length- substring (window) of starting at some position . It is well known that from and with access on , we can compute in constant time. This is commonly referred to as rolling hashing.
Prefix-Free Parsing
The prefix-free parsing (PFP) due to Boucher et al. [6] of is a parsing of into overlapping metacharacters as follows. Let be an integer sampling parameter. We call a window a trigger string if or either or . Let denote the starting positions of all trigger strings of . The prefix-free parsing is the sequence of metacharacters with for every . Intuitively, a metacharacter begins and ends with a trigger string. This gives us the eponymous property that the set of distinct metacharacters is prefix-free. In the PFP, two neighbouring metacharacters share one common trigger string (the suffix of one is the prefix of the next). Figure 1 shows an example.
Fingerprinting using a uniformly random base gives us in expectation. We define the dictionary as the concatenation of the distinct metacharacters. can be represented conceptually as pointers into . We can thus store and in bits of space in expectation. If is repetitive and is aptly chosen, then this can be much less than storing in its uncompressed form (e.g., bits).
Lempel-Ziv 77 Parsing
The Lempel-Ziv 77 (LZ77) parsing [46] of is a factorization of into phrases such that . The -th phrase is either the first occurrence of some character in , or it is the longest possible prefix of that occurs in . If is a single character, we call it a literal phrase, otherwise we call it a copy phrase, because we can encode it as an instruction for the decoder to copy its contents from an already decoded occurrence.
We use the representation introduced by Storer and Szymanski [45]: if is a literal phrase, we represent it simply by the character . If is a copy phrase, we represent it as the tuple , where is the source position (the starting position of the previous occurrence in that we refer to) and is the length of the phrase.
3 Approximating LZ77 via PFP
In this section, we give a brief overview of our LZ77 approximation before switching to a much more practical perspective in the following Section 4.
Given the input string , we first compute the PFP parsing of . Let be a function assigning to each metacharacter its lexicographic rank (in ascending order) among all metacharacters in , which we can obtain by sorting. Then, let be the PFP represented using these ranks. We now compute the suffix array of and its inverse and use these to compute the LZ77 parsing of , all in time and space . Thanks to the preliminary lexicographic sorting of metacharacters, the suffix array of is a sparse suffix array of .
| Metacharacter | lex. rank |
|---|---|
| 2 | |
| 5 | |
| 4 | |
| 3 | |
| 1 |
As a consequence, copy phrases in the LZ77 parsing of correspond to valid copy phrases in . This is where we diverge from the work of Hong et al. [22], who proceed to compute the exact LZ77 parsing of . We, instead, (1) translate copy phrases of to the corresponding copy phrases in and then (2) extend these to the left and right as far as possible. This is visualized in Figure 2. It is important to see that we do not need random access to in order to do the left and right extensions; access to and suffices. However, it is as important to note that we rely on the fact that these fit into memory, and thus implicitly on the fact that is repetitive. We visualize our compression algorithm in Figure 2.
4 Implementation and Engineering
We break down the process described in the previous section into the following steps:
-
1.
Compute a preliminary parsing by identifying all trigger strings and computing the fingerprints of the resulting metacharacters.
-
2.
Find the set of distinct metacharacters and compute the actual PFP.
-
3.
Load the dictionary into RAM and sort the metacharacters lexicographically.
-
4.
Factorize the input via the LZ77 parsing of the PFP.
-
5.
Encode the approximate LZ77 parsing.
We assume that random access to is forbidden, which may be a requirement when processing a very large file. This makes the explicit step to load the dictionary (step 3) necessary. In section 4.6, we address potential advantages in the case that the whole input fits into RAM. In the following, we elaborate on each of the steps and give insight into our engineering. We use to denote the number of threads.
4.1 Blockwise and Parallel Preliminary Parsing
We process blockwise using block size such that a block fits into RAM. Each block is further partitioned into workloads, i.e., one per thread.
Every thread scans its workload using a sliding window of length to identify trigger strings. To enable fast modulus operations for computing fingerprints and for identifying trigger strings, we (1) require , the prime for computing Karp-Rabin fingerprints, be a Mersenne prime (one less than a power of two) and (2) require , the sampling parameter for trigger string detection, be a power of two. For every input character, we update two fingerprints. We maintain a rolling trigger fingerprint for the sliding window to identify a trigger string. We chose to represent these in 32 bits, as these are very fast to compute and fingerprint collisions can safely be ignored for this use case. For this, we choose the prime , the largest Mersenne prime to fit into a 32-bit word.
Furthermore, we maintain a metacharacter fingerprint that represents a metacharacter – from the beginning of the last trigger string until the end of the next – in later steps. We do care about collisions here: Collisions of metacharacter fingerprints will cause different metacharacters to be treated as being equal, ultimately leading to an output that cannot be decoded back to the original input (our implementation is Monte Carlo and produces correct results only w.h.p.). Thus, we represent these as 64-bit integers using the Mersenne prime , which is the largest to fit into 64 bits and large enough to lower the chance of collisions to the point where we never observed any in practice111If the fingerprint base is a power of two (e.g., 256, a common choice for byte alphabets), then modular arithmetics with the Mersenne prime (a sequence of 61 set bits, the most significant byte being 31), in combination with the ASCII encoding of capital versus small letters (e.g. ), cause a clustering of fingerprint collisions for ASCII-encoded text. This may seem surprising, but follows directly from the definitions. As an example, consider the strings The␣Work and the␣Worl, which have the same fingerprint with base 256. We therefore enforce the base is not a power of two. .
Special care needs to be taken at block boundaries as well as workload boundaries. Simply cutting off metacharacters at these boundaries would be harmful to our goal of compressing , thus each thread may scan beyond its own workload until it finds the next trigger string ( extra steps in expectation). At a block boundary, the last thread leaves information (starting position and fingerprint of the current metacharacter’s prefix up to the boundary) for the first workload of the next block to smoothly detect metacharacters across blocks.
The result of this step is a preliminary parsing that consists of sequences of metacharacters defined by their starting positions, lengths (including the trigger strings at the borders) and metacharacter fingerprints. Because the threads do not communicate, there has been no deduplication regarding equal metacharacters yet. We address this next.
4.2 Finding Distinct Metacharacters and Computing the Parsing
The next step is finding the set of distinct metacharacters. At the same time, we want to produce the parsing as a sequence of pointers into , preferably represented as integers from . To solve this in parallel, we looked at a similar problem from a preliminary step of constructing wavelet trees, which we can formalize as follows.
Given a sequence of some length , we want to
-
1.
identify the smallest set such that (effective alphabet),
-
2.
precompute a bijective function (effective mapping) and then
-
3.
produce the sequence (effective transform).
To make use of multiple threads, we borrow the solution from [10, Section 7] for distributed memory. We first partition the metacharacter sequences from the previous step in parts and independently compute hash tables representing the sets of distinct metacharacters for every part (in the hash tables, we map metacharacter fingerprints to their position of occurrence and length). We then merge the hash tables in a parallel all-reduce operation. Conceptually, this can be done using a merge tree of height [44]; in our implementation, we use the reduction framework of OpenMP [42] When merging two hash tables, we tie-break metacharacters with the same fingerprints by preferring the leftmost occurrence. The bijection is computed in sequential, but that can already be done in time . The parsing is then the effective transform, which can straightforwardly be computed in parallel by applying to every metacharacter.
4.3 Loading and Sorting the Dictionary
We now load the dictionary from the input file which stores . The size can be computed as a byproduct of the previous step so that we can accurately pre-allocate memory. Since we want to make no assumption about the storage (e.g., hard disk, SSD, NVMe), we load sequentially. First, we sort the metacharacters of by their starting position in in parallel (using std::sort and the parallel execution policy), then we load them in this order to ensure sequential reads of the input file.
With , we proceed to sort the metacharacters lexicographically in parallel, again using the C++ STL. (We are aware of sophisticated parallel string sorters (e.g., [5]), but decided not to put to use an external implementation for a task that accounts only for a tiny fraction of the total running time.) With this, we have effectively computed function lexrank from Section 3 and can compute from in parallel by partitioning into equisized workloads.
4.4 Factorizing the Parsing
Before we can finally compute the approximate LZ77 parsing of , we need to compute the LZ77 parsing of . We first construct its suffix array using libsais [18], a highly engineered implementation of the SA-IS [39] algorithm that features integer alphabets (which we have in ). We do use libsais’ parallel implementation for OpenMP, but the author claims that it is essentially memory-bound and no noteworthy speedups are to be expected. (We are aware of Bertram et al.’s modification of GSACA [1], which features good parallel speedup [2]. In our experiments, however, it was still clearly outperformed by libsais). From the suffix array, we can compute the inverse in parallel.
We now partition into workloads. Every thread processes its workload using an algorithm similar to KKP [25] and produces a sequence of potential copy phrases for . The differences to KKP are twofold: first, we work only using the suffix array and its inverse, which allows us to operate in parallel. We do not precompute next/previous smaller values or the LCP array, but compute the necessary values on the fly by scanning the suffix array or the parsing, respectively. Second, we inline the conversion of copy phrases as follows.
Assume that at some position we want to output a copy phrase with . Note that , and pertain to the parsing , i.e., metacharacters. We can translate and to their corresponding positions and in using a mapping computed (and stored alongside ) as a byproduct of computing the parsing (Section 4.2). Furthermore, let , which we can compute directly from the parsing. By the fact that the suffix array of is a sparse suffix array of , the copy phrase indeed corresponds to a repeated occurrence of at position in .
Even though the set of metacharacters is prefix-free, it can occur that the prefix of one metacharacter is the suffix of another or vice versa even beyond the the trigger string that they may have in common. If this occurs at the beginning or the end of the copy phrase, we can greedily expand the copy phrase accordingly on the character level (as shown in the bottom part of Figure 2). The necessary accesses to can be simulated via and if we store a mapping from lexicographic rank to the corresponding entry in , which we can compute as a byproduct when sorting the dictionary (Section 4.3).
It is important to note that around workload boundaries, because threads do not communicate, we may introduce multiple copy phrases replacing the same position. This will be handled in the following step.
4.5 Encoding
It remains to encode the copy phrases produced in the previous step. Again, we make no assumption about the storage (which may be a pipe) and thus do this in sequential. Because the workloads were assigned left to right and each thread processed its workload left to right, it holds that the copy phrases are in input order. We can thus simultaneously stream the input and the copy phrases and greedily encode a copy phrase whenever possible, or otherwise copy literal characters directly from the input. Overlapping copy phrases around workload boundaries in the previous step are truncated accordingly in the process. If more than one copy phrase is available starting at the current position, we greedily take one that maximizes the copy length (as shown in Figure 2).
We implement only a straightforward encoding where we keep uncompressed characters byte-aligned and encode copy phrases using VByte codes (a VByte code for the copy length followed by a VByte code for the copy source, given as the distance from the phrase’s starting position). The intention is that a downstream standard compressor (such as gzip) takes care of a succinct encoding. We consider two different ways of encoding strings of uncompressed characters. The straightforward approach is to collapse them: we first encode the number of uncompressed characters to be decoded, followed by the string. This works best for LZ-based encoders (e.g., gzip and zstd). Interestingly, for encoders based on block sorting (e.g., bzip2 and bsc), we achieved better overall compression by preceding every uncompressed character by a zero byte individually, generating higher redundancy.
4.6 Taking Advantage of Text Access
If the input is sufficiently small to be loaded in RAM, we can allow random access on . In this case, there is no need to process blockwise in the first step (Section 4.1). Furthermore, we can skip loading the dictionary for sorting the metacharacters (Section 4.3), as we can instead use pointers into . Lastly, because we no longer need to simulate accesses via and , but simplify left and right extension when computing copy phrases (Section 4.4) accessing directly, potentially saving a number of cache misses.
5 Evaluation
We evaluate our implementation in several experiments that we present in the following sections after describing our setup.
5.1 Experimental Setup
Our experiments are conducted on a Ubuntu 24.04 system with two AMD EPYC 7452 CPUs (32/64x 2.35-3.35GHz, 2/16/128MB L1/2/3 cache) featuring up to 128 threads, and 1TB of RAM (3200 MT/s DDR4). All software is compiled using the GNU compiler collection (gcc) version 13.3.0 with flags for maximum optimization (-O3 -DNDEBUG -march=native).
| File | |||
|---|---|---|---|
| cere | |||
| einstein.de.txt | |||
| einstein.en.txt | |||
| Escherichia-Coli | |||
| influenza | |||
| kernel | |||
| para | |||
| world-leaders |
File sars.1Gi sars.2Gi sars.4Gi sars.8Gi sars.16Gi sars.32Gi sars
Input Files
We do our experiments on two sets of input files. First, we use the Pizza & Chili repetitive corpus [14], a well-established input corpus in the field of compression benchmarks also used in related work [22, 38]. Second, denoted in the following by sars, we downloaded around of nucleotide data of sars-COV-2 genomes (TaxID 2697049) in FASTA format from NCBI Virus [3]. It is the concatenation of all nucleotides released date between July 31, 2021 and December 31, 2021; it features low entropy (the alphabet is dominated by A,C,G,T) and high repetitiveness. We consider several prefixes of sars of increasing length. Table 1 lists relevant statistics for all input files that we use.
Competitors
Our C++ implementation of Section 4, called alz (approximate LZ77) in the following, is publicly available (see Supplementary Material). We compare it against the following competitors from related work:
-
topk-lz77 [11, 12] – approximate LZ77 in constrained space based on maintaining frequent patterns. In preliminary experiments, we found that the space requirement is bytes, where is the number of maintained frequent patterns and is the block size. We set and such that at most bytes (the input size) are used, and such that of the memory is used for blocks and the remaining for the sketch, which yields a good general trade-off. For large inputs, we limit to . Concretely, we set and .
(We conciously excluded KKP [25] from the experiments because only 32-bit implementations are available and the results are rather foreseeable. Indeed, in preliminary experiments on inputs , KKP2, the most space-efficient implementation, was faster than the competitors by at least an order of magnitude, but it requires bytes of memory.)
parameters using all available threads.
The sampling parameter was fixed to .
5.2 Impact of Sampling Rate and Multithreading
Before comparing alz to the competitors, we evaluate the impact of the main parameters to the running time of the individual steps of alz according to Section 4: the sampling parameter that governs the size of the PFP (and the average length of a metacharacter), and the number of available threads. We evaluate this on the largest possible input, which is sars (approximately ). The results are shown in Figure 3.
We first look at Figure 3(a), showing the impact of the sampling parameter on the relative running times of each step in relation to the total running time (later given and discussed in Section 5.3) using the maximum number of available threads (128). Recall that the parsing size is in expectation; for sars, this holds almost precisely for all . For small , we see that computing the suffix array of the parsing dominates the running time ( for ), rendering the other steps nearly negligible except for computing the LZ77 parsing. This is the expected behaviour as the parsing approaches the size of the input. On the other extreme that we consider (), suffix array construction takes even less time than loading the dictionary from disk or computing the set of trigger strings that later defines the parsing. Here, encoding becomes the dominating step taking about of the running time, which is also expected since we have fewer copy phrases and encoding is mostly reduced to Huffman-coding large uncompressed portions of the input.
Figure 3(b) shows the parallel speedup of the individual steps. Here, we fixed an exemplary sampling rate of , which yields a reasonable balance of work to do in every step when using all threads, as seen in Figure 3(a). The preliminary parsing that detects and takes note of trigger strings benefits the most from multiple threads, which is expected given the linearity of the task (computing fingerprints and pushing metacharacter borders to a thread-private list). Still, at , we only achieve a speedup of about 16, where it also appears to plateau, likely because of being memory-bound. Using a binary merge tree to find the set of distinct metacharacters appears to achieve the desired affect of logarithmic speedups up to . The speedups for representing the parsing as lexicographic ranks (concurrent reading from a hash table) as well as computing the approximate LZ77 parsing (via suffix array and dictionary) are rather disappointing, but given the fact that these steps involve mostly random access, this is likely an indication that these steps are memory-bound. This is also the case for suffix sorting, confirming the statement of the author of (libsais) that no speedup can be expected. At the very least, computing the inverse suffix array does benefit somewhat from parallelization. Overall, we can reduce the running time of alz to almost a third using 32 threads; using more, however, does not yield much benefit. In a productive environment, it may be advisable to do some steps in sequential to reduce potential overhead coming from parallelization222We thank the anonymous reviewer for noting this. .
5.3 Performance Comparison
We now evaluate alz with different sampling parameters against the competitors (pfp-lz77, relz and ssszip) regarding running time, peak memory usage and approximation of the LZ77 parsing for all input files. We set the block size for the preliminary parsing (Section 4.1) to (and thus do not take advantage of Section 4.6). Recall how alz does not do any sophisticated encoding in itself but is designed to have its output encoded by a downstream successor (see Section 4.5). For a fair running time comparison, in this experiment, we configure ssszip to use pigz (parallel gzip) as its encoder with the -0 flag for no compression. The encodings of pfp-lz77 and relz are already straightforward: pfp-lz77 writes phrases as source and length pairs of bits each, while relz encodes these pairs using VByte codes. Similarly, rle-lz77-o encodes phrases of pairs of 32-bit integers333This does mean that the output of rle-lz77-o cannot be decoded for input files . We conjecture this to be an oversight in the implementation, but double-checked that it affects only the output. The entire prior computation of the parsing is done using 64-bit integers, i.e., the results are still meaningful. .
Figure 4 shows the average running times over three executions (except for pfp-lz77 and rle-lz77-o on inputs of size and up, where they required over 10 hours and were only executed once). Even for the rather impractical sampling rate of , alz is faster than pfp-lz77 by an order of magnitude on prefixes of sars. On the smaller inputs, pfp-lz77 is always slower when our sampling rate is or higher. For different sampling rates, the running time of our implementation behaves consistently for almost every input as already seen in Figure 3(a) for sars. For very high sampling rates (), the encoding begins to dominate the overall running time (see Section 5.2) causing the alz to become slower in some cases (e.g., cere). For the large inputs, ssszip is about as fast as alz with a sampling rate of ; for other inputs, this varies. Their repetitiveness seems to benefit relz, which becomes competitive only for the large inputs but runs relatively slowly for the smaller corpus. On all inputs, topk-lz77 is comparatively slow and rle-lz77-o is the slowest for most inputs as expected. Remarkably, however, rle-lz77-o outperforms pfp-lz77 on longer prefixes of sars despite using only a fraction of the memory.
Next, we look at peak memory consumption shown in Figure 5. For small sampling rates (), alz requires more memory than pfp-lz77, which is excessive considering that we only compute an approximation of LZ77, albeit much faster. The memory footprint becomes more reasonable at around , where for many inputs, it approaches or falls below the size of the input. We see that our strategy to pre-parse blockwise and only load the dictionary into RAM pays off: The space is dominated by the size of the PFP and the dictionary. For suitable , this allows us to work in memory less than the input size (e.g., sars for ). However, it must be noted that this behaviour depends on the size of the dictionary, and hence ultimately on the repetitiveness of the input. In any event, we have an advantage over ssszip, which always fully loads the input into RAM. In relz, memory consumption is constrained to by making use of external memory if needed. This becomes apparent for the longer prefixes of sars. Unsurprisingly, rle-lz77-o uses the least memory in all instances as it works in compressed space. Furthermore, topk-lz77 always uses space somewhat less than the input size as configured.
Judging by the results thus far, it appears that is a good time/space trade-off for alz, yielding acceptable running times and memory consumption for all inputs.
Preparing to evaluate compression in the next section, we look at the approximation of the LZ77 parsing displayed in Figure 6. Already for , the number of phrases produced by alz is more than ten times that of the exact LZ77 parsing (which is computed by pfp-lz77). This effect is somewhat diminishing: when doubling the sampling rate (e.g., ), we do not double the number of phrases, which is also thanks to left and right extension of copy phrases as described in Section 4.4.
We recall that ssszip follows a similar strategy as our implementation. It uses the (hardcoded) parameter for computing the string synchronizing set. This leads us to conjecture that the resulting parsing should be roughly comparable to alz using a sampling rate of . In direct comparison to that, ssszip is significantly slower on most inputs and has a greater memory footprint. However, it appears to be better than alz at capturing long copy phrases, producing less phrases overall. As stated earlier, relz computes the exact LZ77 parsing for sufficiently small inputs and only approximates it if a memory threshold would be exceeded. This is the case, e.g., for the prefixes of sars. However, the approximation ratio clearly remains very small ( for sars).
5.4 Compression Comparison
Given the results from the previous section, particularly regarding the number of produced phrases, we now get to the most interesting experiment that evaluates the compression ratio, which we define as the size of the output file divided by the size of the input file.
For this experiment, we use alz with sampling parameter and encode its output using the following the standard compressors zstd (Facebook’s ZStandard compressor [9]) and bsc (a highly engineered block-sorting compressor [17]). They are run with flags to achieve the best compression (i.e., -19 for zstd and -b2047e2 for bsc). We apply our observations from Section 4.5 and produce a slightly different intermediate encoding for the LZ-based compressor (zstd) and that based on sorting (bsc). Note that we considered further standard compressors (namely pigz, xz and bzip2) in preliminary experiments. They are omitted for the sake of readability of the plot because they were not competitive. Because the output of rle-lz77-o is correct only for inputs of size less than (see footnote 3) and the encoding is naïve, we omit it in this experiment as well.
By default, ssszip, uses zstd for encoding (we also pass -19 here for best compression). To level the playing field, we also plug in bsc as its encoder (passing also -b2047e2). Furthermore, we also apply zstd and bsc to the output of ReLZ. To argue about the usefulness of precompression via alz (or ssszip) in the first place, we also compress all files with just those standard compressors.
Figure 7 shows compression time versus compression ratio for all competitors and inputs. Our pipeline alz|bsc produces the smallest outputs for all files except influenza, where it is only marginally surpassed by zstd (albeit much slower) and bsc itself (on par). As expected, computing the exact LZ77 factorization is always a comparatively slow approach (pfp-lz77, but also relz for the files from the Pizza & Chili corpus). On the largest input, sars, we can clearly see that computing the exact parsing is completely impractical, pfp-lz77 taking several orders of magnitude longer than all other compressors.
Our conceptually close competitor ssszip yields varying results: encoding using bsc appears to be faster than the default zstd, but the compression ratio appears to be indifferent to that. On some inputs, ssszip is competitive (namely influenza, kernel and para) while on others, it is clearly slower than our alz pipelines (e.g., einstein.en.txt or sars). Similar observations can be made for relz and topk-lz77. While they achieve competitive compression, they are overall slower than alz.
6 Conclusions and Outlook
To restate our initial question: Is it necessary to compute the exact LZ77 parsing to achieve competitive compression? Our answer, based on our experiments, is no.
In fact, we go further than that: using an approximate LZ77 parsing, we can achieve running times several orders of magnitudes faster than the state of the art for exact LZ77 and still output smaller files than those compressed using exact LZ77. If the output is sufficiently repetitive (e.g., like sars), then we can do so using memory less than the input size.
Moreover, our idea of forming a symbiosis of a precompressor that takes care of long-range repetitiveness and a standard compressor that excels at capturing local repetitiveness has been implemented successfully. Our alz pipelines yield a competitive trade-off between compression time and ratio for large, repetitive inputs that is both faster and produces smaller output files than similar state-of-the-art implementations.
However, we found in preliminary experiments that alz fares much worse on inputs that are not repetitive on a large scale (e.g., a Wikipedia dump). In these cases, it may be more advisable, e.g., to run a standard compressor right away instead of wasting time on an attempt to precompress. It would be interesting to be able to, given a (potentially very large) input file, efficiently make a coarse but valid statement about its repetitiveness. While simple statistics such as the zeroth-order entropy do not suffice, the computation of known measures of repetitiveness (such as the number of LZ77 phrases or the number of runs in the Burrows-Wheeler transform) requires too many resources, both time and memory. An idea following immediately from this work could be to plug into the first step (Pre-Parse, see Section 4.1) an estimator for the cardinality of the set of distinct metacharacters. This could be done, e.g., via concurrent hashing (e.g., Intel TBB [24], growt [35] or techniques based on MPSC queues [8]) or cardinality estimating sketches (such as HyperLogLog [20]). If we find that the dictionary is sufficiently small, we can conclude that the input is repetitive and we continue. Otherwise, if we find the input to be not repetitive, we can cancel the precompression before wasting too much time.
References
- [1] Uwe Baier. Linear-time suffix sorting-a new approach for suffix array construction. In 27th Annual Symposium on Combinatorial Pattern Matching (CPM), pages 23–1. Dagstuhl, 2016. doi:10.4230/LIPIcs.CPM.2016.23.
- [2] Nico Bertram, Jonas Ellert, and Johannes Fischer. Lyndon words accelerate suffix sorting. In 29th European Symposium on Algorithms (ESA), pages 15–1. Dagstuhl, 2021. doi:10.4230/LIPIcs.ESA.2021.15.
- [3] Bethesda (MD): National Library of Medicine (US), National Center for Biotechnology Information. NCBI Virus. https://www.ncbi.nlm.nih.gov/labs/virus/vssi/#/. Accessed April 14, 2026.
- [4] Philip Bille, Patrick Hagge Cording, Johannes Fischer, and Inge Li Gørtz. Lempel-Ziv compression in a sliding window. In 28th Annual Symposium on Combinatorial Pattern Matching (CPM), volume 78 of LIPIcs, pages 15:1–15:11. Dagstuhl, 2017. doi:10.4230/LIPIcs.CPM.2017.15.
- [5] Timo Bingmann, Andreas Eberle, and Peter Sanders. Engineering parallel string sorting. Algorithmica, 77(1):235–286, 2017. doi:10.1007/s00453-015-0071-1.
- [6] Christina Boucher, Travis Gagie, Alan Kuhnle, Ben Langmead, Giovanni Manzini, and Taher Mun. Prefix-free parsing for building big BWTs. Algorithms Mol. Biol., 14(1):13:1–13:15, 2019. doi:10.1186/S13015-019-0148-5.
- [7] Michael Burrows and David Wheeler. A block-sorting lossless data compression algorithm. Technical Report 124, Digital Equipment Corporation, 1994.
- [8] Robert Clausecker, Florian Kurpicz, and Etienne Palanga. Practical parallel block tree construction: First results. CoRR (accepted at SEA 2026), abs/2512.23314, 2025. doi:10.48550/arXiv.2512.23314.
- [9] Yann Collet and Murray S. Kucherawy. Zstandard compression and the ’application/zstd’ media type. RFC, 8878:1–45, 2021. doi:10.17487/RFC8878.
- [10] Patrick Dinklage, Jonas Ellert, Johannes Fischer, Florian Kurpicz, and Marvin Löbel. Practical wavelet tree construction. Journal of Experimental Algorithms, 26:1.8:1–1.8:67, 2021. doi:10.1145/3457197.
- [11] Patrick Dinklage, Johannes Fischer, and Nicola Prezza. top-k-compress. https://github.com/pdinklag/top-k-compress. Accessed April 14, 2026.
- [12] Patrick Dinklage, Johannes Fischer, and Nicola Prezza. Top-k frequent patterns in streams and parameterized-space LZ compression. In 22nd International Symposium on Experimental Algorithms (SEA), volume 301 of LIPIcs, pages 9:1–9:20. Dagstuhl, 2024. doi:10.4230/LIPIcs.SEA.2024.9.
- [13] Jonas Ellert. Sublinear time Lempel-Ziv (LZ77) factorization. In 30th International Symposium on String Processing and Information Retrieval (SPIRE), volume 14240 of Lecture Notes in Computer Science, pages 171–187. Springer, 2023. doi:10.1007/978-3-031-43980-3_14.
- [14] Paolo Ferragina and Gonzalo Navarro. Pizza & Chili corpus – compressed indexes and their testbeds. http://pizzachili.dcc.uchile.cl/texts.html. Accessed April 14, 2026.
- [15] Johannes Fischer, Travis Gagie, Pawel Gawrychowski, and Tomasz Kociumaka. Approximating LZ77 via small-space multiple-pattern matching. In 23rd European Symposium on Algorithms (ESA), volume 9294, pages 533–544. Springer, 2015. doi:10.1007/978-3-662-48350-3_45.
- [16] Pawel Gawrychowski, Maria Kosche, and Florin Manea. On the number of factors in the LZ-End factorization. In 30th International Symposium on String Processing and Information Retrieval (SPIRE), volume 14240 of Lecture Notes in Computer Science, pages 253–259. Springer, 2023. doi:10.1007/978-3-031-43980-3_20.
- [17] Ilya Grebnov. libbsc. https://github.com/IlyaGrebnov/libbsc. Accessed April 14, 2026.
- [18] Ilya Grebnov. libsais. https://github.com/IlyaGrebnov/libsais. Accessed April 14, 2026.
- [19] Torben Hagerup. Sorting and searching on the word RAM. In 15th Annual Symposium on Theoretical Aspects of Computer Science (STACS), volume 1373 of Lecture Notes in Computer Science, pages 366–398. Springer, 1998. doi:10.1007/BFb0028575.
- [20] Stefan Heule, Marc Nunkesser, and Alexander Hall. HyperLogLog in practice: algorithmic engineering of a state of the art cardinality estimation algorithm. In Joint 2013 EDBT/ICDT Conferences, pages 683–692. ACM, 2013. doi:10.1145/2452376.2452456.
- [21] Aaron Hong, Massimiliano Rossi, and Christina Boucher. PFP_LZ77. https://github.com/AaronHong1024/PFP_LZ77. Accessed April 14, 2026.
- [22] Aaron Hong, Massimiliano Rossi, and Christina Boucher. LZ77 via prefix-free parsing. In 25th Workshop on Algorithm Engineering and Experiments (ALENEX), pages 123–134. SIAM, 2023. doi:10.1137/1.9781611977561.CH11.
- [23] Takumi Ideue, Takuya Mieno, Mitsuru Funakoshi, Yuto Nakashima, Shunsuke Inenaga, and Masayuki Takeda. On the approximation ratio of LZ-End to LZ77. In 28th International Symposium on String Processing and Information Retrieval (SPIRE), volume 12944 of Lecture Notes in Computer Science, pages 114–126. Springer, 2021. doi:10.1007/978-3-030-86692-1_10.
- [24] Intel Corporation. Intel®oneAPI Threading Building Blocks. https://www.intel.com/content/www/us/en/developer/tools/oneapi/onetbb.html. Accessed April 14, 2026.
- [25] Juha Kärkkäinen, Dominik Kempa, and Simon J. Puglisi. Linear time Lempel-Ziv factorization: Simple, fast, small. In 24th Annual Symposium on Combinatorial Pattern Matching (CPM), volume 7922 of Lecture Notes in Computer Science, pages 189–200. Springer, 2013. doi:10.1007/978-3-642-38905-4_19.
- [26] Juha Kärkkäinen, Dominik Kempa, and Simon J. Puglisi. Lempel-Ziv parsing in external memory. In 2014 Data Compression Conference (DCC), pages 153–162. IEEE, 2014. doi:10.1109/DCC.2014.78.
- [27] Richard M. Karp and Michael O. Rabin. Efficient randomized pattern-matching algorithms. IBM J. Res. Dev., 31(2):249–260, 1987. doi:10.1147/rd.312.0249.
- [28] Dominik Kempa and Tomasz Kociumaka. String synchronizing sets: sublinear-time BWT construction and optimal LCE data structure. In 51st Annual ACM Symposium on Theory of Computing (STOC), pages 756–767. ACM, 2019. doi:10.1145/3313276.3316368.
- [29] Dominik Kempa and Tomasz Kociumaka. Lempel-Ziv (LZ77) factorization in sublinear time. In 65th Symposium on Foundations of Computer Science (FOCS), pages 2045–2055. IEEE, 2024. doi:10.1109/FOCS61266.2024.00122.
- [30] Dominik Kempa and Barna Saha. An upper bound and linear-space queries on the LZ-End parsing. In ACM-SIAM Symposium on Discrete Algorithms (SODA), pages 2847–2866. SIAM, 2022. doi:10.1137/1.9781611977073.111.
- [31] Dmitry Kosolobov, Daniel Valenzuela, Gonzalo Navarro, and Simon J. Puglisi. ReLZ. https://gitlab.com/dvalenzu/ReLZ. Accessed April 14, 2026.
- [32] Dmitry Kosolobov, Daniel Valenzuela, Gonzalo Navarro, and Simon J. Puglisi. Lempel-Ziv-like parsing in small space. Algorithmica, 82(11):3195–3215, 2020. doi:10.1007/S00453-020-00722-6.
- [33] Sebastian Kreft and Gonzalo Navarro. LZ77-like compression with fast random access. In 2010 Data Compression Conference (DCC), pages 239–248. IEEE, 2010. doi:10.1109/DCC.2010.29.
- [34] Shanika Kuruppu, Simon J. Puglisi, and Justin Zobel. Relative Lempel-Ziv compression of genomes for large-scale storage and retrieval. In 17th International Symposium on String Processing and Information Retrieval (SPIRE), pages 201–206. Springer, 2010. doi:10.1007/978-3-642-16321-0_20.
- [35] Tobias Maier, Peter Sanders, and Roman Dementiev. Concurrent hash tables: Fast and general(?)! ACM Trans. Parallel Comput., 5(4):16:1–16:32, 2019. doi:10.1145/3309206.
- [36] Udi Manber and Eugene W. Myers. Suffix arrays: A new method for on-line string searches. SIAM J. Comput., 22(5):935–948, 1993. doi:10.1137/0222058.
- [37] Lukas Nalbach. lz77-sss. https://github.com/LukasNalbach/lz77-sss. Accessed April 14, 2026.
- [38] Lukas Nalbach. Implementing sublinear-time approximation algorithms for the Lempel-Ziv 77 factorization. Master’s thesis, TU Dortmund University, 2024. doi:10.17877/DE290R-25750.
- [39] Ge Nong, Sen Zhang, and Wai Hong Chan. Linear suffix array construction by almost pure induced-sorting. In 2009 Data Compression Conference (DCC), pages 193–202. IEEE, 2009. doi:10.1109/DCC.2009.42.
- [40] Tatsuya Ohno, Kensuke Sakai, Yoshimasa Takabatake, Tomohiro I, and Hiroshi Sakamoto. OnlineRlbwt. https://github.com/itomomoti/OnlineRlbwt. Accessed April 14, 2026.
- [41] Tatsuya Ohno, Kensuke Sakai, Yoshimasa Takabatake, Tomohiro I, and Hiroshi Sakamoto. A faster implementation of online RLBWT and its application to LZ77 parsing. J. Discrete Algorithms, 52-53:18–28, 2018. doi:10.1016/J.JDA.2018.11.002.
- [42] OpenMP ARB. OpenMP. https://www.openmp.org. Accessed April 14, 2026.
- [43] Alberto Policriti and Nicola Prezza. LZ77 computation based on the run-length encoded BWT. Algorithmica, 80(7):1986–2011, 2018. doi:10.1007/S00453-017-0327-Z.
- [44] Rolf Rabenseifner. Optimization of collective reduction operations. In 4th International Conference on Computational Science (ICCS), Lecture Notes in Computer Science, pages 1–9. Springer, 2004. doi:10.1007/978-3-540-24685-5_1.
- [45] James A. Storer and Thomas G. Szymanski. Data compression via textual substitution. J. ACM, 29(4):928–951, 1982. doi:10.1145/322344.322346.
- [46] Jacob Ziv and Abraham Lempel. A universal algorithm for sequential data compression. IEEE Trans. Inform. Theory, 23(3):337–343, 1977. doi:10.1109/TIT.1977.1055714.
