Skip to content

Commit dfa7c91

Browse files
committed
We introduce a loop for finding the root of Aho-Corasick state,
which allows it to be better optimized by the compiler. This shows a 1.67x improvement over the previous algorithm. Additionally, the root finding loop may be replaced by a SIMD loop when looking up to 16 bytes on supported platforms. On supported platforms, the SIMD operation costs 16-bytes of memory when compiling the pattern. Benchmarks were run on arm64 macOS. Full-subject searches use a 4 MiB zero-filled subject, 100 searches per sample, and the median of five samples. Values are searches per second. Counts through 16 use SIMD. 32 measures the scalar skip-loop improvement independently. No match: roots before after speedup 1 261 4228 16.20x 2 262 3414 13.03x 3 261 2101 8.05x 4 260 1805 6.94x 8 258 1147 4.45x 9 260 1052 4.05x 16 259 666 2.57x 32 258 430 1.67x First match immediately after the 4 MiB prefix: roots before after speedup 1 261 4218 16.16x 2 261 3354 12.85x 3 261 2072 7.94x 4 261 1780 6.82x 8 260 1141 4.39x 9 261 1039 3.98x 16 260 663 2.55x 32 261 430 1.65x Pattern compilation uses 100,000 compilations per sample and the median of seven samples. Values are compilations per second: roots before after change 2 628559 647903 +3.1% 3 580009 577474 -0.4% 4 516172 529459 +2.6% 8 393026 390299 -0.7% 9 367666 364475 -0.9% 16 257562 255865 -0.7% 32 150395 149792 -0.4%
1 parent 50d69f7 commit dfa7c91

3 files changed

Lines changed: 365 additions & 6 deletions

File tree

erts/emulator/beam/erl_bif_binary.c

Lines changed: 172 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,19 @@
4141
#include "erl_bits.h"
4242
#include "erl_bif_unique.h"
4343

44+
#if defined(__SSE2__) || defined(_M_X64) || \
45+
(defined(_M_IX86) && defined(_M_IX86_FP) && _M_IX86_FP >= 2)
46+
# include <emmintrin.h>
47+
# define ERTS_HAVE_BYTE_SET_SIMD 1
48+
# define ERTS_BYTE_SET_SIMD_SSE2 1
49+
#elif defined(__ARM_NEON) || defined(_M_ARM64)
50+
# include <arm_neon.h>
51+
# define ERTS_HAVE_BYTE_SET_SIMD 1
52+
# define ERTS_BYTE_SET_SIMD_NEON 1
53+
#else
54+
# define ERTS_HAVE_BYTE_SET_SIMD 0
55+
#endif
56+
4457

4558
/*
4659
* The native implementation functions for the module binary.
@@ -164,6 +177,7 @@ static void *my_alloc(MyAllocator *my, Uint size)
164177
*/
165178

166179
#define ALPHABET_SIZE 256
180+
#define AC_ROOT_SIMD_MAX 16
167181

