Skip to content

Commit 40298f4

Browse files
committed
SVE2 MATCH LP
1 parent ba2b162 commit 40298f4

1 file changed

Lines changed: 204 additions & 14 deletions

File tree

content/learning-paths/servers-and-cloud-computing/sve2-match/sve2-match-search.md

Lines changed: 204 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,15 @@ Let's implement three versions of our search function:
6060
Create a generic implementation in C, checking each element individually against each key. Open a editor of your choice and copy the code shown into a file named `sve2_match_demo.c`:
6161

6262
```c
63+
#include <arm_sve.h>
64+
#include <inttypes.h>
65+
#include <stddef.h>
66+
#include <stdint.h>
67+
#include <stdio.h>
68+
#include <stdlib.h>
69+
#include <string.h>
70+
#include <time.h>
71+
6372
int search_generic_u8(const uint8_t *hay, size_t n, const uint8_t *keys,
6473
size_t nkeys) {
6574
for (size_t i = 0; i < n; ++i) {
@@ -186,6 +195,54 @@ int search_sve2_match_u8_unrolled(const uint8_t *hay, size_t n, const uint8_t *k
186195
}
187196
return 0;
188197
}
198+
199+
// Optimized 16-bit version with unrolling
200+
int search_sve2_match_u16_unrolled(const uint16_t *hay, size_t n, const uint16_t *keys,
201+
size_t nkeys) {
202+
if (nkeys == 0) return 0;
203+
const size_t VL = svcnth();
204+
svbool_t pg = svptrue_b16();
205+
206+
// Prepare key vector
207+
uint16_t tmp[128] __attribute__((aligned(64)));
208+
for (size_t i = 0; i < VL; ++i) tmp[i] = keys[i % nkeys];
209+
svuint16_t keyvec = svld1(pg, tmp);
210+
211+
size_t i = 0;
212+
// Process 4 vectors per iteration
213+
for (; i + 4*VL <= n; i += 4*VL) {
214+
// Prefetch data ahead
215+
__builtin_prefetch(&hay[i + 16*VL], 0, 0);
216+
217+
svuint16_t block1 = svld1(pg, &hay[i]);
218+
svuint16_t block2 = svld1(pg, &hay[i + VL]);
219+
svuint16_t block3 = svld1(pg, &hay[i + 2*VL]);
220+
svuint16_t block4 = svld1(pg, &hay[i + 3*VL]);
221+
222+
svbool_t match1 = svmatch_u16(pg, block1, keyvec);
223+
svbool_t match2 = svmatch_u16(pg, block2, keyvec);
224+
svbool_t match3 = svmatch_u16(pg, block3, keyvec);
225+
svbool_t match4 = svmatch_u16(pg, block4, keyvec);
226+
227+
if (svptest_any(pg, match1) || svptest_any(pg, match2) ||
228+
svptest_any(pg, match3) || svptest_any(pg, match4))
229+
return 1;
230+
}
231+
232+
// Process remaining vectors one at a time
233+
for (; i + VL <= n; i += VL) {
234+
svuint16_t block = svld1(pg, &hay[i]);
235+
if (svptest_any(pg, svmatch_u16(pg, block, keyvec))) return 1;
236+
}
237+
238+
// Handle remainder
239+
for (; i < n; ++i) {
240+
uint16_t v = hay[i];
241+
for (size_t k = 0; k < nkeys; ++k)
242+
if (v == keys[k]) return 1;
243+
}
244+
return 0;
245+
}
189246
```
190247
The main highlights of this implementation are:
191248
- Processes 4 vectors per iteration instead of just one
@@ -195,7 +252,7 @@ The main highlights of this implementation are:
195252
196253
## Benchmarking Framework
197254
198-
To compare the performance of the three implementations, you will use a benchmarking framework that measures the execution time of each implementation:
255+
To compare the performance of the three implementations, you will use a benchmarking framework that measures the execution time of each implementation. You will also add helper functions for membership testing that are needed to setup the test data with controlled hit rates:
199256
200257
```c
201258
// Timing function
@@ -209,6 +266,44 @@ static inline uint64_t nsec_now(void) {
209266
return (uint64_t)ts.tv_sec * 1000000000ULL + ts.tv_nsec;
210267
}
211268
269+
// ---------------- helper: membership test for RNG fill ----------------------
270+
static int key_in_set_u8(uint8_t v, const uint8_t *keys, size_t nkeys) {
271+
for (size_t k = 0; k < nkeys; ++k)
272+
if (v == keys[k]) return 1;
273+
return 0;
274+
}
275+
static int key_in_set_u16(uint16_t v, const uint16_t *keys, size_t nkeys) {
276+
for (size_t k = 0; k < nkeys; ++k)
277+
if (v == keys[k]) return 1;
278+
return 0;
279+
}
280+
281+
// Fill such that P(match) ~= p.
282+
static void fill_bytes_lowhit(uint8_t *buf, size_t n, const uint8_t *keys,
283+
size_t nkeys, double p) {
284+
for (size_t i = 0; i < n; ++i) {
285+
if (drand48() < p) {
286+
buf[i] = keys[rand() % nkeys];
287+
} else {
288+
uint8_t v;
289+
do { v = (uint8_t)rand(); } while (key_in_set_u8(v, keys, nkeys));
290+
buf[i] = v;
291+
}
292+
}
293+
}
294+
static void fill_u16_lowhit(uint16_t *buf, size_t n, const uint16_t *keys,
295+
size_t nkeys, double p) {
296+
for (size_t i = 0; i < n; ++i) {
297+
if (drand48() < p) {
298+
buf[i] = keys[rand() % nkeys];
299+
} else {
300+
uint16_t v;
301+
do { v = (uint16_t)rand(); } while (key_in_set_u16(v, keys, nkeys));
302+
buf[i] = v;
303+
}
304+
}
305+
}
306+
212307
// Main benchmarking function
213308
int main(int argc, char **argv) {
214309
const size_t len = (argc > 1) ? strtoull(argv[1], NULL, 0) : (1ull << 26);
@@ -220,7 +315,106 @@ int main(int argc, char **argv) {
220315
printf("Hit probability : %.6f (%.4f %% )\n\n", hit_prob, hit_prob * 100.0);
221316
222317
// Initialize data and run benchmarks...
223-
}
318+
srand48(42);
319+
srand(42);
320+
321+
// Align memory to 64-byte boundary for better performance
322+
uint8_t *hay8 = aligned_alloc(64, len);
323+
uint16_t *hay16 = aligned_alloc(64, len * sizeof(uint16_t));
324+
325+
const uint8_t keys8[] = {0x13, 0x7F, 0xA5, 0xEE, 0x4C, 0x42, 0x01, 0x9B};
326+
const uint16_t keys16[] = {0x1234, 0x7F7F, 0xA5A5, 0xEEEE, 0x4C4C, 0x4242};
327+
const size_t NKEYS8 = sizeof(keys8) / sizeof(keys8[0]);
328+
const size_t NKEYS16 = sizeof(keys16) / sizeof(keys16[0]);
329+
330+
fill_bytes_lowhit(hay8, len, keys8, NKEYS8, hit_prob);
331+
fill_u16_lowhit(hay16, len, keys16, NKEYS16, hit_prob);
332+
333+
uint64_t t_gen8 = 0, t_sve8 = 0, t_sve8_unrolled = 0;
334+
uint64_t t_gen16 = 0, t_sve16 = 0, t_sve16_unrolled = 0;
335+
336+
for (int it = 0; it < iterations; ++it) {
337+
uint64_t t0;
338+
339+
t0 = nsec_now();
340+
volatile int r = search_generic_u8(hay8, len, keys8, NKEYS8); (void)r;
341+
t_gen8 += nsec_now() - t0;
342+
343+
#if defined(__ARM_FEATURE_SVE2)
344+
t0 = nsec_now();
345+
r = search_sve2_match_u8(hay8, len, keys8, NKEYS8); (void)r;
346+
t_sve8 += nsec_now() - t0;
347+
348+
t0 = nsec_now();
349+
r = search_sve2_match_u8_unrolled(hay8, len, keys8, NKEYS8); (void)r;
350+
t_sve8_unrolled += nsec_now() - t0;
351+
#endif
352+
353+
t0 = nsec_now();
354+
r = search_generic_u16(hay16, len, keys16, NKEYS16); (void)r;
355+
t_gen16 += nsec_now() - t0;
356+
357+
#if defined(__ARM_FEATURE_SVE2)
358+
t0 = nsec_now();
359+
r = search_sve2_match_u16(hay16, len, keys16, NKEYS16); (void)r;
360+
t_sve16 += nsec_now() - t0;
361+
362+
t0 = nsec_now();
363+
r = search_sve2_match_u16_unrolled(hay16, len, keys16, NKEYS16); (void)r;
364+
t_sve16_unrolled += nsec_now() - t0;
365+
#endif
366+
}
367+
// ---------- latency results ----------
368+
printf("Average latency over %d iterations (ns):\n", iterations);
369+
printf(" generic_u8 : %.2f\n", (double)t_gen8 / iterations);
370+
#if defined(__ARM_FEATURE_SVE2)
371+
printf(" sve2_u8 : %.2f\n", (double)t_sve8 / iterations);
372+
printf(" sve2_u8_unrolled : %.2f\n", (double)t_sve8_unrolled / iterations);
373+
printf(" speed‑up (orig) : %.2fx\n", (double)t_gen8 / t_sve8);
374+
printf(" speed‑up (unroll): %.2fx\n\n", (double)t_gen8 / t_sve8_unrolled);
375+
#else
376+
printf(" (SVE2 path not built)\n\n");
377+
#endif
378+
printf(" generic_u16 : %.2f\n", (double)t_gen16 / iterations);
379+
#if defined(__ARM_FEATURE_SVE2)
380+
printf(" sve2_u16 : %.2f\n", (double)t_sve16 / iterations);
381+
printf(" sve2_u16_unrolled: %.2f\n", (double)t_sve16_unrolled / iterations);
382+
printf(" speed‑up (orig) : %.2fx\n", (double)t_gen16 / t_sve16);
383+
printf(" speed‑up (unroll): %.2fx\n", (double)t_gen16 / t_sve16_unrolled);
384+
#else
385+
printf(" (SVE2 path not built)\n");
386+
#endif
387+
// ---------- throughput results ----------
388+
const double elems_total = (double)len * iterations;
389+
printf("\nThroughput (million items/second):\n");
390+
double tp_gen8 = elems_total / (t_gen8 / 1e9) / 1e6;
391+
printf(" generic_u8 : %.2f Mi/s\n", tp_gen8);
392+
#if defined(__ARM_FEATURE_SVE2)
393+
double tp_sve8 = elems_total / (t_sve8 / 1e9) / 1e6;
394+
double tp_sve8_unrolled = elems_total / (t_sve8_unrolled / 1e9) / 1e6;
395+
printf(" sve2_u8 : %.2f Mi/s\n", tp_sve8);
396+
printf(" sve2_u8_unrolled : %.2f Mi/s\n", tp_sve8_unrolled);
397+
printf(" speed‑up (orig) : %.2fx\n", tp_sve8 / tp_gen8);
398+
printf(" speed‑up (unroll): %.2fx\n\n", tp_sve8_unrolled / tp_gen8);
399+
#else
400+
printf(" (SVE2 path not built)\n\n");
401+
#endif
402+
double tp_gen16 = elems_total / (t_gen16 / 1e9) / 1e6;
403+
printf(" generic_u16 : %.2f Mi/s\n", tp_gen16);
404+
#if defined(__ARM_FEATURE_SVE2)
405+
double tp_sve16 = elems_total / (t_sve16 / 1e9) / 1e6;
406+
double tp_sve16_unrolled = elems_total / (t_sve16_unrolled / 1e9) / 1e6;
407+
printf(" sve2_u16 : %.2f Mi/s\n", tp_sve16);
408+
printf(" sve2_u16_unrolled: %.2f Mi/s\n", tp_sve16_unrolled);
409+
printf(" speed‑up (orig) : %.2fx\n", tp_sve16 / tp_gen16);
410+
printf(" speed‑up (unroll): %.2fx\n", tp_sve16_unrolled / tp_gen16);
411+
#else
412+
printf(" (SVE2 path not built)\n");
413+
#endif
414+
415+
free(hay8);
416+
free(hay16);
417+
return 0;
224418
```
225419
## Compiling and Running
226420

