The libsais library provides fast linear-time construction of suffix array (SA), generalized suffix array (GSA), longest common prefix (LCP) array, permuted LCP (PLCP) array, Burrows-Wheeler transform (BWT) and inverse BWT based on the induced sorting algorithm with optional OpenMP support for multi-core parallel construction.
C
256
53 commits
updated Oct 19, 2026
The libsais library provides fast (see Benchmarks below) linear-time construction of suffix array (SA), generalized suffix array (GSA), longest common prefix (LCP) array, permuted LCP (PLCP) array, Burrows-Wheeler transform (BWT) and inverse BWT, based on the induced sorting algorithm described in the following papers (with optional OpenMP support for multi-threaded parallel construction):
Copyright (c) 2021-2025 Ilya Grebnov [email protected]
The libsais is inspired by libdivsufsort, sais libraries by Yuta Mori and msufsort by Michael Maniscalco.
If you are looking for even faster construction times, you can try libcubwt a library for GPU-based suffix array, inverse suffix array and Burrows-Wheeler transform construction.
The libsais provides simple C99 API to construct suffix array and Burrows-Wheeler transformed string from a given string over constant-size alphabet. The algorithm runs in a linear time using typically only ~16KB of extra memory (with 2n bytes as absolute worst-case; where n is the length of the string). OpenMP acceleration uses 200KB of addition memory per thread.
- The libsais works with compilers from GNU, Microsoft and Intel, but I recommend Clang for best performance.
- The libsais is sensitive to fast memory and software prefetching and might not be suitable for some workloads. Please benchmark yourself.
The libsais is released under the Apache License Version 2.0 with LLVM Exceptions
The libsais is memory-bound, so performance scales primarily with memory bandwidth and concurrency, not raw compute. The optimal number of threads for suffix array construction depends on CPU and memory architecture, as different systems (DDR4 vs DDR5, Intel vs AMD, single- vs multi-CCD) saturate memory at different points with maximum throughput generally following the number of memory channels rather than total core count. The x86-64 dual-channel systems typically saturate near 8 threads, but in practice may show good scaling at 6, 12 or even 16 threads.
For the full changelog, see CHANGES.
/**
* Constructs the suffix array of a given string.
* @param T [0..n-1] The input string.
* @param SA [0..n-1+fs] The output array of suffixes.
* @param n The length of the given string.
* @param fs The extra space available at the end of SA array (0 should be enough for most cases).
* @param freq [0..255] The output symbol frequency table (can be NULL).
* @return 0 if no error occurred, -1 or -2 otherwise.
*/
int32_t libsais(const uint8_t * T, int32_t * SA, int32_t n, int32_t fs, int32_t * freq);
/**
* Constructs the suffix array of a given integer array.
* Note, during construction input array will be modified, but restored at the end if no errors occurred.
* @param T [0..n-1] The input integer array.
* @param SA [0..n-1+fs] The output array of suffixes.
* @param n The length of the integer array.
* @param k The alphabet size of the input integer array.
* @param fs Extra space available at the end of SA array (can be 0, but 4k or better 6k is recommended for optimal performance).
* @return 0 if no error occurred, -1 or -2 otherwise.
*/
int32_t libsais_int(int32_t * T, int32_t * SA, int32_t n, int32_t k, int32_t fs);
/**
* Constructs the burrows-wheeler transformed string (BWT) of a given string.
* @param T [0..n-1] The input string.
* @param U [0..n-1] The output string (can be T).
* @param A [0..n-1+fs] The temporary array.
* @param n The length of the given string.
* @param fs The extra space available at the end of A array (0 should be enough for most cases).
* @param freq [0..255] The output symbol frequency table (can be NULL).
* @return The primary index if no error occurred, -1 or -2 otherwise.
*/
int32_t libsais_bwt(const uint8_t * T, uint8_t * U, int32_t * A, int32_t n, int32_t fs, int32_t * freq);
/**
* Constructs the original string from a given burrows-wheeler transformed string (BWT) with primary index.
* @param T [0..n-1] The input string.
* @param U [0..n-1] The output string (can be T).
* @param A [0..n] The temporary array (NOTE, temporary array must be n + 1 size).
* @param n The length of the given string.
* @param freq [0..255] The input symbol frequency table (can be NULL).
* @param i The primary index.
* @return 0 if no error occurred, -1 or -2 otherwise.
*/
int32_t libsais_unbwt(const uint8_t * T, uint8_t * U, int32_t * A, int32_t n, const int32_t * freq, int32_t i);
/**
* Constructs the permuted longest common prefix array (PLCP) of a given string and a suffix array.
* @param T [0..n-1] The input string.
* @param SA [0..n-1] The input suffix array.
* @param PLCP [0..n-1] The output permuted longest common prefix array.
* @param n The length of the string and the suffix array.
* @return 0 if no error occurred, -1 otherwise.
*/
int32_t libsais_plcp(const uint8_t * T, const int32_t * SA, int32_t * PLCP, int32_t n);
CPMAddPackage(
NAME libsais
GITHUB_REPOSITORY IlyaGrebnov/libsais
GIT_TAG v2.10.4
OPTIONS
"LIBSAIS_USE_OPENMP OFF"
"LIBSAIS_BUILD_SHARED_LIB OFF"
)
target_link_libraries(<your target> libsais)
The libsais uses the SA-IS (Suffix Array Induced Sorting) algorithm to construct both the suffix array and the Burrows-Wheeler transform through recursive decomposition and induced sorting:
The SA-IS algorithm is quite elegant, yet implementing it efficiently presents multiple challenges. The primary challenge is that the SA-IS algorithm exhibits random memory access patterns, which can significantly decrease efficiency due to cache misses. Another significant challenge is that the SA-IS algorithm is not a lightweight construction algorithm; it requires additional memory to support positions classification, induced sorting, compacted string representations, and recursive decomposition. To circumvent this, the libsais implements careful optimizations that are worth highlighting:
for (i = m - 1, j = omp_block_start + prefetch_distance + 3; i >= j; i -= 4)
{
libsais_prefetchr(&T[i - 2 * prefetch_distance]);
libsais_prefetchw(&buckets[BUCKETS_INDEX4(T[i - prefetch_distance - 0], 0)]);
libsais_prefetchw(&buckets[BUCKETS_INDEX4(T[i - prefetch_distance - 1], 0)]);
libsais_prefetchw(&buckets[BUCKETS_INDEX4(T[i - prefetch_distance - 2], 0)]);
libsais_prefetchw(&buckets[BUCKETS_INDEX4(T[i - prefetch_distance - 3], 0)]);
c1 = T[i - 0]; f1 = (fast_uint_t)(c1 > (c0 - (fast_sint_t)(f0))); SA[m] = (sa_sint_t)(i + 1); m -= (f1 & ~f0);
buckets[BUCKETS_INDEX4((fast_uint_t)c0, f0 + f0 + f1)]++;
c0 = T[i - 1]; f0 = (fast_uint_t)(c0 > (c1 - (fast_sint_t)(f1))); SA[m] = (sa_sint_t)(i - 0); m -= (f0 & ~f1);
buckets[BUCKETS_INDEX4((fast_uint_t)c1, f1 + f1 + f0)]++;
c1 = T[i - 2]; f1 = (fast_uint_t)(c1 > (c0 - (fast_sint_t)(f0))); SA[m] = (sa_sint_t)(i - 1); m -= (f1 & ~f0);
buckets[BUCKETS_INDEX4((fast_uint_t)c0, f0 + f0 + f1)]++;
c0 = T[i - 3]; f0 = (fast_uint_t)(c0 > (c1 - (fast_sint_t)(f1))); SA[m] = (sa_sint_t)(i - 2); m -= (f0 & ~f1);
buckets[BUCKETS_INDEX4((fast_uint_t)c1, f1 + f1 + f0)]++;
}
for (i = omp_block_start, j = omp_block_start + omp_block_size - 2 * prefetch_distance - 1; i < j; i += 2)
{
libsais_prefetchr(&SA[i + 3 * prefetch_distance]);
libsais_prefetchr(&T[SA[i + 2 * prefetch_distance + 0] & SAINT_MAX] - 1);
libsais_prefetchr(&T[SA[i + 2 * prefetch_distance + 0] & SAINT_MAX] - 2);
libsais_prefetchr(&T[SA[i + 2 * prefetch_distance + 1] & SAINT_MAX] - 1);
libsais_prefetchr(&T[SA[i + 2 * prefetch_distance + 1] & SAINT_MAX] - 2);
sa_sint_t p0 = SA[i + prefetch_distance + 0] & SAINT_MAX; sa_sint_t v0 = BUCKETS_INDEX4(T[p0 - (p0 > 0)], 0); libsais_prefetchw(&buckets[v0]);
sa_sint_t p1 = SA[i + prefetch_distance + 1] & SAINT_MAX; sa_sint_t v1 = BUCKETS_INDEX4(T[p1 - (p1 > 0)], 0); libsais_prefetchw(&buckets[v1]);
sa_sint_t p2 = SA[i + 0]; d += (p2 < 0); p2 &= SAINT_MAX; sa_sint_t v2 = BUCKETS_INDEX4(T[p2 - 1], T[p2 - 2] >= T[p2 - 1]);
SA[buckets[v2]++] = (p2 - 1) | (sa_sint_t)((sa_uint_t)(buckets[2 + v2] != d) << (SAINT_BIT - 1)); buckets[2 + v2] = d;
sa_sint_t p3 = SA[i + 1]; d += (p3 < 0); p3 &= SAINT_MAX; sa_sint_t v3 = BUCKETS_INDEX4(T[p3 - 1], T[p3 - 2] >= T[p3 - 1]);
SA[buckets[v3]++] = (p3 - 1) | (sa_sint_t)((sa_uint_t)(buckets[2 + v3] != d) << (SAINT_BIT - 1)); buckets[2 + v3] = d;
}
Full list of benchmarks are moved to own Benchmarks.md file.
824 followers · starred Aug 2022
1,456 followers · starred Oct 2021
366 followers · starred May 2021
46 followers · starred Mar 2025
The libsais library provides fast linear-time construction of suffix array (SA), generalized suffix array (GSA), longest common prefix (LCP) array, permuted LCP (PLCP) array, Burrows-Wheeler transform (BWT) and inverse BWT based on the induced sorting algorithm with optional OpenMP support for multi-core parallel construction.
C
256
53 commits
updated Oct 19, 2026
The libsais library provides fast (see Benchmarks below) linear-time construction of suffix array (SA), generalized suffix array (GSA), longest common prefix (LCP) array, permuted LCP (PLCP) array, Burrows-Wheeler transform (BWT) and inverse BWT, based on the induced sorting algorithm described in the following papers (with optional OpenMP support for multi-threaded parallel construction):
Copyright (c) 2021-2025 Ilya Grebnov [email protected]
The libsais is inspired by libdivsufsort, sais libraries by Yuta Mori and msufsort by Michael Maniscalco.
If you are looking for even faster construction times, you can try libcubwt a library for GPU-based suffix array, inverse suffix array and Burrows-Wheeler transform construction.
The libsais provides simple C99 API to construct suffix array and Burrows-Wheeler transformed string from a given string over constant-size alphabet. The algorithm runs in a linear time using typically only ~16KB of extra memory (with 2n bytes as absolute worst-case; where n is the length of the string). OpenMP acceleration uses 200KB of addition memory per thread.
- The libsais works with compilers from GNU, Microsoft and Intel, but I recommend Clang for best performance.
- The libsais is sensitive to fast memory and software prefetching and might not be suitable for some workloads. Please benchmark yourself.
The libsais is released under the Apache License Version 2.0 with LLVM Exceptions
The libsais is memory-bound, so performance scales primarily with memory bandwidth and concurrency, not raw compute. The optimal number of threads for suffix array construction depends on CPU and memory architecture, as different systems (DDR4 vs DDR5, Intel vs AMD, single- vs multi-CCD) saturate memory at different points with maximum throughput generally following the number of memory channels rather than total core count. The x86-64 dual-channel systems typically saturate near 8 threads, but in practice may show good scaling at 6, 12 or even 16 threads.
For the full changelog, see CHANGES.
/**
* Constructs the suffix array of a given string.
* @param T [0..n-1] The input string.
* @param SA [0..n-1+fs] The output array of suffixes.
* @param n The length of the given string.
* @param fs The extra space available at the end of SA array (0 should be enough for most cases).
* @param freq [0..255] The output symbol frequency table (can be NULL).
* @return 0 if no error occurred, -1 or -2 otherwise.
*/
int32_t libsais(const uint8_t * T, int32_t * SA, int32_t n, int32_t fs, int32_t * freq);
/**
* Constructs the suffix array of a given integer array.
* Note, during construction input array will be modified, but restored at the end if no errors occurred.
* @param T [0..n-1] The input integer array.
* @param SA [0..n-1+fs] The output array of suffixes.
* @param n The length of the integer array.
* @param k The alphabet size of the input integer array.
* @param fs Extra space available at the end of SA array (can be 0, but 4k or better 6k is recommended for optimal performance).
* @return 0 if no error occurred, -1 or -2 otherwise.
*/
int32_t libsais_int(int32_t * T, int32_t * SA, int32_t n, int32_t k, int32_t fs);
/**
* Constructs the burrows-wheeler transformed string (BWT) of a given string.
* @param T [0..n-1] The input string.
* @param U [0..n-1] The output string (can be T).
* @param A [0..n-1+fs] The temporary array.
* @param n The length of the given string.
* @param fs The extra space available at the end of A array (0 should be enough for most cases).
* @param freq [0..255] The output symbol frequency table (can be NULL).
* @return The primary index if no error occurred, -1 or -2 otherwise.
*/
int32_t libsais_bwt(const uint8_t * T, uint8_t * U, int32_t * A, int32_t n, int32_t fs, int32_t * freq);
/**
* Constructs the original string from a given burrows-wheeler transformed string (BWT) with primary index.
* @param T [0..n-1] The input string.
* @param U [0..n-1] The output string (can be T).
* @param A [0..n] The temporary array (NOTE, temporary array must be n + 1 size).
* @param n The length of the given string.
* @param freq [0..255] The input symbol frequency table (can be NULL).
* @param i The primary index.
* @return 0 if no error occurred, -1 or -2 otherwise.
*/
int32_t libsais_unbwt(const uint8_t * T, uint8_t * U, int32_t * A, int32_t n, const int32_t * freq, int32_t i);
/**
* Constructs the permuted longest common prefix array (PLCP) of a given string and a suffix array.
* @param T [0..n-1] The input string.
* @param SA [0..n-1] The input suffix array.
* @param PLCP [0..n-1] The output permuted longest common prefix array.
* @param n The length of the string and the suffix array.
* @return 0 if no error occurred, -1 otherwise.
*/
int32_t libsais_plcp(const uint8_t * T, const int32_t * SA, int32_t * PLCP, int32_t n);
CPMAddPackage(
NAME libsais
GITHUB_REPOSITORY IlyaGrebnov/libsais
GIT_TAG v2.10.4
OPTIONS
"LIBSAIS_USE_OPENMP OFF"
"LIBSAIS_BUILD_SHARED_LIB OFF"
)
target_link_libraries(<your target> libsais)
The libsais uses the SA-IS (Suffix Array Induced Sorting) algorithm to construct both the suffix array and the Burrows-Wheeler transform through recursive decomposition and induced sorting:
The SA-IS algorithm is quite elegant, yet implementing it efficiently presents multiple challenges. The primary challenge is that the SA-IS algorithm exhibits random memory access patterns, which can significantly decrease efficiency due to cache misses. Another significant challenge is that the SA-IS algorithm is not a lightweight construction algorithm; it requires additional memory to support positions classification, induced sorting, compacted string representations, and recursive decomposition. To circumvent this, the libsais implements careful optimizations that are worth highlighting:
for (i = m - 1, j = omp_block_start + prefetch_distance + 3; i >= j; i -= 4)
{
libsais_prefetchr(&T[i - 2 * prefetch_distance]);
libsais_prefetchw(&buckets[BUCKETS_INDEX4(T[i - prefetch_distance - 0], 0)]);
libsais_prefetchw(&buckets[BUCKETS_INDEX4(T[i - prefetch_distance - 1], 0)]);
libsais_prefetchw(&buckets[BUCKETS_INDEX4(T[i - prefetch_distance - 2], 0)]);
libsais_prefetchw(&buckets[BUCKETS_INDEX4(T[i - prefetch_distance - 3], 0)]);
c1 = T[i - 0]; f1 = (fast_uint_t)(c1 > (c0 - (fast_sint_t)(f0))); SA[m] = (sa_sint_t)(i + 1); m -= (f1 & ~f0);
buckets[BUCKETS_INDEX4((fast_uint_t)c0, f0 + f0 + f1)]++;
c0 = T[i - 1]; f0 = (fast_uint_t)(c0 > (c1 - (fast_sint_t)(f1))); SA[m] = (sa_sint_t)(i - 0); m -= (f0 & ~f1);
buckets[BUCKETS_INDEX4((fast_uint_t)c1, f1 + f1 + f0)]++;
c1 = T[i - 2]; f1 = (fast_uint_t)(c1 > (c0 - (fast_sint_t)(f0))); SA[m] = (sa_sint_t)(i - 1); m -= (f1 & ~f0);
buckets[BUCKETS_INDEX4((fast_uint_t)c0, f0 + f0 + f1)]++;
c0 = T[i - 3]; f0 = (fast_uint_t)(c0 > (c1 - (fast_sint_t)(f1))); SA[m] = (sa_sint_t)(i - 2); m -= (f0 & ~f1);
buckets[BUCKETS_INDEX4((fast_uint_t)c1, f1 + f1 + f0)]++;
}
for (i = omp_block_start, j = omp_block_start + omp_block_size - 2 * prefetch_distance - 1; i < j; i += 2)
{
libsais_prefetchr(&SA[i + 3 * prefetch_distance]);
libsais_prefetchr(&T[SA[i + 2 * prefetch_distance + 0] & SAINT_MAX] - 1);
libsais_prefetchr(&T[SA[i + 2 * prefetch_distance + 0] & SAINT_MAX] - 2);
libsais_prefetchr(&T[SA[i + 2 * prefetch_distance + 1] & SAINT_MAX] - 1);
libsais_prefetchr(&T[SA[i + 2 * prefetch_distance + 1] & SAINT_MAX] - 2);
sa_sint_t p0 = SA[i + prefetch_distance + 0] & SAINT_MAX; sa_sint_t v0 = BUCKETS_INDEX4(T[p0 - (p0 > 0)], 0); libsais_prefetchw(&buckets[v0]);
sa_sint_t p1 = SA[i + prefetch_distance + 1] & SAINT_MAX; sa_sint_t v1 = BUCKETS_INDEX4(T[p1 - (p1 > 0)], 0); libsais_prefetchw(&buckets[v1]);
sa_sint_t p2 = SA[i + 0]; d += (p2 < 0); p2 &= SAINT_MAX; sa_sint_t v2 = BUCKETS_INDEX4(T[p2 - 1], T[p2 - 2] >= T[p2 - 1]);
SA[buckets[v2]++] = (p2 - 1) | (sa_sint_t)((sa_uint_t)(buckets[2 + v2] != d) << (SAINT_BIT - 1)); buckets[2 + v2] = d;
sa_sint_t p3 = SA[i + 1]; d += (p3 < 0); p3 &= SAINT_MAX; sa_sint_t v3 = BUCKETS_INDEX4(T[p3 - 1], T[p3 - 2] >= T[p3 - 1]);
SA[buckets[v3]++] = (p3 - 1) | (sa_sint_t)((sa_uint_t)(buckets[2 + v3] != d) << (SAINT_BIT - 1)); buckets[2 + v3] = d;
}
Full list of benchmarks are moved to own Benchmarks.md file.
824 followers · starred Aug 2022
1,456 followers · starred Oct 2021
366 followers · starred May 2021
46 followers · starred Mar 2025