sin_fast_blank 4.0.0 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +215 -0
- data/LICENSE.txt +23 -0
- data/README.md +307 -0
- data/Rakefile +45 -0
- data/ext/sin_fast_blank/extconf.rb +30 -5
- data/ext/sin_fast_blank/sin_fast_blank.c +374 -262
- data/lib/sin_fast_blank/version.rb +5 -0
- data/lib/sin_fast_blank.rb +19 -0
- metadata +14 -11
|
@@ -1,35 +1,65 @@
|
|
|
1
1
|
#include <ruby.h>
|
|
2
2
|
#include <ruby/encoding.h>
|
|
3
3
|
#include <stdbool.h>
|
|
4
|
-
#
|
|
5
|
-
|
|
4
|
+
#include <string.h>
|
|
5
|
+
|
|
6
|
+
/* MSVC does not define __SSE2__, but SSE2 is part of the x64 ABI and of 32-bit /arch:SSE2 builds. */
|
|
7
|
+
#if defined(__SSE2__) || defined(_M_X64) || (defined(_M_IX86_FP) && _M_IX86_FP >= 2)
|
|
8
|
+
#define SIN_FAST_BLANK_SSE2 1
|
|
9
|
+
#endif
|
|
10
|
+
|
|
11
|
+
/*
|
|
12
|
+
* __AVX2__: the user opted in with -mavx2/-march=native, so every AVX2 helper can be compiled directly.
|
|
13
|
+
* HAVE_AVX2_RUNTIME_DISPATCH (from extconf.rb): only the helpers marked with __attribute__((target("avx2"))) use AVX2 and
|
|
14
|
+
* Init_sin_fast_blank selects them via __builtin_cpu_supports, so the binary stays safe on CPUs without AVX2.
|
|
15
|
+
*/
|
|
16
|
+
#if defined(__AVX2__)
|
|
17
|
+
#define SIN_FAST_BLANK_AVX2 1
|
|
18
|
+
#define SIN_FAST_BLANK_AVX2_TARGET
|
|
19
|
+
#elif defined(HAVE_AVX2_RUNTIME_DISPATCH) && defined(SIN_FAST_BLANK_SSE2) && (defined(__GNUC__) || defined(__clang__))
|
|
20
|
+
#define SIN_FAST_BLANK_AVX2 1
|
|
21
|
+
#define SIN_FAST_BLANK_AVX2_DISPATCH 1
|
|
22
|
+
#define SIN_FAST_BLANK_AVX2_TARGET __attribute__((target("avx2")))
|
|
23
|
+
#endif
|
|
24
|
+
|
|
25
|
+
#if defined(SIN_FAST_BLANK_SSE2)
|
|
26
|
+
#include <emmintrin.h>
|
|
27
|
+
#endif
|
|
28
|
+
#if defined(SIN_FAST_BLANK_AVX2)
|
|
29
|
+
#include <immintrin.h>
|
|
6
30
|
#endif
|
|
7
|
-
#
|
|
8
|
-
|
|
31
|
+
#if defined(__ARM_NEON) && defined(__aarch64__)
|
|
32
|
+
#define SIN_FAST_BLANK_NEON 1
|
|
33
|
+
#include <arm_neon.h>
|
|
34
|
+
#endif
|
|
35
|
+
|
|
36
|
+
#if defined(SIN_FAST_BLANK_SSE2) || defined(SIN_FAST_BLANK_AVX2)
|
|
37
|
+
#if defined(_MSC_VER) && !defined(__clang__)
|
|
38
|
+
#include <intrin.h>
|
|
39
|
+
static inline int count_trailing_zeros(unsigned int value) {
|
|
40
|
+
unsigned long index;
|
|
41
|
+
_BitScanForward(&index, value);
|
|
42
|
+
return (int)index;
|
|
43
|
+
}
|
|
44
|
+
#else
|
|
45
|
+
static inline int count_trailing_zeros(unsigned int value) { return __builtin_ctz(value); }
|
|
9
46
|
#endif
|
|
10
|
-
#ifdef __ARM_NEON
|
|
11
|
-
#include <arm_neon.h>
|
|
12
47
|
#endif
|
|
13
48
|
|
|
14
49
|
#define STR_ENC_GET(str) rb_enc_from_index(ENCODING_GET(str))
|
|
15
50
|
|
|
16
|
-
#define
|
|
17
|
-
#define
|
|
18
|
-
#define
|
|
19
|
-
#define
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
static inline bool
|
|
24
|
-
return c ==
|
|
25
|
-
c == ASCII_BLANK_TAB ||
|
|
26
|
-
c == ASCII_BLANK_LF ||
|
|
27
|
-
c == ASCII_BLANK_VT ||
|
|
28
|
-
c == ASCII_BLANK_FF ||
|
|
29
|
-
c == ASCII_BLANK_CR;
|
|
51
|
+
#define ASCII_WS_RANGE_MIN 0x09
|
|
52
|
+
#define ASCII_WS_RANGE_MAX 0x0d
|
|
53
|
+
#define ASCII_WS_SPACE 0x20
|
|
54
|
+
#define MAX_CTYPE_CODEPOINT 0xFF
|
|
55
|
+
|
|
56
|
+
static inline bool is_ascii_blank_char(unsigned char c) { return (c >= ASCII_WS_RANGE_MIN && c <= ASCII_WS_RANGE_MAX) || c == ASCII_WS_SPACE; }
|
|
57
|
+
|
|
58
|
+
static inline bool is_ascii_blank_or_null_char(unsigned char c) {
|
|
59
|
+
return c == 0x00 || (c >= ASCII_WS_RANGE_MIN && c <= ASCII_WS_RANGE_MAX) || c == ASCII_WS_SPACE;
|
|
30
60
|
}
|
|
31
61
|
|
|
32
|
-
static inline
|
|
62
|
+
static inline bool is_unicode_blank(unsigned int codepoint) {
|
|
33
63
|
switch (codepoint) {
|
|
34
64
|
case 0x9:
|
|
35
65
|
case 0xa:
|
|
@@ -56,65 +86,39 @@ static inline int is_unicode_blank(unsigned int codepoint) {
|
|
|
56
86
|
case 0x202f:
|
|
57
87
|
case 0x205f:
|
|
58
88
|
case 0x3000:
|
|
59
|
-
return
|
|
89
|
+
return true;
|
|
60
90
|
default:
|
|
61
|
-
return
|
|
91
|
+
return false;
|
|
62
92
|
}
|
|
63
93
|
}
|
|
64
94
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const __m256i cr = _mm256_set1_epi8(ASCII_BLANK_CR);
|
|
74
|
-
|
|
75
|
-
size_t i = 0;
|
|
76
|
-
|
|
77
|
-
for (; i + 31 < len; i += 32) {
|
|
78
|
-
__m256i chunk = _mm256_loadu_si256((const __m256i *)(ptr + i));
|
|
79
|
-
|
|
80
|
-
__m256i non_ascii = _mm256_and_si256(chunk, ascii_mask);
|
|
81
|
-
if (!_mm256_testz_si256(non_ascii, non_ascii)) {
|
|
82
|
-
for (size_t j = 0; j < 32; j++) {
|
|
83
|
-
if (ptr[i + j] >= 0x80) {
|
|
84
|
-
*non_ascii_pos = ptr + i + j;
|
|
85
|
-
return false;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
}
|
|
95
|
+
/*
|
|
96
|
+
* Matched by name because ONIGENC_IS_UNICODE() reads rb_encoding internals that other Ruby implementations may not expose. Every
|
|
97
|
+
* Unicode encoding Ruby ships is named UTF-8, UTF-16*, UTF-32*, UTF8-* (the MAC and carrier replicas) or CESU-8.
|
|
98
|
+
*/
|
|
99
|
+
static inline bool is_unicode_encoding(rb_encoding* enc) {
|
|
100
|
+
const char* name = rb_enc_name(enc);
|
|
101
|
+
return strncmp(name, "UTF", 3) == 0 || strncmp(name, "CESU", 4) == 0;
|
|
102
|
+
}
|
|
89
103
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
for (size_t j = 0; j < 32; j++) {
|
|
105
|
-
unsigned char c = ptr[i + j];
|
|
106
|
-
if (c >= 0x80) {
|
|
107
|
-
*non_ascii_pos = ptr + i + j;
|
|
108
|
-
return false;
|
|
109
|
-
}
|
|
110
|
-
if (!is_ascii_blank_char(c)) {
|
|
111
|
-
return false;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
}
|
|
104
|
+
/*
|
|
105
|
+
* ActiveSupport's blank regexp matches [[:space:]] with the ctype table of the string's own encoding, which differs from the Unicode
|
|
106
|
+
* table for some encodings (e.g. 0x85 is blank in UTF-8 but not in ISO-8859-1 or ASCII-8BIT). rb_enc_isspace() reads that same table.
|
|
107
|
+
*
|
|
108
|
+
* Only single-byte codes reach it, and that is not an optimization. TruffleRuby declares it as taking an unsigned char, so a wider
|
|
109
|
+
* codepoint would be silently truncated, and Emacs-Mule and the stateless ISO-2022-JP variants answer every ctype query for a
|
|
110
|
+
* multi-byte code with "true" (enc/emacs_mule.c returns code_to_mbclen(code) > 1 regardless of the ctype asked for). Falling back to
|
|
111
|
+
* the switch loses nothing: in an ASCII-compatible encoding a multi-byte codepoint always starts at 0x8000 or above, past the U+3000
|
|
112
|
+
* the switch tops out at, so it only ever answers "not blank" there.
|
|
113
|
+
*/
|
|
114
|
+
static inline bool is_blank_codepoint(unsigned int codepoint, rb_encoding* enc, bool is_unicode) {
|
|
115
|
+
if (is_unicode || codepoint > MAX_CTYPE_CODEPOINT) return is_unicode_blank(codepoint);
|
|
116
|
+
return rb_enc_isspace(codepoint, enc) != 0;
|
|
117
|
+
}
|
|
116
118
|
|
|
117
|
-
|
|
119
|
+
/* Returns true if all blank. On false, sets *non_ascii_pos if non-ASCII found. NULL if non-blank ASCII found. */
|
|
120
|
+
static inline bool scan_ascii_blank(const unsigned char* ptr, size_t len, const unsigned char** non_ascii_pos) {
|
|
121
|
+
for (size_t i = 0; i < len; i++) {
|
|
118
122
|
unsigned char c = ptr[i];
|
|
119
123
|
if (c >= 0x80) {
|
|
120
124
|
*non_ascii_pos = ptr + i;
|
|
@@ -124,208 +128,315 @@ static bool check_blank_avx2(const unsigned char *ptr, size_t len, const unsigne
|
|
|
124
128
|
return false;
|
|
125
129
|
}
|
|
126
130
|
}
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
127
133
|
|
|
134
|
+
/* No non-ASCII position to report: a byte of 0x80 or above is not an ASCII blank either, so it settles the answer on its own. */
|
|
135
|
+
static inline bool scan_ascii_blank_or_null(const unsigned char* ptr, size_t len) {
|
|
136
|
+
for (size_t i = 0; i < len; i++) {
|
|
137
|
+
if (!is_ascii_blank_or_null_char(ptr[i])) {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
128
141
|
return true;
|
|
129
142
|
}
|
|
130
|
-
#endif
|
|
131
143
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
144
|
+
/*
|
|
145
|
+
* Every SIMD path below has the same shape: a chunk helper answers "is this whole chunk blank?" for one register of bytes, and a
|
|
146
|
+
* check_*() walks the string chunk by chunk. The blank? helpers also hand back the first non-blank byte when it is non-ASCII, which is
|
|
147
|
+
* where rb_str_blank() picks the decode up.
|
|
148
|
+
*
|
|
149
|
+
* The range test folds 0x09..0x0d into one comparison: the unsigned subtraction wraps every byte below 0x09 up past the span, so
|
|
150
|
+
* min(c - 0x09, 0x04) == c - 0x09 holds inside the range and nowhere else. Space is compared on its own, and ascii_blank? adds NUL.
|
|
151
|
+
*
|
|
152
|
+
* The bytes left over when the length is not a whole number of chunks are covered by re-reading the final chunk at [len - chunk, len),
|
|
153
|
+
* rather than walking them one at a time. What the re-read repeats is already known to be blank, so it cannot change the answer, and
|
|
154
|
+
* measuring back from the end keeps the load inside the string. It does need a whole chunk to look back on, which is why each check_*()
|
|
155
|
+
* hands anything shorter to the scalar scanner before the loop: that guard is what keeps the load in bounds, not a speed heuristic.
|
|
156
|
+
*/
|
|
157
|
+
#if defined(SIN_FAST_BLANK_SSE2)
|
|
158
|
+
static inline bool sse2_chunk_blank(const unsigned char* chunk_ptr, const unsigned char** non_ascii_pos) {
|
|
159
|
+
const __m128i ws_base = _mm_set1_epi8(ASCII_WS_RANGE_MIN);
|
|
160
|
+
const __m128i ws_span = _mm_set1_epi8(ASCII_WS_RANGE_MAX - ASCII_WS_RANGE_MIN);
|
|
161
|
+
const __m128i space = _mm_set1_epi8(ASCII_WS_SPACE);
|
|
162
|
+
|
|
163
|
+
__m128i chunk = _mm_loadu_si128((const __m128i*)chunk_ptr);
|
|
164
|
+
__m128i adjusted = _mm_sub_epi8(chunk, ws_base);
|
|
165
|
+
__m128i in_range = _mm_cmpeq_epi8(_mm_min_epu8(adjusted, ws_span), adjusted);
|
|
166
|
+
__m128i is_sp = _mm_cmpeq_epi8(chunk, space);
|
|
167
|
+
__m128i is_blank = _mm_or_si128(in_range, is_sp);
|
|
168
|
+
|
|
169
|
+
int mask = _mm_movemask_epi8(is_blank);
|
|
170
|
+
if (mask == 0xFFFF) return true;
|
|
171
|
+
|
|
172
|
+
int first = count_trailing_zeros((unsigned int)(~mask & 0xFFFF));
|
|
173
|
+
if (chunk_ptr[first] >= 0x80) {
|
|
174
|
+
*non_ascii_pos = chunk_ptr + first;
|
|
175
|
+
}
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
141
178
|
|
|
142
|
-
|
|
179
|
+
static inline bool sse2_chunk_ascii_blank(const unsigned char* chunk_ptr) {
|
|
180
|
+
const __m128i ws_base = _mm_set1_epi8(ASCII_WS_RANGE_MIN);
|
|
181
|
+
const __m128i ws_span = _mm_set1_epi8(ASCII_WS_RANGE_MAX - ASCII_WS_RANGE_MIN);
|
|
182
|
+
const __m128i space = _mm_set1_epi8(ASCII_WS_SPACE);
|
|
143
183
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
if (ptr[i + j] >= 0x80) {
|
|
151
|
-
*non_ascii_pos = ptr + i + j;
|
|
152
|
-
return false;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
}
|
|
184
|
+
__m128i chunk = _mm_loadu_si128((const __m128i*)chunk_ptr);
|
|
185
|
+
__m128i adjusted = _mm_sub_epi8(chunk, ws_base);
|
|
186
|
+
__m128i in_range = _mm_cmpeq_epi8(_mm_min_epu8(adjusted, ws_span), adjusted);
|
|
187
|
+
__m128i is_sp = _mm_cmpeq_epi8(chunk, space);
|
|
188
|
+
__m128i is_null = _mm_cmpeq_epi8(chunk, _mm_setzero_si128());
|
|
189
|
+
__m128i is_blank = _mm_or_si128(_mm_or_si128(in_range, is_sp), is_null);
|
|
156
190
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
is_blank = _mm_or_si128(is_blank, is_vt);
|
|
167
|
-
is_blank = _mm_or_si128(is_blank, is_ff);
|
|
168
|
-
is_blank = _mm_or_si128(is_blank, is_cr);
|
|
169
|
-
|
|
170
|
-
if (_mm_movemask_epi8(is_blank) != 0xFFFF) {
|
|
171
|
-
for (size_t j = 0; j < 16; j++) {
|
|
172
|
-
unsigned char c = ptr[i + j];
|
|
173
|
-
if (c >= 0x80) {
|
|
174
|
-
*non_ascii_pos = ptr + i + j;
|
|
175
|
-
return false;
|
|
176
|
-
}
|
|
177
|
-
if (!is_ascii_blank_char(c)) {
|
|
178
|
-
return false;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
}
|
|
191
|
+
return _mm_movemask_epi8(is_blank) == 0xFFFF;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
static inline bool check_blank_sse2(const unsigned char* ptr, size_t len, const unsigned char** non_ascii_pos) {
|
|
195
|
+
if (len < 16) return scan_ascii_blank(ptr, len, non_ascii_pos);
|
|
196
|
+
|
|
197
|
+
size_t i = 0;
|
|
198
|
+
for (; i + 16 <= len; i += 16) {
|
|
199
|
+
if (!sse2_chunk_blank(ptr + i, non_ascii_pos)) return false;
|
|
182
200
|
}
|
|
201
|
+
if (i == len) return true;
|
|
183
202
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
203
|
+
return sse2_chunk_blank(ptr + len - 16, non_ascii_pos);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
static inline bool check_ascii_blank_sse2(const unsigned char* ptr, size_t len) {
|
|
207
|
+
if (len < 16) return scan_ascii_blank_or_null(ptr, len);
|
|
208
|
+
|
|
209
|
+
size_t i = 0;
|
|
210
|
+
for (; i + 16 <= len; i += 16) {
|
|
211
|
+
if (!sse2_chunk_ascii_blank(ptr + i)) return false;
|
|
193
212
|
}
|
|
213
|
+
if (i == len) return true;
|
|
194
214
|
|
|
195
|
-
return
|
|
215
|
+
return sse2_chunk_ascii_blank(ptr + len - 16);
|
|
196
216
|
}
|
|
197
217
|
#endif
|
|
198
218
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
const
|
|
203
|
-
const
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
219
|
+
/* AVX2 implies SSE2 under both macro definitions above, so the sub-32-byte cases can fall back to the 16-byte chunks. */
|
|
220
|
+
#if defined(SIN_FAST_BLANK_AVX2)
|
|
221
|
+
SIN_FAST_BLANK_AVX2_TARGET static inline bool avx2_chunk_blank(const unsigned char* chunk_ptr, const unsigned char** non_ascii_pos) {
|
|
222
|
+
const __m256i ws_base = _mm256_set1_epi8(ASCII_WS_RANGE_MIN);
|
|
223
|
+
const __m256i ws_span = _mm256_set1_epi8(ASCII_WS_RANGE_MAX - ASCII_WS_RANGE_MIN);
|
|
224
|
+
const __m256i space = _mm256_set1_epi8(ASCII_WS_SPACE);
|
|
225
|
+
|
|
226
|
+
__m256i chunk = _mm256_loadu_si256((const __m256i*)chunk_ptr);
|
|
227
|
+
__m256i adjusted = _mm256_sub_epi8(chunk, ws_base);
|
|
228
|
+
__m256i in_range = _mm256_cmpeq_epi8(_mm256_min_epu8(adjusted, ws_span), adjusted);
|
|
229
|
+
__m256i is_sp = _mm256_cmpeq_epi8(chunk, space);
|
|
230
|
+
__m256i is_blank = _mm256_or_si256(in_range, is_sp);
|
|
231
|
+
|
|
232
|
+
int mask = _mm256_movemask_epi8(is_blank);
|
|
233
|
+
if (mask == -1) return true;
|
|
234
|
+
|
|
235
|
+
int first = count_trailing_zeros(~(unsigned int)mask);
|
|
236
|
+
if (chunk_ptr[first] >= 0x80) {
|
|
237
|
+
*non_ascii_pos = chunk_ptr + first;
|
|
238
|
+
}
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
208
241
|
|
|
209
|
-
|
|
242
|
+
SIN_FAST_BLANK_AVX2_TARGET static inline bool avx2_chunk_ascii_blank(const unsigned char* chunk_ptr) {
|
|
243
|
+
const __m256i ws_base = _mm256_set1_epi8(ASCII_WS_RANGE_MIN);
|
|
244
|
+
const __m256i ws_span = _mm256_set1_epi8(ASCII_WS_RANGE_MAX - ASCII_WS_RANGE_MIN);
|
|
245
|
+
const __m256i space = _mm256_set1_epi8(ASCII_WS_SPACE);
|
|
210
246
|
|
|
211
|
-
|
|
212
|
-
|
|
247
|
+
__m256i chunk = _mm256_loadu_si256((const __m256i*)chunk_ptr);
|
|
248
|
+
__m256i adjusted = _mm256_sub_epi8(chunk, ws_base);
|
|
249
|
+
__m256i in_range = _mm256_cmpeq_epi8(_mm256_min_epu8(adjusted, ws_span), adjusted);
|
|
250
|
+
__m256i is_sp = _mm256_cmpeq_epi8(chunk, space);
|
|
251
|
+
__m256i is_null = _mm256_cmpeq_epi8(chunk, _mm256_setzero_si256());
|
|
252
|
+
__m256i is_blank = _mm256_or_si256(_mm256_or_si256(in_range, is_sp), is_null);
|
|
213
253
|
|
|
214
|
-
|
|
215
|
-
|
|
254
|
+
return _mm256_movemask_epi8(is_blank) == -1;
|
|
255
|
+
}
|
|
216
256
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
if (ptr[i + j] >= 0x80) {
|
|
220
|
-
*non_ascii_pos = ptr + i + j;
|
|
221
|
-
return false;
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
}
|
|
257
|
+
SIN_FAST_BLANK_AVX2_TARGET static bool check_blank_avx2(const unsigned char* ptr, size_t len, const unsigned char** non_ascii_pos) {
|
|
258
|
+
if (len < 32) return check_blank_sse2(ptr, len, non_ascii_pos);
|
|
225
259
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
uint8x16_t is_vt = vceqq_u8(chunk, vt);
|
|
230
|
-
uint8x16_t is_ff = vceqq_u8(chunk, ff);
|
|
231
|
-
uint8x16_t is_cr = vceqq_u8(chunk, cr);
|
|
232
|
-
|
|
233
|
-
uint8x16_t is_blank = vorrq_u8(is_space, is_tab);
|
|
234
|
-
is_blank = vorrq_u8(is_blank, is_lf);
|
|
235
|
-
is_blank = vorrq_u8(is_blank, is_vt);
|
|
236
|
-
is_blank = vorrq_u8(is_blank, is_ff);
|
|
237
|
-
is_blank = vorrq_u8(is_blank, is_cr);
|
|
238
|
-
|
|
239
|
-
if (vminvq_u8(is_blank) == 0) {
|
|
240
|
-
for (size_t j = 0; j < 16; j++) {
|
|
241
|
-
unsigned char c = ptr[i + j];
|
|
242
|
-
if (c >= 0x80) {
|
|
243
|
-
*non_ascii_pos = ptr + i + j;
|
|
244
|
-
return false;
|
|
245
|
-
}
|
|
246
|
-
if (!is_ascii_blank_char(c)) {
|
|
247
|
-
return false;
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
}
|
|
260
|
+
size_t i = 0;
|
|
261
|
+
for (; i + 32 <= len; i += 32) {
|
|
262
|
+
if (!avx2_chunk_blank(ptr + i, non_ascii_pos)) return false;
|
|
251
263
|
}
|
|
264
|
+
if (i == len) return true;
|
|
252
265
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
266
|
+
return avx2_chunk_blank(ptr + len - 32, non_ascii_pos);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
SIN_FAST_BLANK_AVX2_TARGET static bool check_ascii_blank_avx2(const unsigned char* ptr, size_t len) {
|
|
270
|
+
if (len < 32) return check_ascii_blank_sse2(ptr, len);
|
|
271
|
+
|
|
272
|
+
size_t i = 0;
|
|
273
|
+
for (; i + 32 <= len; i += 32) {
|
|
274
|
+
if (!avx2_chunk_ascii_blank(ptr + i)) return false;
|
|
262
275
|
}
|
|
276
|
+
if (i == len) return true;
|
|
263
277
|
|
|
264
|
-
return
|
|
278
|
+
return avx2_chunk_ascii_blank(ptr + len - 32);
|
|
265
279
|
}
|
|
266
280
|
#endif
|
|
267
281
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
282
|
+
#if defined(SIN_FAST_BLANK_NEON)
|
|
283
|
+
static inline bool neon_chunk_blank(const unsigned char* chunk_ptr, const unsigned char** non_ascii_pos) {
|
|
284
|
+
const uint8x16_t ws_base = vdupq_n_u8(ASCII_WS_RANGE_MIN);
|
|
285
|
+
const uint8x16_t ws_span = vdupq_n_u8(ASCII_WS_RANGE_MAX - ASCII_WS_RANGE_MIN);
|
|
286
|
+
const uint8x16_t space = vdupq_n_u8(ASCII_WS_SPACE);
|
|
271
287
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
288
|
+
uint8x16_t chunk = vld1q_u8(chunk_ptr);
|
|
289
|
+
uint8x16_t adjusted = vsubq_u8(chunk, ws_base);
|
|
290
|
+
uint8x16_t in_range = vceqq_u8(vminq_u8(adjusted, ws_span), adjusted);
|
|
291
|
+
uint8x16_t is_sp = vceqq_u8(chunk, space);
|
|
292
|
+
uint8x16_t is_blank = vorrq_u8(in_range, is_sp);
|
|
276
293
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
294
|
+
if (vminvq_u8(is_blank) != 0) return true;
|
|
295
|
+
|
|
296
|
+
/* NEON has no movemask, so the scalar scanner locates the first non-blank byte. It cannot report true here. */
|
|
297
|
+
return scan_ascii_blank(chunk_ptr, 16, non_ascii_pos);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
static inline bool neon_chunk_ascii_blank(const unsigned char* chunk_ptr) {
|
|
301
|
+
const uint8x16_t ws_base = vdupq_n_u8(ASCII_WS_RANGE_MIN);
|
|
302
|
+
const uint8x16_t ws_span = vdupq_n_u8(ASCII_WS_RANGE_MAX - ASCII_WS_RANGE_MIN);
|
|
303
|
+
const uint8x16_t space = vdupq_n_u8(ASCII_WS_SPACE);
|
|
304
|
+
|
|
305
|
+
uint8x16_t chunk = vld1q_u8(chunk_ptr);
|
|
306
|
+
uint8x16_t adjusted = vsubq_u8(chunk, ws_base);
|
|
307
|
+
uint8x16_t in_range = vceqq_u8(vminq_u8(adjusted, ws_span), adjusted);
|
|
308
|
+
uint8x16_t is_sp = vceqq_u8(chunk, space);
|
|
309
|
+
uint8x16_t is_null = vceqq_u8(chunk, vdupq_n_u8(0));
|
|
310
|
+
uint8x16_t is_blank = vorrq_u8(vorrq_u8(in_range, is_sp), is_null);
|
|
311
|
+
|
|
312
|
+
return vminvq_u8(is_blank) != 0;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
static bool check_blank_neon(const unsigned char* ptr, size_t len, const unsigned char** non_ascii_pos) {
|
|
316
|
+
if (len < 16) return scan_ascii_blank(ptr, len, non_ascii_pos);
|
|
317
|
+
|
|
318
|
+
size_t i = 0;
|
|
319
|
+
for (; i + 16 <= len; i += 16) {
|
|
320
|
+
if (!neon_chunk_blank(ptr + i, non_ascii_pos)) return false;
|
|
280
321
|
}
|
|
322
|
+
if (i == len) return true;
|
|
281
323
|
|
|
282
|
-
return
|
|
324
|
+
return neon_chunk_blank(ptr + len - 16, non_ascii_pos);
|
|
283
325
|
}
|
|
284
326
|
|
|
285
|
-
static
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
327
|
+
static bool check_ascii_blank_neon(const unsigned char* ptr, size_t len) {
|
|
328
|
+
if (len < 16) return scan_ascii_blank_or_null(ptr, len);
|
|
329
|
+
|
|
330
|
+
size_t i = 0;
|
|
331
|
+
for (; i + 16 <= len; i += 16) {
|
|
332
|
+
if (!neon_chunk_ascii_blank(ptr + i)) return false;
|
|
289
333
|
}
|
|
334
|
+
if (i == len) return true;
|
|
335
|
+
|
|
336
|
+
return neon_chunk_ascii_blank(ptr + len - 16);
|
|
337
|
+
}
|
|
338
|
+
#endif
|
|
290
339
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
340
|
+
#if defined(SIN_FAST_BLANK_AVX2_DISPATCH)
|
|
341
|
+
typedef bool (*blank_check_func)(const unsigned char* ptr, size_t len, const unsigned char** non_ascii_pos);
|
|
342
|
+
typedef bool (*ascii_blank_check_func)(const unsigned char* ptr, size_t len);
|
|
294
343
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
344
|
+
/* Written once by Init_sin_fast_blank before the methods become callable, read-only afterwards. */
|
|
345
|
+
static blank_check_func check_blank_dispatch = check_blank_sse2;
|
|
346
|
+
static ascii_blank_check_func check_ascii_blank_dispatch = check_ascii_blank_sse2;
|
|
347
|
+
#endif
|
|
298
348
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
349
|
+
/*
|
|
350
|
+
* The len < 32 shortcut is check_blank_avx2's own first line, hoisted above the dispatch pointer. A call through a pointer cannot be
|
|
351
|
+
* inlined, and an AVX2-target function cannot be inlined into this caller either, so without the hoist a short string pays two calls to
|
|
352
|
+
* reach a scan the compiler would otherwise have inlined outright. The strings blank? is asked about are mostly short, and the condition
|
|
353
|
+
* is the one check_blank_avx2 already tests, so nothing longer changes path.
|
|
354
|
+
*/
|
|
355
|
+
static inline bool check_blank(const unsigned char* ptr, size_t len, const unsigned char** non_ascii_pos) {
|
|
356
|
+
#if defined(SIN_FAST_BLANK_AVX2_DISPATCH)
|
|
357
|
+
if (len < 32) return check_blank_sse2(ptr, len, non_ascii_pos);
|
|
358
|
+
return check_blank_dispatch(ptr, len, non_ascii_pos);
|
|
359
|
+
#elif defined(SIN_FAST_BLANK_AVX2)
|
|
360
|
+
return check_blank_avx2(ptr, len, non_ascii_pos);
|
|
361
|
+
#elif defined(SIN_FAST_BLANK_SSE2)
|
|
362
|
+
return check_blank_sse2(ptr, len, non_ascii_pos);
|
|
363
|
+
#elif defined(SIN_FAST_BLANK_NEON)
|
|
364
|
+
return check_blank_neon(ptr, len, non_ascii_pos);
|
|
305
365
|
#else
|
|
306
|
-
|
|
366
|
+
return scan_ascii_blank(ptr, len, non_ascii_pos);
|
|
307
367
|
#endif
|
|
368
|
+
}
|
|
308
369
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
370
|
+
static inline bool check_ascii_blank(const unsigned char* ptr, size_t len) {
|
|
371
|
+
#if defined(SIN_FAST_BLANK_AVX2_DISPATCH)
|
|
372
|
+
if (len < 32) return check_ascii_blank_sse2(ptr, len);
|
|
373
|
+
return check_ascii_blank_dispatch(ptr, len);
|
|
374
|
+
#elif defined(SIN_FAST_BLANK_AVX2)
|
|
375
|
+
return check_ascii_blank_avx2(ptr, len);
|
|
376
|
+
#elif defined(SIN_FAST_BLANK_SSE2)
|
|
377
|
+
return check_ascii_blank_sse2(ptr, len);
|
|
378
|
+
#elif defined(SIN_FAST_BLANK_NEON)
|
|
379
|
+
return check_ascii_blank_neon(ptr, len);
|
|
380
|
+
#else
|
|
381
|
+
return scan_ascii_blank_or_null(ptr, len);
|
|
382
|
+
#endif
|
|
383
|
+
}
|
|
312
384
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
385
|
+
/*
|
|
386
|
+
* Reached when the scanner cannot decode the bytes ahead. ActiveSupport's regexp never rescans: it trusts the code range Ruby cached on
|
|
387
|
+
* the string, so it raises only for a string Ruby itself calls broken. Ruby's Big5-HKSCS, Big5-UAO, CP950 and CP951 transcoders emit
|
|
388
|
+
* byte sequences their own scanner rejects while the code range still reads valid ('À'.encode('Big5-HKSCS')), and there ActiveSupport
|
|
389
|
+
* answers "not blank" rather than raising. Reading the cached code range costs nothing; computing an uncomputed one would scan the
|
|
390
|
+
* whole string, give up the early exit this loop exists for, and only ever come out broken anyway, since it runs the decode that just
|
|
391
|
+
* failed here.
|
|
392
|
+
*/
|
|
393
|
+
static VALUE blank_undecodable(VALUE str, rb_encoding* enc) {
|
|
394
|
+
if (ENC_CODERANGE(str) == ENC_CODERANGE_VALID) return Qfalse;
|
|
395
|
+
rb_raise(rb_eArgError, "invalid byte sequence in %s", rb_enc_name(enc));
|
|
396
|
+
}
|
|
316
397
|
|
|
317
|
-
|
|
318
|
-
|
|
398
|
+
static VALUE rb_str_blank(VALUE str) {
|
|
399
|
+
long len = RSTRING_LEN(str);
|
|
400
|
+
if (len == 0) return Qtrue;
|
|
319
401
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
402
|
+
const unsigned char* ptr = (const unsigned char*)RSTRING_PTR(str);
|
|
403
|
+
const unsigned char* end = ptr + len;
|
|
404
|
+
rb_encoding* enc = STR_ENC_GET(str);
|
|
323
405
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
406
|
+
bool asciicompat = rb_enc_asciicompat(enc) != 0;
|
|
407
|
+
if (asciicompat) {
|
|
408
|
+
const unsigned char* non_ascii_pos = NULL;
|
|
409
|
+
|
|
410
|
+
if (check_blank(ptr, (size_t)len, &non_ascii_pos)) return Qtrue;
|
|
411
|
+
if (non_ascii_pos == NULL) return Qfalse;
|
|
412
|
+
|
|
413
|
+
ptr = non_ascii_pos;
|
|
414
|
+
}
|
|
327
415
|
|
|
328
|
-
|
|
416
|
+
bool is_unicode = is_unicode_encoding(enc);
|
|
417
|
+
while (ptr < end) {
|
|
418
|
+
int clen = rb_enc_precise_mbclen((const char*)ptr, (const char*)end, enc);
|
|
419
|
+
if (!MBCLEN_CHARFOUND_P(clen)) return blank_undecodable(str, enc);
|
|
420
|
+
unsigned int codepoint = rb_enc_mbc_to_codepoint((const char*)ptr, (const char*)end, enc);
|
|
421
|
+
if (!is_blank_codepoint(codepoint, enc, is_unicode)) return Qfalse;
|
|
422
|
+
ptr += MBCLEN_CHARFOUND_LEN(clen);
|
|
423
|
+
|
|
424
|
+
/*
|
|
425
|
+
* An ASCII run starts here, so hand it back to the SIMD scan instead of decoding it a character at a time. Only an ASCII-compatible
|
|
426
|
+
* encoding may do this: anywhere else a byte below 0x80 is not a character on its own.
|
|
427
|
+
*
|
|
428
|
+
* Resuming the decode afterwards is safe too. The scan only ever hands back a position holding a byte of 0x80 or above, and every
|
|
429
|
+
* byte it passed was a single-byte blank, so the decode restarts on a character boundary. A non-blank ASCII byte settles the answer
|
|
430
|
+
* outright and leaves no position to hand back.
|
|
431
|
+
*/
|
|
432
|
+
if (asciicompat && ptr < end && *ptr < 0x80) {
|
|
433
|
+
const unsigned char* non_ascii_pos = NULL;
|
|
434
|
+
|
|
435
|
+
if (check_blank(ptr, (size_t)(end - ptr), &non_ascii_pos)) return Qtrue;
|
|
436
|
+
if (non_ascii_pos == NULL) return Qfalse;
|
|
437
|
+
|
|
438
|
+
ptr = non_ascii_pos;
|
|
439
|
+
}
|
|
329
440
|
}
|
|
330
441
|
|
|
331
442
|
return Qtrue;
|
|
@@ -333,46 +444,47 @@ static VALUE rb_str_blank(VALUE str) {
|
|
|
333
444
|
|
|
334
445
|
static VALUE rb_str_ascii_blank(VALUE str) {
|
|
335
446
|
long len = RSTRING_LEN(str);
|
|
336
|
-
if (len == 0)
|
|
337
|
-
return Qtrue;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
const char *ptr = RSTRING_PTR(str);
|
|
341
|
-
const char *end = ptr + len;
|
|
342
|
-
rb_encoding *enc = STR_ENC_GET(str);
|
|
447
|
+
if (len == 0) return Qtrue;
|
|
343
448
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
449
|
+
const unsigned char* ptr = (const unsigned char*)RSTRING_PTR(str);
|
|
450
|
+
const unsigned char* end = ptr + len;
|
|
451
|
+
rb_encoding* enc = STR_ENC_GET(str);
|
|
347
452
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
return Qtrue;
|
|
358
|
-
}
|
|
453
|
+
/*
|
|
454
|
+
* This one never raises, because bytes that decode to nothing are no more an ASCII blank than the characters they failed to form.
|
|
455
|
+
* An ASCII-compatible encoding does not even need the decoder: a byte of 0x80 or above only ever starts a character whose codepoint
|
|
456
|
+
* is 0x80 or above there, so the first one settles the answer.
|
|
457
|
+
*/
|
|
458
|
+
if (rb_enc_asciicompat(enc)) return check_ascii_blank(ptr, (size_t)len) ? Qtrue : Qfalse;
|
|
359
459
|
|
|
360
|
-
FULL_CHECK:;
|
|
361
460
|
while (ptr < end) {
|
|
362
|
-
int clen;
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
if (codepoint != 0 && !rb_isspace(codepoint))
|
|
366
|
-
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
ptr += clen;
|
|
461
|
+
int clen = rb_enc_precise_mbclen((const char*)ptr, (const char*)end, enc);
|
|
462
|
+
if (!MBCLEN_CHARFOUND_P(clen)) return Qfalse;
|
|
463
|
+
unsigned int codepoint = rb_enc_mbc_to_codepoint((const char*)ptr, (const char*)end, enc);
|
|
464
|
+
if (codepoint != 0 && !rb_isspace(codepoint)) return Qfalse;
|
|
465
|
+
ptr += MBCLEN_CHARFOUND_LEN(clen);
|
|
370
466
|
}
|
|
371
467
|
|
|
372
468
|
return Qtrue;
|
|
373
469
|
}
|
|
374
470
|
|
|
375
471
|
void Init_sin_fast_blank(void) {
|
|
472
|
+
/*
|
|
473
|
+
* Declaring this is a promise, and what backs it is that both methods only ever read the string they are handed: the dispatch pointers
|
|
474
|
+
* are written in this function and read everywhere else, and the code range is taken from the cache rather than computed, which would
|
|
475
|
+
* write the result back onto the string. It has to run before the definitions, because what it marks is whatever is defined after it.
|
|
476
|
+
*/
|
|
477
|
+
#ifdef HAVE_RB_EXT_RACTOR_SAFE
|
|
478
|
+
rb_ext_ractor_safe(true);
|
|
479
|
+
#endif
|
|
480
|
+
|
|
481
|
+
#if defined(SIN_FAST_BLANK_AVX2_DISPATCH)
|
|
482
|
+
if (__builtin_cpu_supports("avx2")) {
|
|
483
|
+
check_blank_dispatch = check_blank_avx2;
|
|
484
|
+
check_ascii_blank_dispatch = check_ascii_blank_avx2;
|
|
485
|
+
}
|
|
486
|
+
#endif
|
|
487
|
+
|
|
376
488
|
rb_define_method(rb_cString, "blank?", rb_str_blank, 0);
|
|
377
489
|
rb_define_method(rb_cString, "ascii_blank?", rb_str_ascii_blank, 0);
|
|
378
490
|
}
|