@@ -234,7 +428,7 @@ Now run the benchmark on a dataset of 65,536 elements (2^16) with a 0.001% hit r
234428

235429
```bash
236430
./sve2_match_demo $((1<<16)) 3 0.00001
237-
431+
```
238432
The output will look like:
239433

240434
```output
@@ -299,13 +493,9 @@ When running on a Graviton4 instance with Ubuntu 24.04 and a dataset of 65,536 e
299493
| 0.01% | 21.00x | 27.08x |
300494
| 0.1% | 17.25x | 20.97x |
301495

302-
# Understanding the Performance Results
303-
304-
The benchmark results reveal several important insights about the performance characteristics of SVE2 MATCH instructions:
305496

306497
### Impact of Hit Rate on Performance
307-
308-
The most striking observation is how the performance advantage of SVE2 MATCH varies with the hit rate:
498+
The benchmark results reveal several important insights about the performance characteristics of SVE2 MATCH instructions. The most striking observation is how the performance advantage of SVE2 MATCH varies with the hit rate:
309499

310500
1. **Very Low Hit Rates (0% - 0.001%)**:
311501
- For 8-bit data, SVE2 MATCH Unrolled achieves an impressive 90-95x speedup
@@ -338,32 +528,32 @@ The unrolled implementation consistently outperforms the basic SVE2 MATCH implem
338528

339529
This demonstrates the value of combining algorithmic optimizations (loop unrolling, prefetching) with hardware-specific instructions for maximum performance.
340530

341-
## Applications of SVE2 MATCH
531+
### Applications of SVE2 MATCH
342532

343-
The SVE2 MATCH instruction can be applied to various real-world scenarios:
533+
The SVE2 MATCH instruction can be applied to various real-world scenarios such as:
344534

345-
### 1. Database Systems
535+
**Database Systems**
346536

347537
In database systems, MATCH can accelerate:
348538
- String pattern matching in text columns
349539
- Value existence checks in arrays
350540
- Filtering operations in columnar databases
351541

352-
### 2. Text Processing
542+
**Text Processing**
353543

354544
For text processing applications, MATCH can speed up:
355545
- Character set membership tests
356546
- Word boundary detection
357547
- Special character identification
358548

359-
### 3. Network Packet Inspection
549+
**Network Packet Inspection**
360550

361551
In network applications, MATCH can improve:
362552
- Protocol header inspection
363553
- Pattern matching in packet payloads
364554
- Signature-based intrusion detection
365555

366-
### 4. Image Processing
556+
**Image Processing**
367557

368558
For image processing, MATCH can accelerate:
369559
- Color palette lookups

0 commit comments

Comments
 (0)