168182
typedef struct _findall_data {
169183
Uint pos;
@@ -203,6 +217,10 @@ typedef struct _ac_trie {
203217
#endif
204218
Uint32 counter; /* Number of added patterns */
205219
ACNode *root; /* pointer to the root state */
220+
#if ERTS_HAVE_BYTE_SET_SIMD
221+
Uint root_byte_count;
222+
byte root_bytes[AC_ROOT_SIMD_MAX];
223+
#endif
206224
} ACTrie;
207225

208226
typedef struct _bm_data {
@@ -450,6 +468,9 @@ static ACTrie *create_acdata(MyAllocator *my, Uint len,
450468
allocation */
451469
act->counter = 0;
452470
act->root = acn = my_alloc(my, sizeof(ACNode));
471+
#if ERTS_HAVE_BYTE_SET_SIMD
472+
act->root_byte_count = 0;
473+
#endif
453474
acn->d = 0;
454475
acn->final = 0;
455476
acn->h = NULL;
@@ -533,6 +554,15 @@ static void ac_add_one_pattern(MyAllocator *my, ACTrie *act,
533554
} else {
534555
/* allocate a new node */
535556
ACNode *nn = my_alloc(my,sizeof(ACNode));
557+
#if ERTS_HAVE_BYTE_SET_SIMD
558+
if (acn == act->root &&
559+
act->root_byte_count <= AC_ROOT_SIMD_MAX) {
560+
if (act->root_byte_count < AC_ROOT_SIMD_MAX) {
561+
act->root_bytes[act->root_byte_count] = x[i];
562+
}
563+
act->root_byte_count++;
564+
}
565+
#endif
536566
#ifdef HARDDEBUG
537567
nn->id = ++(act->idc);
538568
#endif
@@ -611,6 +641,106 @@ static void ac_compute_failure_functions(ACTrie *act, ACNode **qbuff)
611641
root->h = root;
612642
}
613643

644+
#if ERTS_HAVE_BYTE_SET_SIMD
645+
static ERTS_INLINE Uint ac_root_simd_skip_block(const ACTrie *act,
646+
const byte *haystack)
647+
{
648+
Uint i;
649+
650+
#if defined(ERTS_BYTE_SET_SIMD_NEON)
651+
static const byte lane_indices[16] = {
652+
0, 1, 2, 3, 4, 5, 6, 7,
653+
8, 9, 10, 11, 12, 13, 14, 15
654+
};
655+
uint8x16_t input = vld1q_u8(haystack);
656+
uint8x16_t matches = vdupq_n_u8(0);
657+
uint8x16_t positions;
658+
659+
for (i = 0; i < act->root_byte_count; i++) {
660+
matches = vorrq_u8(matches,
661+
vceqq_u8(input,
662+
vdupq_n_u8(act->root_bytes[i])));
663+
}
664+
if (vmaxvq_u8(matches) == 0) {
665+
return 16;
666+
}
667+
positions = vbslq_u8(matches,
668+
vld1q_u8(lane_indices),
669+
vdupq_n_u8(16));
670+
return vminvq_u8(positions);
671+
#elif defined(ERTS_BYTE_SET_SIMD_SSE2)
672+
__m128i input = _mm_loadu_si128((const __m128i *)haystack);
673+
__m128i matches = _mm_setzero_si128();
674+
unsigned int mask;
675+
676+
for (i = 0; i < act->root_byte_count; i++) {
677+
matches = _mm_or_si128(matches,
678+
_mm_cmpeq_epi8(
679+
input,
680+
_mm_set1_epi8(act->root_bytes[i])));
681+
}
682+
mask = (unsigned int)_mm_movemask_epi8(matches);
683+
if (mask == 0) {
684+
return 16;
685+
}
686+
# if defined(_MSC_VER)
687+
{
688+
unsigned long first;
689+
_BitScanForward(&first, mask);
690+
return first;
691+
}
692+
# else
693+
return (Uint)__builtin_ctz(mask);
694+
# endif
695+
#endif
696+
}
697+
#endif
698+
699+
/* Advance over bytes that cannot leave the root state. Return non-zero when
700+
* the byte at *pos has a root transition. Each SIMD block or scalar byte costs
701+
* one reduction, including the scan that finds a root transition. */
702+
static ERTS_INLINE int ac_root_skip(const ACTrie *act,
703+
const byte *haystack,
704+
Uint len,
705+
Uint max_reductions,
706+
Uint *pos,
707+
Uint *used_reductions)
708+
{
709+
Uint i = *pos;
710+
Uint used = 0;
711+
712+
#if ERTS_HAVE_BYTE_SET_SIMD
713+
if (act->root_byte_count != 0 &&
714+
act->root_byte_count <= AC_ROOT_SIMD_MAX) {
715+
while (used < max_reductions && len - i >= 16) {
716+
Uint skip = ac_root_simd_skip_block(act, haystack + i);
717+
718+
used++;
719+
i += skip;
720+
if (skip < 16) {
721+
*pos = i;
722+
*used_reductions = used;
723+
return 1;
724+
}
725+
}
726+
}
727+
#endif
728+
729+
while (used < max_reductions && i < len) {
730+
used++;
731+
if (act->root->g[haystack[i]] != NULL) {
732+
*pos = i;
733+
*used_reductions = used;
734+
return 1;
735+
}
736+
i++;
737+
}
738+
739+
*pos = i;
740+
*used_reductions = used;
741+
return 0;
742+
}
743+
614744

615745
/*
616746
* The actual searching for needles in the haystack...
@@ -638,6 +768,7 @@ static BFReturn ac_find_first_match(BinaryFindContext *ctx,
638768
const byte *haystack)
639769
{
640770
ACFindFirstState *state = &(ctx->u.ff.d.ac);
771+
ACTrie *act = ERTS_MAGIC_BIN_DATA(ctx->pat_bin);
641772
Uint *mpos = &(ctx->u.ff.pos);
642773
Uint *mlen = &(ctx->u.ff.len);
643774
Uint *reductions = &(ctx->reds);
@@ -650,16 +781,34 @@ static BFReturn ac_find_first_match(BinaryFindContext *ctx,
650781
register Uint reds = *reductions;
651782

652783
while (i < len) {
653-
if (reds == 0) {
784+
int reduction_charged = 0;
785+
786+
if (candidate == NULL && q == act->root) {
787+
Uint used_reductions;
788+
789+
reduction_charged = ac_root_skip(act,
790+
haystack,
791+
len,
792+
reds,
793+
&i,
794+
&used_reductions);
795+
reds -= used_reductions;
796+
if (i == len) {
797+
break;
798+
}
799+
}
800+
801+
if (!reduction_charged && reds == 0) {
654802
state->q = q;
655803
state->pos = i;
656804
state->len = len;
657805
state->candidate = candidate;
658806
state->candidate_start = candidate_start;
659807
return BF_RESTART;
660808
}
661-
662-
reds--;
809+
if (!reduction_charged) {
810+
reds--;
811+
}
663812

664813
while (q->g[haystack[i]] == NULL && q->h != q) {
665814
q = q->h;
@@ -730,6 +879,7 @@ static BFReturn ac_find_all_non_overlapping(BinaryFindContext *ctx,
730879
const byte *haystack)
731880
{
732881
ACFindAllState *state = &(ctx->u.fa.d.ac);
882+
ACTrie *act = ERTS_MAGIC_BIN_DATA(ctx->pat_bin);
733883
Uint *reductions = &(ctx->reds);
734884
ACNode *q = state->q;
735885
Uint i = state->pos;
@@ -742,7 +892,25 @@ static BFReturn ac_find_all_non_overlapping(BinaryFindContext *ctx,
742892
register Uint reds = *reductions;
743893

744894
while (i < len) {
745-
if (--reds == 0) {
895+
int reduction_charged = 0;
896+
897+
if (q == act->root) {
898+
Uint used_reductions;
899+
Uint max_reductions = reds > 0 ? reds - 1 : 0;
900+
901+
reduction_charged = ac_root_skip(act,
902+
haystack,
903+
len,
904+
max_reductions,
905+
&i,
906+
&used_reductions);
907+
reds -= used_reductions;
908+
if (i == len) {
909+
break;
910+
}
911+
}
912+
913+
if (!reduction_charged && --reds == 0) {
746914
state->q = q;
747915
state->pos = i;
748916
state->len = len;

lib/stdlib/test/binary_module_SUITE.erl

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
-module(binary_module_SUITE).
2323

2424
-export([all/0, suite/0,
25-
interesting/1,scope_return/1,random_ref_comp/1,random_ref_sr_comp/1,
25+
interesting/1,ac_simd_root_skip/1,scope_return/1,random_ref_comp/1,random_ref_sr_comp/1,
2626
random_ref_fla_comp/1,parts/1, bin_to_list/1, list_to_bin/1,
2727
copy/1, referenced/1,guard/1,encode_decode/1,badargs/1,longest_common_trap/1,
2828
check_no_invalid_read_bug/1,error_info/1, hex_encoding/1, join/1, doctests/1]).
@@ -36,7 +36,8 @@ suite() ->
3636
{timetrap,{minutes,10}}].
3737

3838
all() ->
39-
[scope_return,interesting, random_ref_fla_comp, random_ref_sr_comp,
39+
[scope_return,interesting, ac_simd_root_skip,
40+
random_ref_fla_comp, random_ref_sr_comp,
4041
random_ref_comp, parts, bin_to_list, list_to_bin, copy,
4142
referenced, guard, encode_decode, badargs,
4243
longest_common_trap, check_no_invalid_read_bug,
@@ -383,6 +384,59 @@ interesting(Config) when is_list(Config) ->
383384
X = do_interesting(binary),
384385
X = do_interesting(binref).
385386

387+
%% Exercise the SIMD filter used to skip 16-byte subject blocks while an
388+
%% Aho-Corasick search is at its root by covering lengths of 3 and 9.
389+
ac_simd_root_skip(Config) when is_list(Config) ->
390+
ThreeRoots = binary:compile_pattern([<<Byte>> || Byte <- lists:seq(1, 3)]),
391+
{31, 1} = binary:match(<<0:248, 3>>, ThreeRoots),
392+
393+
NineRoots = binary:compile_pattern([<<Byte>> || Byte <- lists:seq(1, 9)]),
394+
{31, 1} = binary:match(<<0:248, 9>>, NineRoots),
395+
396+
Patterns = [<<Byte>> || Byte <- lists:seq(1, 16)],
397+
Compiled = binary:compile_pattern(Patterns),
398+
{ac, _} = Compiled,
399+
400+
nomatch = binary:match(<<0, 0, 0>>, Compiled),
401+
{3, 1} = binary:match(<<0, 0, 0, 16>>, Compiled),
402+
{31, 1} = binary:match(<<0:248, 16>>, Compiled),
403+
{2, 1} = binary:match(<<0, 0, 1, 16>>, Compiled,
404+
[{scope, {2, 2}}]),
405+
[{1, 1}, {3, 1}] = binary:matches(<<0, 1, 0, 16>>, Compiled),
406+
[{15, 1}, {32, 1}] = binary:matches(<<0:120, 1, 0:128, 2>>,
407+
Compiled),
408+
[<<0>>, <<0, 16>>] = binary:split(<<0, 1, 0, 16>>, Compiled),
409+
[<<0>>, <<0>>, <<>>] = binary:split(<<0, 1, 0, 16>>, Compiled,
410+
[global]),
411+
412+
Duplicate = binary:compile_pattern([<<7>>, <<7>>, <<9>>]),
413+
{1, 1} = binary:match(<<0, 9, 7>>, Duplicate),
414+
{ac, _} = binary:compile_pattern([<<7>>, <<7>>]),
415+
416+
UnalignedPatterns = [make_unaligned2(Pattern) || Pattern <- Patterns],
417+
{2, 1} = binary:match(make_unaligned(<<0, 0, 8>>),
418+
binary:compile_pattern(UnalignedPatterns)),
419+
420+
%% The root filter is also used for ordinary multi-byte AC tries. Match
421+
%% selection remains AC's leftmost-longest result after the skipped data.
422+
Mixed = binary:compile_pattern([<<"ab">>, <<"abc">>, <<"bc">>]),
423+
{32, 3} = binary:match(<<0:256, "abc">>, Mixed),
424+
[{16, 3}, {36, 2}] = binary:matches(<<0:128, "abc", 0:136, "bc">>,
425+
Mixed),
426+
427+
%% Multiple patterns may share a single unique root transition.
428+
SingleRoot = binary:compile_pattern([<<"ab">>, <<"abc">>, <<"ad">>]),
429+
{32, 3} = binary:match(<<0:256, "abc">>, SingleRoot),
430+
[{16, 3}, {36, 2}] = binary:matches(<<0:128, "abc", 0:136, "ad">>,
431+
SingleRoot),
432+
433+
%% More unique root bytes than fit in the SIMD filter continue to use
434+
%% the scalar Aho-Corasick search loop.
435+
Fallback = binary:compile_pattern([<<Byte>> || Byte <- lists:seq(1, 17)]),
436+
{ac, _} = Fallback,
437+
{2, 1} = binary:match(<<0, 0, 17>>, Fallback),
438+
ok.
439+
386440
do_interesting(Module) ->
387441
{0,4} = Module:match(<<"123456">>,
388442
Module:compile_pattern([<<"12">>,<<"1234">>,

0 commit comments

Comments
 (0)