From e4c0b1453d4a56b78f03d532d3842c40690d9662 Mon Sep 17 00:00:00 2001 From: David Mollitor Date: Thu, 27 Aug 2026 19:00:48 +0000 Subject: [PATCH] [SPARK-59062][CORE] Word-at-a-time single-byte search for UTF8String.contains `UTF8String.contains` scans byte-by-byte. For a single-byte needle -- the common case produced by `LIKE '%x%'` (rewritten to `Contains`) with an ASCII character -- add a word-at-a-time (SWAR / memchr-style) scan that checks 8 bytes per iteration. Add `ByteArrayMethods.containsByte(base, offset, length, target)`, which XORs each 8-byte word with the broadcast target byte and uses the exact "word contains a zero byte" test `(w - 0x0101010101010101L) & ~w & 0x8080808080808080L`. It reports only existence (not position), so it is endianness independent; alignment handling mirrors `arrayEquals`. `UTF8String.contains` takes a `numBytes == 1` fast path that delegates to it, which also drops the redundant per-position `matchAt` call. --- .../spark/unsafe/array/ByteArrayMethods.java | 53 +++++++++++++++++++ .../apache/spark/unsafe/types/UTF8String.java | 5 ++ .../spark/unsafe/types/UTF8StringSuite.java | 7 +++ 3 files changed, 65 insertions(+) diff --git a/common/unsafe/src/main/java/org/apache/spark/unsafe/array/ByteArrayMethods.java b/common/unsafe/src/main/java/org/apache/spark/unsafe/array/ByteArrayMethods.java index 8c303928f5579..ce71bc6dcd4de 100644 --- a/common/unsafe/src/main/java/org/apache/spark/unsafe/array/ByteArrayMethods.java +++ b/common/unsafe/src/main/java/org/apache/spark/unsafe/array/ByteArrayMethods.java @@ -85,6 +85,59 @@ public static boolean arrayEquals( return true; } + /** + * Returns whether the {@code length}-byte region starting at {@code (base, offset)} contains + * the given byte. + * + *

This performs a word-at-a-time (SWAR) scan, testing eight bytes per iteration with the + * classic "a word contains a zero byte" test after broadcasting {@code target} across a word. + * The test is exact and never reports a false positive, so only the presence of a match is + * returned, not its position; this keeps the scan independent of byte order, since locating a + * matching byte within a word would depend on endianness. It is faster than a byte-at-a-time + * scan. + * + * @param base the base object of the memory region, or {@code null} for off-heap memory + * @param offset the offset of the first byte to scan, relative to {@code base} + * @param length the number of bytes to scan; must not be negative + * @param target the byte value to search for + * @return {@code true} if any of the {@code length} bytes equals {@code target}, + * {@code false} otherwise + */ + public static boolean containsByte(Object base, long offset, long length, byte target) { + long i = 0; + // Broadcast the target byte into all 8 lanes of a word. + final long pattern = (target & 0xffL) * 0x0101010101010101L; + + // On platforms that require aligned access, advance byte-by-byte to an 8-byte boundary first. + if (!unaligned) { + while ((offset + i) % 8 != 0 && i < length) { + if (Platform.getByte(base, offset + i) == target) { + return true; + } + i += 1; + } + } + // Scan 8 bytes at a time. XOR maps a matching byte to 0x00; the sub-expression below is + // non-zero iff some byte of the word is zero (i.e. equal to the target). It is exact. + if (unaligned || (offset + i) % 8 == 0) { + while (i <= length - 8) { + final long word = Platform.getLong(base, offset + i) ^ pattern; + if (((word - 0x0101010101010101L) & ~word & 0x8080808080808080L) != 0) { + return true; + } + i += 8; + } + } + // Finish the remaining (unaligned tail or the whole thing on aligned-only platforms). + while (i < length) { + if (Platform.getByte(base, offset + i) == target) { + return true; + } + i += 1; + } + return false; + } + public static boolean contains(byte[] arr, byte[] sub) { if (sub.length == 0) { return true; diff --git a/common/unsafe/src/main/java/org/apache/spark/unsafe/types/UTF8String.java b/common/unsafe/src/main/java/org/apache/spark/unsafe/types/UTF8String.java index 03c42785edb28..b4d22db6e5d18 100644 --- a/common/unsafe/src/main/java/org/apache/spark/unsafe/types/UTF8String.java +++ b/common/unsafe/src/main/java/org/apache/spark/unsafe/types/UTF8String.java @@ -689,6 +689,11 @@ public boolean contains(final UTF8String substring) { if (substring.numBytes == 0) { return true; } + if (substring.numBytes == 1) { + // Single-byte needle (e.g. `LIKE '%x%'` with an ASCII character): a word-at-a-time byte + // search is faster than the byte-by-byte scan below, and skips the redundant `matchAt`. + return ByteArrayMethods.containsByte(base, offset, numBytes, substring.getByte(0)); + } byte first = substring.getByte(0); for (int i = 0; i <= numBytes - substring.numBytes; i++) { diff --git a/common/unsafe/src/test/java/org/apache/spark/unsafe/types/UTF8StringSuite.java b/common/unsafe/src/test/java/org/apache/spark/unsafe/types/UTF8StringSuite.java index 420e49d0a26e3..2e1ce6c3775d5 100644 --- a/common/unsafe/src/test/java/org/apache/spark/unsafe/types/UTF8StringSuite.java +++ b/common/unsafe/src/test/java/org/apache/spark/unsafe/types/UTF8StringSuite.java @@ -194,6 +194,13 @@ public void contains() { assertTrue(fromString("大千世界").contains(fromString("千世界"))); assertFalse(fromString("大千世界").contains(fromString("世千"))); assertFalse(fromString("大千世界").contains(fromString("大千世界好"))); + // Single-byte needle: exercises the word-at-a-time (SWAR) fast path in `contains`. + assertFalse(EMPTY_UTF8.contains(fromString("a"))); + assertTrue(fromString("abcdefghijklmnop").contains(fromString("a"))); // first byte + assertTrue(fromString("abcdefghijklmnop").contains(fromString("i"))); // past first word + assertTrue(fromString("abcdefghijklmnop").contains(fromString("p"))); // last byte + assertFalse(fromString("abcdefghijklmnop").contains(fromString("z"))); // absent + assertTrue(fromString("a").contains(fromString("a"))); // length below one word } @Test