Repository: ashvardanian/StringZilla (language: rust)

Issue:
Overwrite LibC symbols with `LD_PRELOAD`
We may want to extend the `c/lib.c` to override some of the most commonly used LibC operations, that StringZilla may be better equipped to handle. This includes:

- `memmem` and `memchr` - definitely
- `memcpy`, `memmove`, `memset` - possibly
- `memrchr` - GNU extension for reverse-order `memchr`
- `memfrob` - GNU extension with the `sz_generate` random generator

If we can afford a `strlen` call before the operation, several other APIs and [GNU extensions](https://www.gnu.org/software/gnulib/manual/html_node/Glibc-string_002eh.html) can be replaced.



Discussion / hints:

Fix patch:
diff --git a/.github/workflows/Dockerfile.libs b/.github/workflows/Dockerfile.libs
new file mode 100644
index 00000000..69a34a01
--- /dev/null
+++ b/.github/workflows/Dockerfile.libs
@@ -0,0 +1,36 @@
+FROM ubuntu:22.04
+ENV DEBIAN_FRONTEND=noninteractive
+
+ENV user_name="runner"
+ARG TARGETPLATFORM
+ARG docker_ip
+ARG user_pass
+
+RUN apt-get update && \
+    apt-get install -y --no-install-recommends cmake g++-12 gcc-12 libc++-dev libc++abi-dev build-essential openssh-client sshpass && \
+    apt-get clean && \
+    rm -rf /var/lib/apt/lists/*
+
+ENV CC=gcc-12
+ENV CXX=g++-12
+
+COPY . .
+
+RUN cmake -DCMAKE_BUILD_TYPE=Release -B ./build_release
+RUN cmake --build ./build_release --config Release
+
+RUN if [ "$TARGETPLATFORM" = "linux/amd64" ]; then \
+        export arch="amd"; \
+    elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then \
+        export arch="arm"; \
+    fi && \
+    version=$(cat VERSION) && \
+    mkdir -p stringzillite_linux_"$arch"_"$version"/DEBIAN && \
+    touch stringzillite_linux_"$arch"_"$version"/DEBIAN/control && \
+    mkdir -p stringzillite_linux_"$arch"_"$version"/usr/local/lib && \
+    mkdir stringzillite_linux_"$arch"_"$version"/usr/local/include && \
+    cp include/stringzilla/stringzilla.h stringzillite_linux_"$arch"_"$version"/usr/local/include/ && \
+    cp build_release/stringzillite.so stringzillite_linux_"$arch"_"$version"/usr/local/lib/ && \
+    echo "Package: stringzilla\nVersion: $version\nMaintainer: Ash Vardanian\nArchitecture: "$arch"\nDescription: SIMD-accelerated string search, sort, hashes, fingerprints, & edit distances" > stringzillite_linux_"$arch"_"$version"/DEBIAN/control && \
+    dpkg-deb --build stringzillite_linux_"$arch"_"$version" && \
+    sshpass -p "$user_pass" scp -o StrictHostKeyChecking=no stringzillite_linux_"$arch"_"$version".deb "$user_name"@"$docker_ip":/home/$user_name/work/stringzilla/stringzilla/
\ No newline at end of file
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index a4d9fd26..ed11671e 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -150,3 +150,146 @@ jobs:
   #     - uses: JS-DevTools/npm-publish@v2
   #       with:
   #         token: ${{ secrets.NPM_TOKEN }}
+
+  create_linux_deb_package:
+    name: Create Debian Package for ${{ matrix.arch }}
+    runs-on: ubuntu-22.04
+    needs: versioning
+    strategy:
+      fail-fast: false
+      matrix:
+        arch: ["linux/amd64", "linux/arm64"]
+    steps:
+      - uses: actions/checkout@v3
+        with:
+          persist-credentials: false
+          ref: "main"
+      - run: git submodule update --init --recursive
+
+      - name: Setup QEMU
+        uses: docker/setup-qemu-action@v2
+
+      - name: Setup Docker
+        uses: docker/setup-buildx-action@v2
+
+      - name: Get Docker daemon IP
+        id: dockerip
+        run: |
+          docker_ip=$(ip addr show docker0 | grep -Po 'inet \K[\d.]+') && \
+          echo "docker_ip=$docker_ip" >> "$GITHUB_OUTPUT"
+
+      - name: Set runner user password
+        id: userpass
+        run: |
+          user_pass="1122"
+          echo "runner:$user_pass" | sudo chpasswd
+          echo "user_pass=$user_pass" >> "$GITHUB_OUTPUT"
+
+      - name: Build library
+        run: |
+          docker buildx create --use
+          docker buildx build \
+          --platform ${{ matrix.arch }} \
+          --build-arg docker_ip=${{ steps.dockerip.outputs.docker_ip }} \
+          --build-arg user_pass=${{ steps.userpass.outputs.user_pass }} \
+          --file .github/workflows/Dockerfile.libs \
+          --tag unum/stringzillite-library:latest \
+          --load .
+      - name: Clear Docker buildx cache
+        run: docker buildx prune --all --force
+
+      - name: Upload archive
+        uses: xresloader/upload-to-github-release@v1
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+        with:
+          file: "*.deb"
+          update_latest_release: true
+
+  create_windows_dll_library:
+    name: Create Dll Library for Windows ${{ matrix.arch }}
+    runs-on: windows-2022
+    needs: versioning
+    strategy:
+      fail-fast: false
+      matrix:
+        arch: [x64, x86]
+    steps:
+      - uses: actions/checkout@v3
+        with:
+          persist-credentials: false
+          ref: "main"
+      - run: git submodule update --init --recursive
+
+      - name: Setup CMake
+        uses: jwlawson/actions-setup-cmake@v1.13
+
+      - uses: actions/setup-go@v4
+        with:
+          go-version: "^1.19.0"
+
+      - name: Setup MinGW
+        uses: egor-tensin/setup-mingw@v2
+        with:
+          platform: ${{ matrix.arch }}
+          version: 12.2.0
+
+      - name: Get version
+        id: set_version
+        shell: bash
+        run: |
+          version=$(cat VERSION)
+          echo "version=$version" >> $GITHUB_OUTPUT
+
+      - name: Build library
+        run: |
+          cmake -DCMAKE_BUILD_TYPE=Release -B ./build_release
+          cmake --build ./build_release --config Release
+          tar -cvf stringzillite_windows_${{ matrix.arch }}_${{ steps.set_version.outputs.version }}.tar .\build_release\libstringzillite.dll .\include\stringzilla\stringzilla.h
+
+      - name: Upload archive
+        uses: xresloader/upload-to-github-release@v1
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+        with:
+          file: "stringzillite_windows_${{ matrix.arch }}_${{ steps.set_version.outputs.version }}.tar"
+          update_latest_release: true
+
+  create_macos_library:
+    name: Create Library for MacOS ${{ matrix.arch }}
+    runs-on: macos-12
+    needs: versioning
+    strategy:
+      fail-fast: false
+      matrix:
+        arch: [arm64, x86_64]
+    steps:
+      - uses: actions/checkout@v3
+        with:
+          persist-credentials: false
+          ref: "main"
+      - run: git submodule update --init --recursive
+
+      - uses: actions/setup-go@v4
+        with:
+          go-version: "^1.21.0"
+
+      - name: Get version
+        id: version
+        run: |
+          version=$(cat VERSION)
+          echo "version=$version" >> $GITHUB_OUTPUT
+
+      - name: Build library
+        run: |
+          cmake -DCMAKE_BUILD_TYPE=Release -B ./build_release
+          cmake --build ./build_release --config Release        
+          zip -r stringzillite_macOS_${{ matrix.arch }}_${{ steps.version.outputs.version }}.zip build_release/libstringzillite.dylib include/stringzilla/stringzilla.h
+
+      - name: Upload archive
+        uses: xresloader/upload-to-github-release@v1
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+        with:
+          file: "*.zip"
+          update_latest_release: true
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 0c36c8cc..2a8d7439 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -22,6 +22,12 @@ message(STATUS "C++ Compiler Version: ${CMAKE_CXX_COMPILER_VERSION}")
 message(STATUS "C++ Compiler: ${CMAKE_CXX_COMPILER}")
 message(STATUS "Build type: ${CMAKE_BUILD_TYPE}")
 
+if(CMAKE_SIZEOF_VOID_P EQUAL 8)
+  message(STATUS "Pointer size: 64-bit")
+else()
+  message(STATUS "Pointer size: 32-bit")
+endif()
+
 # Set a default build type to "Release" if none was specified
 if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
   message(STATUS "Setting build type to 'Release' as none was specified.")
@@ -273,4 +279,12 @@ if(${STRINGZILLA_BUILD_SHARED})
     SOVERSION 1
     POSITION_INDEPENDENT_CODE ON
     PUBLIC_HEADER include/stringzilla/stringzilla.h)
+
+  # Avoid built-ins on MSVC and other compilers, as that will cause compileration errors
+  target_compile_options(stringzillite PRIVATE
+    "$<$<CXX_COMPILER_ID:GNU,Clang>:-fno-builtin>"
+    "$<$<CXX_COMPILER_ID:MSVC>:/Oi->")
+  target_compile_options(stringzilla_shared PRIVATE
+    "$<$<CXX_COMPILER_ID:GNU,Clang>:-fno-builtin>"
+    "$<$<CXX_COMPILER_ID:MSVC>:/Oi->")
 endif()
\ No newline at end of file
diff --git a/README.md b/README.md
index 4bafcd1e..3c07735e 100644
--- a/README.md
+++ b/README.md
@@ -118,18 +118,18 @@ __Who is this for?__
   </tr>
   <!-- Characters, normal order -->
   <tr>
-    <td colspan="4" align="center">find the first occurrence of any of 6 whitespaces <sup>2</sup></td>
+    <td colspan="4" align="center">split lines separated by <code>\n</code> or <code>\r</code> <sup>2</sup></td>
   </tr>
   <tr>
     <td align="center">
       <code>strcspn</code> <sup>1</sup><br/>
-      <span style="color:#ABABAB;">x86:</span> <b>0.74</b> &centerdot;
-      <span style="color:#ABABAB;">arm:</span> <b>0.29</b> GB/s
+      <span style="color:#ABABAB;">x86:</span> <b>5.42</b> &centerdot;
+      <span style="color:#ABABAB;">arm:</span> <b>2.19</b> GB/s
     </td>
     <td align="center">
       <code>.find_first_of</code><br/>
-      <span style="color:#ABABAB;">x86:</span> <b>0.25</b> &centerdot;
-      <span style="color:#ABABAB;">arm:</span> <b>0.23</b> GB/s
+      <span style="color:#ABABAB;">x86:</span> <b>0.59</b> &centerdot;
+      <span style="color:#ABABAB;">arm:</span> <b>0.46</b> GB/s
     </td>
     <td align="center">
       <code>re.finditer</code><br/>
@@ -138,8 +138,8 @@ __Who is this for?__
     </td>
     <td align="center">
       <code>sz_find_charset</code><br/>
-      <span style="color:#ABABAB;">x86:</span> <b>0.43</b> &centerdot;
-      <span style="color:#ABABAB;">arm:</span> <b>0.23</b> GB/s
+      <span style="color:#ABABAB;">x86:</span> <b>4.08</b> &centerdot;
+      <span style="color:#ABABAB;">arm:</span> <b>3.22</b> GB/s
     </td>
   </tr>
   <!-- Characters, reverse order -->
@@ -270,7 +270,7 @@ Notably, if the CPU supports misaligned loads, even the 64-bit SWAR backends are
 > <sup>6</sup> Contrary to the popular opinion, Python's default `sorted` function works faster than the C and C++ standard libraries.
 > That holds for large lists or tuples of strings, but fails as soon as you need more complex logic, like sorting dictionaries by a string key, or producing the "sorted order" permutation.
 > The latter is very common in database engines and is most similar to `numpy.argsort`.
-> Despite being faster than the standard libraries, current StringZilla solution can be at least 4x faster without loss of generality.
+> Current StringZilla solution can be at least 4x faster without loss of generality.
 
 [faq-mersenne-twister]: https://en.wikipedia.org/wiki/Mersenne_Twister
 
@@ -398,10 +398,12 @@ Python's operations like `split()` and `readlines()` immediately materialize a `
 This can be very memory-inefficient for large datasets.
 StringZilla saves a lot of memory by viewing existing memory regions as substrings, but even more memory can be saved by using lazily evaluated iterators.
 
-- `text.split_iter(separator=' ', keepseparator=False) -> SplitIterator[Str]`
-- `text.rsplit_iter(separator=' ', keepseparator=False) -> SplitIterator[Str]`
-- `text.split_charset_iter(separator='chars', keepseparator=False) -> SplitIterator[Str]`
-- `text.rsplit_charset_iter(separator='chars', keepseparator=False) -> SplitIterator[Str]`
+```py
+x: SplitIterator[Str] = text.split_iter(separator=' ', keepseparator=False)
+x: SplitIterator[Str] = text.rsplit_iter(separator=' ', keepseparator=False)
+x: SplitIterator[Str] = text.split_charset_iter(separator='chars', keepseparator=False)
+x: SplitIterator[Str] = text.rsplit_charset_iter(separator='chars', keepseparator=False)
+```
 
 StringZilla can easily be 10x more memory efficient than native Python classes for tokenization.
 With lazy operations, it practically becomes free.
@@ -524,7 +526,7 @@ web_archieve.write_to("next_doc.html")
 A `Str` is easy to cast to [PyArrow](https://arrow.apache.org/docs/python/arrays.html#string-and-binary-types) buffers.
 
 ```py
-from pyarrow as foreign_buffer
+from pyarrow import foreign_buffer
 from stringzilla import Str
 
 original = "hello"
@@ -997,9 +999,20 @@ sz::randomize(sz::string_span(uuid, 36), "0123456789abcdef-"); // Overwrite any
 
 ### Levenshtein Edit Distance and Alignment Scores
 
+Levenshtein and Hamming edit distance are provided for both byte-strings and UTF-8 strings.
+The latter will output the distance in Unicode code points, not bytes.
+Needleman-Wunsch alignment scores are only defined for byte-strings.
+
 ```cpp
+// Count number of substitutions in same length strings
+sz::hamming_distance(first, second[, upper_bound]) -> std::size_t;
+sz::hamming_distance_utf8(first, second[, upper_bound]) -> std::size_t;
+
+// Count number of insertions, deletions and substitutions
 sz::edit_distance(first, second[, upper_bound[, allocator]]) -> std::size_t;
+sz::edit_distance_utf8(first, second[, upper_bound[, allocator]]) -> std::size_t;
 
+// Substitution-parametrized Needleman-Wunsch global alignment score
 std::int8_t costs[256][256]; // Substitution costs matrix
 sz::alignment_score(first, second, costs[, gap_score[, allocator]) -> std::ptrdiff_t;
 ```
@@ -1092,15 +1105,20 @@ __`SZ_USE_MISALIGNED_LOADS`__:
 > Going from `char`-like types to `uint64_t`-like ones can significantly accelerate the serial (SWAR) backend.
 > So consider enabling it if you are building for some embedded device.
 
-__`SZ_AVOID_LIBC`__:
+__`SZ_AVOID_LIBC`__ and __`SZ_OVERRIDE_LIBC`__:
 
 > When using the C header-only library one can disable the use of LibC.
 > This may affect the type resolution system on obscure hardware platforms. 
+> Moreover, one may let `stringzilla` override the common symbols like the `memcpy` and `memset` with its own implementations.
+> In that case you can use the [`LD_PRELOAD` trick][ld-preload-trick] to prioritize it's symbols over the ones from the LibC and accelerate existing string-heavy applications without recompiling them.
+
+[ld-preload-trick]: https://ashvardanian.com/posts/ld-preload-libsee
 
-__`SZ_AVOID_STL`__:
+__`SZ_AVOID_STL`__ and __`SZ_SAFETY_OVER_COMPATIBILITY`__:
 
-> When using the C++ interface one can disable conversions from `std::string` to `sz::string` and back.
+> When using the C++ interface one can disable implicit conversions from `std::string` to `sz::string` and back.
 > If not needed, the `<string>` and `<string_view>` headers will be excluded, reducing compilation time.
+> Moreover, if STL compatibility is a low priority, one can make the API safer by disabling the overloads, which are subjectively error prone.
 
 __`STRINGZILLA_BUILD_SHARED`, `STRINGZILLA_BUILD_TEST`, `STRINGZILLA_BUILD_BENCHMARK`, `STRINGZILLA_TARGET_ARCH`__ for CMake users:
 
diff --git a/c/lib.c b/c/lib.c
index bcfe7834..281716d7 100644
--- a/c/lib.c
+++ b/c/lib.c
@@ -9,6 +9,13 @@
 #include <windows.h> // `DllMain`
 #endif
 
+// When enabled, this library will override the symbols usually provided by the C standard library.
+// It's handy if you want to use the `LD_PRELOAD` trick for non-intrusive profiling and replacing
+// the C standard library implementation without recompiling.
+#if !defined(SZ_OVERRIDE_LIBC)
+#define SZ_OVERRIDE_LIBC SZ_AVOID_LIBC
+#endif
+
 // Overwrite `SZ_DYNAMIC_DISPATCH` before including StringZilla.
 #ifdef SZ_DYNAMIC_DISPATCH
 #undef SZ_DYNAMIC_DISPATCH
@@ -70,17 +77,19 @@ SZ_DYNAMIC sz_capability_t sz_capabilities(void) {
     // Check for GFNI (Function ID 1, ECX register)
     // https://github.com/llvm/llvm-project/blob/50598f0ff44f3a4e75706f8c53f3380fe7faa896/clang/lib/Headers/cpuid.h#L171C30-L171C40
     unsigned supports_avx512vbmi = (info1.named.ecx & 0x00000002) != 0;
+    unsigned supports_avx512vbmi2 = (info1.named.ecx & 0x00000040) != 0;
     // Check for GFNI (Function ID 1, ECX register)
     // https://github.com/llvm/llvm-project/blob/50598f0ff44f3a4e75706f8c53f3380fe7faa896/clang/lib/Headers/cpuid.h#L177C30-L177C40
     unsigned supports_gfni = (info1.named.ecx & 0x00000100) != 0;
 
-    return (sz_capability_t)(                             //
-        (sz_cap_x86_avx2_k * supports_avx2) |             //
-        (sz_cap_x86_avx512f_k * supports_avx512f) |       //
-        (sz_cap_x86_avx512vl_k * supports_avx512vl) |     //
-        (sz_cap_x86_avx512bw_k * supports_avx512bw) |     //
-        (sz_cap_x86_avx512vbmi_k * supports_avx512vbmi) | //
-        (sz_cap_x86_gfni_k * (supports_gfni)) |           //
+    return (sz_capability_t)(                               //
+        (sz_cap_x86_avx2_k * supports_avx2) |               //
+        (sz_cap_x86_avx512f_k * supports_avx512f) |         //
+        (sz_cap_x86_avx512vl_k * supports_avx512vl) |       //
+        (sz_cap_x86_avx512bw_k * supports_avx512bw) |       //
+        (sz_cap_x86_avx512vbmi_k * supports_avx512vbmi) |   //
+        (sz_cap_x86_avx512vbmi2_k * supports_avx512vbmi2) | //
+        (sz_cap_x86_gfni_k * (supports_gfni)) |             //
         (sz_cap_serial_k));
 
 #endif // SIMSIMD_TARGET_X86
@@ -160,6 +169,8 @@ static void sz_dispatch_table_init(void) {
         impl->rfind_byte = sz_rfind_byte_avx2;
         impl->find = sz_find_avx2;
         impl->rfind = sz_rfind_avx2;
+        impl->find_from_set = sz_find_charset_avx2;
+        impl->rfind_from_set = sz_rfind_charset_avx2;
     }
 #endif
 
@@ -179,7 +190,7 @@ static void sz_dispatch_table_init(void) {
         impl->edit_distance = sz_edit_distance_avx512;
     }
 
-    if ((caps & sz_cap_x86_avx512f_k) && (caps & sz_cap_x86_avx512vl_k) && (caps & sz_cap_x86_gfni_k) &&
+    if ((caps & sz_cap_x86_avx512f_k) && (caps & sz_cap_x86_avx512vl_k) && (caps & sz_cap_x86_avx512vbmi2_k) &&
         (caps & sz_cap_x86_avx512bw_k) && (caps & sz_cap_x86_avx512vbmi_k)) {
         impl->find_from_set = sz_find_charset_avx512;
         impl->rfind_from_set = sz_rfind_charset_avx512;
@@ -335,3 +346,43 @@ SZ_DYNAMIC void sz_generate(sz_cptr_t alphabet, sz_size_t alphabet_size, sz_ptr_
     if (!generator) generator = _sz_random_generator;
     sz_generate_serial(alphabet, alphabet_size, result, result_length, generator, generator_user_data);
 }
+
+// It's much harder to override the C standard library on Windows and MSVC,
+// so we'll just provide the symbols for other Operating Systems.
+#if SZ_OVERRIDE_LIBC && !(defined(_WIN32) || defined(__CYGWIN__))
+
+SZ_DYNAMIC void *memchr(void const *s, int c_wide, size_t n) {
+    sz_u8_t c = (sz_u8_t)c_wide;
+    return (void *)sz_find_byte(s, n, (sz_cptr_t)&c);
+}
+
+SZ_DYNAMIC void *memcpy(void *dest, void const *src, size_t n) {
+    sz_copy(dest, src, n);
+    return (void *)dest;
+}
+
+SZ_DYNAMIC void *memmove(void *dest, void const *src, size_t n) {
+    sz_move(dest, src, n);
+    return (void *)dest;
+}
+
+SZ_DYNAMIC void *memset(void *s, int c, size_t n) {
+    sz_fill(s, n, c);
+    return (void *)s;
+}
+
+SZ_DYNAMIC void *memmem(void const *h, size_t h_len, void const *n, size_t n_len) {
+    return (void *)sz_find(h, h_len, n, n_len);
+}
+
+SZ_DYNAMIC void *memrchr(void const *s, int c_wide, size_t n) {
+    sz_u8_t c = (sz_u8_t)c_wide;
+    return (void *)sz_rfind_byte(s, n, (sz_cptr_t)&c);
+}
+
+SZ_DYNAMIC void memfrob(void *s, size_t n) {
+    char const *base64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+    sz_generate(base64, 64, s, n, SZ_NULL, SZ_NULL);
+}
+
+#endif // SZ_OVERRIDE_LIBC
diff --git a/include/stringzilla/experimental.h b/include/stringzilla/experimental.h
index b20de5bb..b769c0f5 100644
--- a/include/stringzilla/experimental.h
+++ b/include/stringzilla/experimental.h
@@ -364,6 +364,116 @@ sz_u512_vec_t sz_inclusive_min(sz_i32_t previous, sz_error_cost_t gap, sz_u512_v
     return new_vec;
 }
 
+SZ_PUBLIC sz_cptr_t sz_find_charset_avx512(sz_cptr_t text, sz_size_t length, sz_charset_t const *filter) {
+
+    sz_size_t load_length;
+    __mmask32 load_mask, matches_mask;
+    // To store the set in the register we need just 256 bits, but the `VPERMB` instruction
+    // we are going to invoke is surprisingly cheaper on ZMM registers.
+    sz_u512_vec_t text_vec, filter_vec;
+    filter_vec.ymms[0] = _mm256_loadu_epi64(&filter->_u64s[0]);
+
+    // We are going to view the `filter` at 8-bit word granularity.
+    sz_u512_vec_t filter_slice_offsets_vec;
+    sz_u512_vec_t filter_slice_vec;
+    sz_u512_vec_t offset_within_slice_vec;
+    sz_u512_vec_t mask_in_filter_slice_vec;
+    sz_u512_vec_t matches_vec;
+
+    while (length) {
+        // For every byte:
+        // 1. Find corresponding word in a set.
+        // 2. Produce a bitmask to check against that word.
+        load_length = sz_min_of_two(length, 32);
+        load_mask = _sz_u64_mask_until(load_length);
+        text_vec.ymms[0] = _mm256_maskz_loadu_epi8(load_mask, text);
+
+        // To shift right every byte by 3 bits we can use the GF2 affine transformations.
+        // https://wunkolo.github.io/post/2020/11/gf2p8affineqb-int8-shifting/
+        // After next line, all 8-bit offsets in the `filter_slice_offsets_vec` should be under 32.
+        filter_slice_offsets_vec.ymms[0] =
+            _mm256_gf2p8affine_epi64_epi8(text_vec.ymms[0], _mm256_set1_epi64x(0x0102040810204080ull << (3 * 8)), 0);
+
+        // After next line, `filter_slice_vec` will contain the right word from the set,
+        // needed to filter the presence of the byte in the set.
+        filter_slice_vec.ymms[0] = _mm256_permutexvar_epi8(filter_slice_offsets_vec.ymms[0], filter_vec.ymms[0]);
+
+        // After next line, all 8-bit offsets in the `filter_slice_offsets_vec` should be under 8.
+        offset_within_slice_vec.ymms[0] = _mm256_and_si256(text_vec.ymms[0], _mm256_set1_epi64x(0x0707070707070707ull));
+
+        // Instead of performing one more Galois Field operation, we can upcast to 16-bit integers,
+        // and perform the fift and intersection there.
+        filter_slice_vec.zmm = _mm512_cvtepi8_epi16(filter_slice_vec.ymms[0]);
+        offset_within_slice_vec.zmm = _mm512_cvtepi8_epi16(offset_within_slice_vec.ymms[0]);
+        mask_in_filter_slice_vec.zmm = _mm512_sllv_epi16(_mm512_set1_epi16(1), offset_within_slice_vec.zmm);
+        matches_vec.zmm = _mm512_and_si512(filter_slice_vec.zmm, mask_in_filter_slice_vec.zmm);
+
+        matches_mask = _mm512_mask_cmpneq_epi16_mask(load_mask, matches_vec.zmm, _mm512_setzero_si512());
+        if (matches_mask) {
+            int offset = sz_u32_ctz(matches_mask);
+            return text + offset;
+        }
+        else { text += load_length, length -= load_length; }
+    }
+
+    return SZ_NULL_CHAR;
+}
+
+SZ_PUBLIC sz_cptr_t sz_rfind_charset_avx512(sz_cptr_t text, sz_size_t length, sz_charset_t const *filter) {
+
+    sz_size_t load_length;
+    __mmask32 load_mask, matches_mask;
+    // To store the set in the register we need just 256 bits, but the `VPERMB` instruction
+    // we are going to invoke is surprisingly cheaper on ZMM registers.
+    sz_u512_vec_t text_vec, filter_vec;
+    filter_vec.ymms[0] = _mm256_loadu_epi64(&filter->_u64s[0]);
+
+    // We are going to view the `filter` at 8-bit word granularity.
+    sz_u512_vec_t filter_slice_offsets_vec;
+    sz_u512_vec_t filter_slice_vec;
+    sz_u512_vec_t offset_within_slice_vec;
+    sz_u512_vec_t mask_in_filter_slice_vec;
+    sz_u512_vec_t matches_vec;
+
+    while (length) {
+        // For every byte:
+        // 1. Find corresponding word in a set.
+        // 2. Produce a bitmask to check against that word.
+        load_length = sz_min_of_two(length, 32);
+        load_mask = _sz_u64_mask_until(load_length);
+        text_vec.ymms[0] = _mm256_maskz_loadu_epi8(load_mask, text + length - load_length);
+
+        // To shift right every byte by 3 bits we can use the GF2 affine transformations.
+        // https://wunkolo.github.io/post/2020/11/gf2p8affineqb-int8-shifting/
+        // After next line, all 8-bit offsets in the `filter_slice_offsets_vec` should be under 32.
+        filter_slice_offsets_vec.ymms[0] =
+            _mm256_gf2p8affine_epi64_epi8(text_vec.ymms[0], _mm256_set1_epi64x(0x0102040810204080ull << (3 * 8)), 0);
+
+        // After next line, `filter_slice_vec` will contain the right word from the set,
+        // needed to filter the presence of the byte in the set.
+        filter_slice_vec.ymms[0] = _mm256_permutexvar_epi8(filter_slice_offsets_vec.ymms[0], filter_vec.ymms[0]);
+
+        // After next line, all 8-bit offsets in the `filter_slice_offsets_vec` should be under 8.
+        offset_within_slice_vec.ymms[0] = _mm256_and_si256(text_vec.ymms[0], _mm256_set1_epi64x(0x0707070707070707ull));
+
+        // Instead of performing one more Galois Field operation, we can upcast to 16-bit integers,
+        // and perform the fift and intersection there.
+        filter_slice_vec.zmm = _mm512_cvtepi8_epi16(filter_slice_vec.ymms[0]);
+        offset_within_slice_vec.zmm = _mm512_cvtepi8_epi16(offset_within_slice_vec.ymms[0]);
+        mask_in_filter_slice_vec.zmm = _mm512_sllv_epi16(_mm512_set1_epi16(1), offset_within_slice_vec.zmm);
+        matches_vec.zmm = _mm512_and_si512(filter_slice_vec.zmm, mask_in_filter_slice_vec.zmm);
+
+        matches_mask = _mm512_mask_cmpneq_epi16_mask(load_mask, matches_vec.zmm, _mm512_setzero_si512());
+        if (matches_mask) {
+            int offset = sz_u32_clz(matches_mask);
+            return text + length - load_length + 32 - offset - 1;
+        }
+        else { length -= load_length; }
+    }
+
+    return SZ_NULL_CHAR;
+}
+
 #endif // SZ_USE_AVX512
 
 #if SZ_USE_ARM_NEON
diff --git a/include/stringzilla/stringzilla.h b/include/stringzilla/stringzilla.h
index 1b4d3e28..eee65782 100644
--- a/include/stringzilla/stringzilla.h
+++ b/include/stringzilla/stringzilla.h
@@ -177,6 +177,22 @@ typedef unsigned int sz_u32_t;       // Always 32 bits
 typedef long long sz_i64_t;          // Always 64 bits
 typedef unsigned long long sz_u64_t; // Always 64 bits
 
+// Now we need to redefine the `size_t`.
+// Microsoft Visual C++ (MSVC) typically follows LLP64 data model on 64-bit platforms,
+// where integers, pointers, and long types have different sizes:
+//
+//  > `int` is 32 bits
+//  > `long` is 32 bits
+//  > `long long` is 64 bits
+//  > pointer (thus, `size_t`) is 64 bits
+//
+// In contrast, GCC and Clang on 64-bit Unix-like systems typically follow the LP64 model, where:
+//
+//  > `int` is 32 bits
+//  > `long` and pointer (thus, `size_t`) are 64 bits
+//  > `long long` is also 64 bits
+//
+// Source: https://learn.microsoft.com/en-us/windows/win32/winprog64/abstract-data-models
 #if SZ_DETECT_64_BIT
 typedef unsigned long long sz_size_t; // 64-bit.
 typedef long long sz_ssize_t;         // 64-bit.
@@ -235,6 +251,8 @@ typedef enum sz_capability_t {
     sz_cap_x86_avx512vbmi_k = 1 << 24, /// x86 AVX512 VBMI instruction capability
     sz_cap_x86_gfni_k = 1 << 25,       /// x86 AVX512 GFNI instruction capability
 
+    sz_cap_x86_avx512vbmi2_k = 1 << 26, /// x86 AVX512 VBMI 2 instruction capability
+
 } sz_capability_t;
 
 /**
@@ -719,9 +737,15 @@ SZ_PUBLIC sz_cptr_t sz_rfind_serial(sz_cptr_t haystack, sz_size_t h_length, sz_c
  *          Equivalent to `strspn(text, accepted)` and `strcspn(text, rejected)` in LibC.
  *          May have identical implementation and performance to ::sz_rfind_charset.
  *
- *  @param text     String to be trimmed.
- *  @param accepted Set of accepted characters.
- *  @return         Number of bytes forming the prefix.
+ *  Useful for parsing, when we want to skip a set of characters. Examples:
+ *  * 6 whitespaces: " \t\n\r\v\f".
+ *  * 16 digits forming a float number: "0123456789,.eE+-".
+ *  * 5 HTML reserved characters: "\"'&<>", of which "<>" can be useful for parsing.
+ *  * 2 JSON string special characters useful to locate the end of the string: "\"\\".
+ *
+ *  @param text     String to be scanned.
+ *  @param set      Set of relevant characters.
+ *  @return         Pointer to the first matching character from ::set.
  */
 SZ_DYNAMIC sz_cptr_t sz_find_charset(sz_cptr_t text, sz_size_t length, sz_charset_t const *set);
 
@@ -739,9 +763,9 @@ SZ_PUBLIC sz_cptr_t sz_find_charset_serial(sz_cptr_t text, sz_size_t length, sz_
  *  * 5 HTML reserved characters: "\"'&<>", of which "<>" can be useful for parsing.
  *  * 2 JSON string special characters useful to locate the end of the string: "\"\\".
  *
- *  @param text     String to be trimmed.
- *  @param rejected Set of rejected characters.
- *  @return         Number of bytes forming the prefix.
+ *  @param text     String to be scanned.
+ *  @param set      Set of relevant characters.
+ *  @return         Pointer to the last matching character from ::set.
  */
 SZ_DYNAMIC sz_cptr_t sz_rfind_charset(sz_cptr_t text, sz_size_t length, sz_charset_t const *set);
 
@@ -1123,7 +1147,7 @@ SZ_PUBLIC void sz_sort_intro(sz_sequence_t *sequence, sz_sequence_comparator_t l
 #include <arm_sve.h>
 #endif // SZ_USE_ARM_SVE
 
-#pragma region Hardware-Specific API
+#pragma region Hardware - Specific API
 
 #if SZ_USE_X86_AVX512
 
@@ -1265,7 +1289,7 @@ SZ_PUBLIC sz_cptr_t sz_rfind_charset_neon(sz_cptr_t text, sz_size_t length, sz_c
         }                                                                                                   \
     } while (0)
 #else
-#define sz_assert(condition) ((void)0)
+#define sz_assert(condition) ((void)(condition))
 #endif
 
 /*  Intrinsics aliases for MSVC, GCC, Clang, and Clang-Cl.
@@ -3306,7 +3330,7 @@ SZ_PUBLIC void sz_fill_serial(sz_ptr_t target, sz_size_t length, sz_u8_t value)
 
     // In case of long strings, skip unaligned bytes, and then fill the rest in 64-bit chunks.
     else {
-        sz_u64_t value64 = (sz_u64_t)(value) * 0x0101010101010101ull;
+        sz_u64_t value64 = (sz_u64_t)(value)*0x0101010101010101ull;
         while ((sz_size_t)target & 7ull) *(target++) = value;
         while (target + 8 <= end) *(sz_u64_t *)target = value64, target += 8;
         while (target != end) *(target++) = value;
@@ -3785,6 +3809,97 @@ SZ_PUBLIC sz_cptr_t sz_rfind_avx2(sz_cptr_t h, sz_size_t h_length, sz_cptr_t n,
     return sz_rfind_serial(h, h_length, n, n_length);
 }
 
+SZ_PUBLIC sz_cptr_t sz_find_charset_avx2(sz_cptr_t text, sz_size_t length, sz_charset_t const *filter) {
+
+    // Let's unzip even and odd elements and replicate them into both lanes of the YMM register.
+    // That way when we invoke `_mm256_shuffle_epi8` we can use the same mask for both lanes.
+    sz_u256_vec_t filter_even_vec, filter_odd_vec;
+    for (sz_size_t i = 0; i != 16; ++i)
+        filter_even_vec.u8s[i] = filter->_u8s[i * 2], filter_odd_vec.u8s[i] = filter->_u8s[i * 2 + 1];
+    filter_even_vec.xmms[1] = filter_even_vec.xmms[0];
+    filter_odd_vec.xmms[1] = filter_odd_vec.xmms[0];
+
+    sz_u256_vec_t text_vec;
+    sz_u256_vec_t matches_vec;
+    sz_u256_vec_t lower_nibbles_vec, higher_nibbles_vec;
+    sz_u256_vec_t bitset_even_vec, bitset_odd_vec;
+    sz_u256_vec_t bitmask_vec, bitmask_lookup_vec;
+    bitmask_lookup_vec.ymm = _mm256_set_epi8(-128, 64, 32, 16, 8, 4, 2, 1, -128, 64, 32, 16, 8, 4, 2, 1, //
+                                             -128, 64, 32, 16, 8, 4, 2, 1, -128, 64, 32, 16, 8, 4, 2, 1);
+
+    while (length >= 32) {
+        // The following algorithm is a transposed equivalent of the "SIMDized check which bytes are in a set"
+        // solutions by Wojciech Muła. We populate the bitmask differently and target newer CPUs, so
+        // StrinZilla uses a somewhat different approach.
+        // http://0x80.pl/articles/simd-byte-lookup.html#alternative-implementation-new
+        //
+        //      sz_u8_t input = *(sz_u8_t const *)text;
+        //      sz_u8_t lo_nibble = input & 0x0f;
+        //      sz_u8_t hi_nibble = input >> 4;
+        //      sz_u8_t bitset_even = filter_even_vec.u8s[hi_nibble];
+        //      sz_u8_t bitset_odd = filter_odd_vec.u8s[hi_nibble];
+        //      sz_u8_t bitmask = (1 << (lo_nibble & 0x7));
+        //      sz_u8_t bitset = lo_nibble < 8 ? bitset_even : bitset_odd;
+        //      if ((bitset & bitmask) != 0) return text;
+        //      else { length--, text++; }
+        //
+        // The nice part about this, loading the strided data is vey easy with Arm NEON,
+        // while with x86 CPUs after AVX, shuffles within 256 bits shouldn't be an issue either.
+        text_vec.ymm = _mm256_lddqu_si256((__m256i const *)text);
+        lower_nibbles_vec.ymm = _mm256_and_si256(text_vec.ymm, _mm256_set1_epi8(0x0f));
+        bitmask_vec.ymm = _mm256_shuffle_epi8(bitmask_lookup_vec.ymm, lower_nibbles_vec.ymm);
+        //
+        // At this point we can validate the `bitmask_vec` contents like this:
+        //
+        //      for (sz_size_t i = 0; i != 32; ++i) {
+        //          sz_u8_t input = *(sz_u8_t const *)(text + i);
+        //          sz_u8_t lo_nibble = input & 0x0f;
+        //          sz_u8_t bitmask = (1 << (lo_nibble & 0x7));
+        //          sz_assert(bitmask_vec.u8s[i] == bitmask);
+        //      }
+        //
+        // Shift right every byte by 4 bits.
+        // There is no `_mm256_srli_epi8` intrinsic, so we have to use `_mm256_srli_epi16`
+        // and combine it with a mask to clear the higher bits.
+        higher_nibbles_vec.ymm = _mm256_and_si256(_mm256_srli_epi16(text_vec.ymm, 4), _mm256_set1_epi8(0x0f));
+        bitset_even_vec.ymm = _mm256_shuffle_epi8(filter_even_vec.ymm, higher_nibbles_vec.ymm);
+        bitset_odd_vec.ymm = _mm256_shuffle_epi8(filter_odd_vec.ymm, higher_nibbles_vec.ymm);
+        //
+        // At this point we can validate the `bitset_even_vec` and `bitset_odd_vec` contents like this:
+        //
+        //      for (sz_size_t i = 0; i != 32; ++i) {
+        //          sz_u8_t input = *(sz_u8_t const *)(text + i);
+        //          sz_u8_t const *bitset_ptr = &filter->_u8s[0];
+        //          sz_u8_t hi_nibble = input >> 4;
+        //          sz_u8_t bitset_even = bitset_ptr[hi_nibble * 2];
+        //          sz_u8_t bitset_odd = bitset_ptr[hi_nibble * 2 + 1];
+        //          sz_assert(bitset_even_vec.u8s[i] == bitset_even);
+        //          sz_assert(bitset_odd_vec.u8s[i] == bitset_odd);
+        //      }
+        //
+        __m256i take_first = _mm256_cmpgt_epi8(_mm256_set1_epi8(8), lower_nibbles_vec.ymm);
+        bitset_even_vec.ymm = _mm256_blendv_epi8(bitset_odd_vec.ymm, bitset_even_vec.ymm, take_first);
+
+        // It would have been great to have an instruction that tests the bits and then broadcasts
+        // the matching bit into all bits in that byte. But we don't have that, so we have to
+        // `and`, `cmpeq`, `movemask`, and then invert at the end...
+        matches_vec.ymm = _mm256_and_si256(bitset_even_vec.ymm, bitmask_vec.ymm);
+        matches_vec.ymm = _mm256_cmpeq_epi8(matches_vec.ymm, _mm256_setzero_si256());
+        int matches_mask = ~_mm256_movemask_epi8(matches_vec.ymm);
+        if (matches_mask) {
+            int offset = sz_u32_ctz(matches_mask);
+            return text + offset;
+        }
+        else { text += 32, length -= 32; }
+    }
+
+    return sz_find_charset_serial(text, length, filter);
+}
+
+SZ_PUBLIC sz_cptr_t sz_rfind_charset_avx2(sz_cptr_t text, sz_size_t length, sz_charset_t const *filter) {
+    return sz_rfind_charset_serial(text, length, filter);
+}
+
 /**
  *  @brief  There is no AVX2 instruction for fast multiplication of 64-bit integers.
  *          This implementation is coming from Agner Fog's Vector Class Library.
@@ -3935,7 +4050,7 @@ SZ_PUBLIC void sz_hashes_avx2(sz_cptr_t start, sz_size_t length, sz_size_t windo
  *  * 2019 IceLake: VPOPCNTDQ, VNNI, VBMI2, BITALG, GFNI, VPCLMULQDQ, VAES
  *  * 2020 TigerLake: VP2INTERSECT
  */
-#pragma region AVX-512 Implementation
+#pragma region AVX - 512 Implementation
 
 #if SZ_USE_X86_AVX512
 #pragma GCC push_options
@@ -4539,57 +4654,116 @@ SZ_PUBLIC void sz_hashes_avx512(sz_cptr_t start, sz_size_t length, sz_size_t win
 #pragma GCC pop_options
 
 #pragma GCC push_options
-#pragma GCC target("avx", "avx512f", "avx512vl", "avx512bw", "avx512vbmi", "bmi", "bmi2", "gfni")
-#pragma clang attribute push(__attribute__((target("avx,avx512f,avx512vl,avx512bw,avx512vbmi,bmi,bmi2,gfni"))), \
+#pragma GCC target("avx", "avx512f", "avx512vl", "avx512bw", "avx512vbmi", "avx512vbmi2", "bmi", "bmi2")
+#pragma clang attribute push(__attribute__((target("avx,avx512f,avx512vl,avx512bw,avx512vbmi,avx512vbmi2,bmi,bmi2"))), \
                              apply_to = function)
 
 SZ_PUBLIC sz_cptr_t sz_find_charset_avx512(sz_cptr_t text, sz_size_t length, sz_charset_t const *filter) {
 
-    sz_size_t load_length;
-    __mmask32 load_mask, matches_mask;
-    // To store the set in the register we need just 256 bits, but the `VPERMB` instruction
-    // we are going to invoke is surprisingly cheaper on ZMM registers.
-    sz_u512_vec_t text_vec, filter_vec;
-    filter_vec.ymms[0] = _mm256_loadu_epi64(&filter->_u64s[0]);
-
-    // We are going to view the `filter` at 8-bit word granularity.
-    sz_u512_vec_t filter_slice_offsets_vec;
-    sz_u512_vec_t filter_slice_vec;
-    sz_u512_vec_t offset_within_slice_vec;
-    sz_u512_vec_t mask_in_filter_slice_vec;
-    sz_u512_vec_t matches_vec;
+    // Before initializing the AVX-512 vectors, we may want to run the sequential code for the first few bytes.
+    // In practice, that only hurts, even when we have matches every 5-ish bytes.
+    //
+    //      if (length < SZ_SWAR_THRESHOLD) return sz_find_charset_serial(text, length, filter);
+    //      sz_cptr_t early_result = sz_find_charset_serial(text, SZ_SWAR_THRESHOLD, filter);
+    //      if (early_result) return early_result;
+    //      text += SZ_SWAR_THRESHOLD;
+    //      length -= SZ_SWAR_THRESHOLD;
+    //
+    // Let's unzip even and odd elements and replicate them into both lanes of the YMM register.
+    // That way when we invoke `_mm512_shuffle_epi8` we can use the same mask for both lanes.
+    sz_u512_vec_t filter_even_vec, filter_odd_vec;
+    __m256i filter_ymm = _mm256_loadu_si256((__m256i const *)filter);
+    // There are a few way to initialize filters without having native strided loads.
+    // In the cronological order of experiments:
+    // - serial code initializing 128 bytes of odd and even mask
+    // - using several shuffles
+    // - using `_mm512_permutexvar_epi8`
+    // - using `_mm512_broadcast_i32x4(_mm256_castsi256_si128(_mm256_maskz_compress_epi8(0x55555555, filter_ymm)))`
+    //   and `_mm512_broadcast_i32x4(_mm256_castsi256_si128(_mm256_maskz_compress_epi8(0xaaaaaaaa, filter_ymm)))`
+    filter_even_vec.zmm = _mm512_broadcast_i32x4(_mm256_castsi256_si128( // broadcast __m128i to __m512i
+        _mm256_maskz_compress_epi8(0x55555555, filter_ymm)));
+    filter_odd_vec.zmm = _mm512_broadcast_i32x4(_mm256_castsi256_si128( // broadcast __m128i to __m512i
+        _mm256_maskz_compress_epi8(0xaaaaaaaa, filter_ymm)));
+    // After the unzipping operation, we can validate the contents of the vectors like this:
+    //
+    //      for (sz_size_t i = 0; i != 16; ++i) {
+    //          sz_assert(filter_even_vec.u8s[i] == filter->_u8s[i * 2]);
+    //          sz_assert(filter_odd_vec.u8s[i] == filter->_u8s[i * 2 + 1]);
+    //          sz_assert(filter_even_vec.u8s[i + 16] == filter->_u8s[i * 2]);
+    //          sz_assert(filter_odd_vec.u8s[i + 16] == filter->_u8s[i * 2 + 1]);
+    //          sz_assert(filter_even_vec.u8s[i + 32] == filter->_u8s[i * 2]);
+    //          sz_assert(filter_odd_vec.u8s[i + 32] == filter->_u8s[i * 2 + 1]);
+    //          sz_assert(filter_even_vec.u8s[i + 48] == filter->_u8s[i * 2]);
+    //          sz_assert(filter_odd_vec.u8s[i + 48] == filter->_u8s[i * 2 + 1]);
+    //      }
+    //
+    sz_u512_vec_t text_vec;
+    sz_u512_vec_t lower_nibbles_vec, higher_nibbles_vec;
+    sz_u512_vec_t bitset_even_vec, bitset_odd_vec;
+    sz_u512_vec_t bitmask_vec, bitmask_lookup_vec;
+    bitmask_lookup_vec.zmm = _mm512_set_epi8(-128, 64, 32, 16, 8, 4, 2, 1, -128, 64, 32, 16, 8, 4, 2, 1, //
+                                             -128, 64, 32, 16, 8, 4, 2, 1, -128, 64, 32, 16, 8, 4, 2, 1, //
+                                             -128, 64, 32, 16, 8, 4, 2, 1, -128, 64, 32, 16, 8, 4, 2, 1, //
+                                             -128, 64, 32, 16, 8, 4, 2, 1, -128, 64, 32, 16, 8, 4, 2, 1);
 
     while (length) {
-        // For every byte:
-        // 1. Find corresponding word in a set.
-        // 2. Produce a bitmask to check against that word.
-        load_length = sz_min_of_two(length, 32);
-        load_mask = _sz_u64_mask_until(load_length);
-        text_vec.ymms[0] = _mm256_maskz_loadu_epi8(load_mask, text);
-
-        // To shift right every byte by 3 bits we can use the GF2 affine transformations.
-        // https://wunkolo.github.io/post/2020/11/gf2p8affineqb-int8-shifting/
-        // After next line, all 8-bit offsets in the `filter_slice_offsets_vec` should be under 32.
-        filter_slice_offsets_vec.ymms[0] =
-            _mm256_gf2p8affine_epi64_epi8(text_vec.ymms[0], _mm256_set1_epi64x(0x0102040810204080ull << (3 * 8)), 0);
-
-        // After next line, `filter_slice_vec` will contain the right word from the set,
-        // needed to filter the presence of the byte in the set.
-        filter_slice_vec.ymms[0] = _mm256_permutexvar_epi8(filter_slice_offsets_vec.ymms[0], filter_vec.ymms[0]);
-
-        // After next line, all 8-bit offsets in the `filter_slice_offsets_vec` should be under 8.
-        offset_within_slice_vec.ymms[0] = _mm256_and_si256(text_vec.ymms[0], _mm256_set1_epi64x(0x0707070707070707ull));
-
-        // Instead of performing one more Galois Field operation, we can upcast to 16-bit integers,
-        // and perform the fift and intersection there.
-        filter_slice_vec.zmm = _mm512_cvtepi8_epi16(filter_slice_vec.ymms[0]);
-        offset_within_slice_vec.zmm = _mm512_cvtepi8_epi16(offset_within_slice_vec.ymms[0]);
-        mask_in_filter_slice_vec.zmm = _mm512_sllv_epi16(_mm512_set1_epi16(1), offset_within_slice_vec.zmm);
-        matches_vec.zmm = _mm512_and_si512(filter_slice_vec.zmm, mask_in_filter_slice_vec.zmm);
-
-        matches_mask = _mm512_mask_cmpneq_epi16_mask(load_mask, matches_vec.zmm, _mm512_setzero_si512());
+        // The following algorithm is a transposed equivalent of the "SIMDized check which bytes are in a set"
+        // solutions by Wojciech Muła. We populate the bitmask differently and target newer CPUs, so
+        // StrinZilla uses a somewhat different approach.
+        // http://0x80.pl/articles/simd-byte-lookup.html#alternative-implementation-new
+        //
+        //      sz_u8_t input = *(sz_u8_t const *)text;
+        //      sz_u8_t lo_nibble = input & 0x0f;
+        //      sz_u8_t hi_nibble = input >> 4;
+        //      sz_u8_t bitset_even = filter_even_vec.u8s[hi_nibble];
+        //      sz_u8_t bitset_odd = filter_odd_vec.u8s[hi_nibble];
+        //      sz_u8_t bitmask = (1 << (lo_nibble & 0x7));
+        //      sz_u8_t bitset = lo_nibble < 8 ? bitset_even : bitset_odd;
+        //      if ((bitset & bitmask) != 0) return text;
+        //      else { length--, text++; }
+        //
+        // The nice part about this, loading the strided data is vey easy with Arm NEON,
+        // while with x86 CPUs after AVX, shuffles within 256 bits shouldn't be an issue either.
+        sz_size_t load_length = sz_min_of_two(length, 64);
+        __mmask64 load_mask = _sz_u64_mask_until(load_length);
+        text_vec.zmm = _mm512_maskz_loadu_epi8(load_mask, text);
+        lower_nibbles_vec.zmm = _mm512_and_si512(text_vec.zmm, _mm512_set1_epi8(0x0f));
+        bitmask_vec.zmm = _mm512_shuffle_epi8(bitmask_lookup_vec.zmm, lower_nibbles_vec.zmm);
+        //
+        // At this point we can validate the `bitmask_vec` contents like this:
+        //
+        //      for (sz_size_t i = 0; i != load_length; ++i) {
+        //          sz_u8_t input = *(sz_u8_t const *)(text + i);
+        //          sz_u8_t lo_nibble = input & 0x0f;
+        //          sz_u8_t bitmask = (1 << (lo_nibble & 0x7));
+        //          sz_assert(bitmask_vec.u8s[i] == bitmask);
+        //      }
+        //
+        // Shift right every byte by 4 bits.
+        // There is no `_mm512_srli_epi8` intrinsic, so we have to use `_mm512_srli_epi16`
+        // and combine it with a mask to clear the higher bits.
+        higher_nibbles_vec.zmm = _mm512_and_si512(_mm512_srli_epi16(text_vec.zmm, 4), _mm512_set1_epi8(0x0f));
+        bitset_even_vec.zmm = _mm512_shuffle_epi8(filter_even_vec.zmm, higher_nibbles_vec.zmm);
+        bitset_odd_vec.zmm = _mm512_shuffle_epi8(filter_odd_vec.zmm, higher_nibbles_vec.zmm);
+        //
+        // At this point we can validate the `bitset_even_vec` and `bitset_odd_vec` contents like this:
+        //
+        //      for (sz_size_t i = 0; i != load_length; ++i) {
+        //          sz_u8_t input = *(sz_u8_t const *)(text + i);
+        //          sz_u8_t const *bitset_ptr = &filter->_u8s[0];
+        //          sz_u8_t hi_nibble = input >> 4;
+        //          sz_u8_t bitset_even = bitset_ptr[hi_nibble * 2];
+        //          sz_u8_t bitset_odd = bitset_ptr[hi_nibble * 2 + 1];
+        //          sz_assert(bitset_even_vec.u8s[i] == bitset_even);
+        //          sz_assert(bitset_odd_vec.u8s[i] == bitset_odd);
+        //      }
+        //
+        // TODO: Is this a good place for ternary logic?
+        __mmask64 take_first = _mm512_cmplt_epi8_mask(lower_nibbles_vec.zmm, _mm512_set1_epi8(8));
+        bitset_even_vec.zmm = _mm512_mask_blend_epi8(take_first, bitset_odd_vec.zmm, bitset_even_vec.zmm);
+        __mmask64 matches_mask = _mm512_mask_test_epi8_mask(load_mask, bitset_even_vec.zmm, bitmask_vec.zmm);
         if (matches_mask) {
-            int offset = sz_u32_ctz(matches_mask);
+            int offset = sz_u64_ctz(matches_mask);
             return text + offset;
         }
         else { text += load_length, length -= load_length; }
@@ -4599,58 +4773,7 @@ SZ_PUBLIC sz_cptr_t sz_find_charset_avx512(sz_cptr_t text, sz_size_t length, sz_
 }
 
 SZ_PUBLIC sz_cptr_t sz_rfind_charset_avx512(sz_cptr_t text, sz_size_t length, sz_charset_t const *filter) {
-
-    sz_size_t load_length;
-    __mmask32 load_mask, matches_mask;
-    // To store the set in the register we need just 256 bits, but the `VPERMB` instruction
-    // we are going to invoke is surprisingly cheaper on ZMM registers.
-    sz_u512_vec_t text_vec, filter_vec;
-    filter_vec.ymms[0] = _mm256_loadu_epi64(&filter->_u64s[0]);
-
-    // We are going to view the `filter` at 8-bit word granularity.
-    sz_u512_vec_t filter_slice_offsets_vec;
-    sz_u512_vec_t filter_slice_vec;
-    sz_u512_vec_t offset_within_slice_vec;
-    sz_u512_vec_t mask_in_filter_slice_vec;
-    sz_u512_vec_t matches_vec;
-
-    while (length) {
-        // For every byte:
-        // 1. Find corresponding word in a set.
-        // 2. Produce a bitmask to check against that word.
-        load_length = sz_min_of_two(length, 32);
-        load_mask = _sz_u64_mask_until(load_length);
-        text_vec.ymms[0] = _mm256_maskz_loadu_epi8(load_mask, text + length - load_length);
-
-        // To shift right every byte by 3 bits we can use the GF2 affine transformations.
-        // https://wunkolo.github.io/post/2020/11/gf2p8affineqb-int8-shifting/
-        // After next line, all 8-bit offsets in the `filter_slice_offsets_vec` should be under 32.
-        filter_slice_offsets_vec.ymms[0] =
-            _mm256_gf2p8affine_epi64_epi8(text_vec.ymms[0], _mm256_set1_epi64x(0x0102040810204080ull << (3 * 8)), 0);
-
-        // After next line, `filter_slice_vec` will contain the right word from the set,
-        // needed to filter the presence of the byte in the set.
-        filter_slice_vec.ymms[0] = _mm256_permutexvar_epi8(filter_slice_offsets_vec.ymms[0], filter_vec.ymms[0]);
-
-        // After next line, all 8-bit offsets in the `filter_slice_offsets_vec` should be under 8.
-        offset_within_slice_vec.ymms[0] = _mm256_and_si256(text_vec.ymms[0], _mm256_set1_epi64x(0x0707070707070707ull));
-
-        // Instead of performing one more Galois Field operation, we can upcast to 16-bit integers,
-        // and perform the fift and intersection there.
-        filter_slice_vec.zmm = _mm512_cvtepi8_epi16(filter_slice_vec.ymms[0]);
-        offset_within_slice_vec.zmm = _mm512_cvtepi8_epi16(offset_within_slice_vec.ymms[0]);
-        mask_in_filter_slice_vec.zmm = _mm512_sllv_epi16(_mm512_set1_epi16(1), offset_within_slice_vec.zmm);
-        matches_vec.zmm = _mm512_and_si512(filter_slice_vec.zmm, mask_in_filter_slice_vec.zmm);
-
-        matches_mask = _mm512_mask_cmpneq_epi16_mask(load_mask, matches_vec.zmm, _mm512_setzero_si512());
-        if (matches_mask) {
-            int offset = sz_u32_clz(matches_mask);
-            return text + length - load_length + 32 - offset - 1;
-        }
-        else { length -= load_length; }
-    }
-
-    return SZ_NULL_CHAR;
+    return sz_rfind_charset_serial(text, length, filter);
 }
 
 /**
@@ -5130,7 +5253,7 @@ SZ_PUBLIC sz_cptr_t sz_rfind_charset_neon(sz_cptr_t h, sz_size_t h_length, sz_ch
 /*
  *  @brief  Pick the right implementation for the string search algorithms.
  */
-#pragma region Compile-Time Dispatching
+#pragma region Compile - Time Dispatching
 
 SZ_PUBLIC sz_u64_t sz_hash(sz_cptr_t ins, sz_size_t length) { return sz_hash_serial(ins, length); }
 SZ_PUBLIC void sz_tolower(sz_cptr_t ins, sz_size_t length, sz_ptr_t outs) { sz_tolower_serial(ins, length, outs); }
@@ -5254,6 +5377,8 @@ SZ_DYNAMIC sz_cptr_t sz_rfind(sz_cptr_t haystack, sz_size_t h_length, sz_cptr_t
 SZ_DYNAMIC sz_cptr_t sz_find_charset(sz_cptr_t text, sz_size_t length, sz_charset_t const *set) {
 #if SZ_USE_X86_AVX512
     return sz_find_charset_avx512(text, length, set);
+#elif SZ_USE_X86_AVX2
+    return sz_find_charset_avx2(text, length, set);
 #elif SZ_USE_ARM_NEON
     return sz_find_charset_neon(text, length, set);
 #else
diff --git a/python/lib.c b/python/lib.c
index 75acfad6..a8645940 100644
--- a/python/lib.c
+++ b/python/lib.c
@@ -3060,7 +3060,9 @@ PyMODINIT_FUNC PyInit_stringzilla(void) {
         char const *avx512bw = (caps & sz_cap_x86_avx512bw_k) ? "avx512bw," : "";
         char const *avx512vbmi = (caps & sz_cap_x86_avx512vbmi_k) ? "avx512vbmi," : "";
         char const *gfni = (caps & sz_cap_x86_gfni_k) ? "gfni," : "";
-        sprintf(caps_str, "%s%s%s%s%s%s%s%s%s", serial, neon, sve, avx2, avx512f, avx512vl, avx512bw, avx512vbmi, gfni);
+        char const *avx512vbmi2 = (caps & sz_cap_x86_avx512vbmi2_k) ? "avx512vbmi2," : "";
+        sprintf(caps_str, "%s%s%s%s%s%s%s%s%s%s", serial, neon, sve, avx2, avx512f, avx512vl, avx512bw, avx512vbmi,
+                avx512vbmi2, gfni);
         PyModule_AddStringConstant(m, "__capabilities__", caps_str);
     }
 
diff --git a/scripts/bench.hpp b/scripts/bench.hpp
index ed89a02a..b685474a 100644
--- a/scripts/bench.hpp
+++ b/scripts/bench.hpp
@@ -86,14 +86,14 @@ struct tracked_function_gt {
         }
 
         // Now let's print in the format:
-        //  - name, up to 20 characters
+        //  - name, up to 32 characters
         //  - throughput in GB/s with up to 3 significant digits, 10 characters
         //  - call latency in ns with up to 1 significant digit, 10 characters
         //  - number of failed tests, 10 characters
         //  - first example of a failed test, up to 20 characters
         char const *format;
-        if (is_binary) { format = "- %-20s %15.4f GB/s %15.1f ns %10zu errors in %10zu iterations %-20s %-20s\n"; }
-        else { format = "- %-20s %15.4f GB/s %15.1f ns %10zu errors in %10zu iterations %-20s\n"; }
+        if (is_binary) { format = "- %-32s %15.4f GB/s %15.1f ns %10zu errors in %10zu iterations %-20s %-20s\n"; }
+        else { format = "- %-32s %15.4f GB/s %15.1f ns %10zu errors in %10zu iterations %-20s\n"; }
 
         std::printf(format, name.c_str(), results.bytes_passed / results.seconds / 1.e9,
                     results.seconds * 1e9 / results.iterations, failed_count, results.iterations,
diff --git a/scripts/bench_search.cpp b/scripts/bench_search.cpp
index c0411c40..6fdd70ae 100644
--- a/scripts/bench_search.cpp
+++ b/scripts/bench_search.cpp
@@ -138,6 +138,9 @@ tracked_binary_functions_t find_charset_functions() {
              return (match == std::string_view::npos ? h.size() : match);
          }},
         {"sz_find_charset_serial", wrap_sz(sz_find_charset_serial), true},
+#if SZ_USE_X86_AVX2
+        {"sz_find_charset_avx2", wrap_sz(sz_find_charset_avx2), true},
+#endif
 #if SZ_USE_X86_AVX512
         {"sz_find_charset_avx512", wrap_sz(sz_find_charset_avx512), true},
 #endif
@@ -295,25 +298,25 @@ int main(int argc, char const **argv) {
     bench_rfinds(dataset.text, {"\n"}, rfind_functions());
 
     std::printf("Benchmarking for an [\\n\\r] RegEx:\n");
-    bench_finds(dataset.text, {sz::newlines()}, find_charset_functions());
-    bench_rfinds(dataset.text, {sz::newlines()}, rfind_charset_functions());
+    bench_finds(dataset.text, {{sz::newlines(), sizeof(sz::newlines())}}, find_charset_functions());
+    bench_rfinds(dataset.text, {{sz::newlines(), sizeof(sz::newlines())}}, rfind_charset_functions());
 
     // Typical ASCII tokenization and validation benchmarks
     std::printf("Benchmarking for whitespaces:\n");
-    bench_finds(dataset.text, {sz::whitespaces()}, find_charset_functions());
-    bench_rfinds(dataset.text, {sz::whitespaces()}, rfind_charset_functions());
+    bench_finds(dataset.text, {{sz::whitespaces(), sizeof(sz::whitespaces())}}, find_charset_functions());
+    bench_rfinds(dataset.text, {{sz::whitespaces(), sizeof(sz::whitespaces())}}, rfind_charset_functions());
 
     std::printf("Benchmarking for HTML tag start/end:\n");
     bench_finds(dataset.text, {"<>"}, find_charset_functions());
     bench_rfinds(dataset.text, {"<>"}, rfind_charset_functions());
 
     std::printf("Benchmarking for punctuation marks:\n");
-    bench_finds(dataset.text, {sz::punctuation()}, find_charset_functions());
-    bench_rfinds(dataset.text, {sz::punctuation()}, rfind_charset_functions());
+    bench_finds(dataset.text, {{sz::punctuation(), sizeof(sz::punctuation())}}, find_charset_functions());
+    bench_rfinds(dataset.text, {{sz::punctuation(), sizeof(sz::punctuation())}}, rfind_charset_functions());
 
     std::printf("Benchmarking for non-printable characters:\n");
-    bench_finds(dataset.text, {sz::ascii_controls()}, find_charset_functions());
-    bench_rfinds(dataset.text, {sz::ascii_controls()}, rfind_charset_functions());
+    bench_finds(dataset.text, {{sz::ascii_controls(), sizeof(sz::ascii_controls())}}, find_charset_functions());
+    bench_rfinds(dataset.text, {{sz::ascii_controls(), sizeof(sz::ascii_controls())}}, rfind_charset_functions());
 
     // Baseline benchmarks for present tokens, coming in all lengths
     std::printf("Benchmarking on present lines:\n");


Test patch:
diff --git a/scripts/test.cpp b/scripts/test.cpp
index bc730d65..5eb2f9db 100644
--- a/scripts/test.cpp
+++ b/scripts/test.cpp
@@ -268,6 +268,14 @@ static void test_api_readonly() {
     assert(str("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+-").find_last_of("xyz") == 25);  // sets
     assert(str("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+-").find_last_of("XYZ") == 51);  // sets
 
+    // clang-format off
+    // Using single-byte non-ASCII values, e.g., À (0xC0), Æ (0xC6)
+    assert(str("abcdefgh" "\x01" "\xC6" "ijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" "\xC0" "\xFA" "0123456789+-", 68).find_first_of("\xC6\xC7") == 9);  // sets
+    assert(str("abcdefgh" "\x01" "\xC6" "ijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" "\xC0" "\xFA" "0123456789+-", 68).find_first_of("\xC0\xC1") == 54); // sets
+    assert(str("abcdefgh" "\x01" "\xC6" "ijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" "\xC0" "\xFA" "0123456789+-", 68).find_last_of("\xC6\xC7") == 9);   // sets
+    assert(str("abcdefgh" "\x01" "\xC6" "ijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" "\xC0" "\xFA" "0123456789+-", 68).find_last_of("\xC0\xC1") == 54);  // sets
+    // clang-format on
+
     // Boundary consitions.
     assert(str("hello").find_first_of("ox", 4) == 4);
     assert(str("hello").find_first_of("ox", 5) == str::npos);