sin_fast_blank 4.0.1 → 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.
@@ -1,21 +1,57 @@
1
1
  #include <ruby.h>
2
2
  #include <ruby/encoding.h>
3
3
  #include <stdbool.h>
4
- #ifdef __SSE2__
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)
5
26
  #include <emmintrin.h>
6
27
  #endif
7
- #ifdef __AVX2__
28
+ #if defined(SIN_FAST_BLANK_AVX2)
8
29
  #include <immintrin.h>
9
30
  #endif
10
31
  #if defined(__ARM_NEON) && defined(__aarch64__)
32
+ #define SIN_FAST_BLANK_NEON 1
11
33
  #include <arm_neon.h>
12
34
  #endif
13
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); }
46
+ #endif
47
+ #endif
48
+
14
49
  #define STR_ENC_GET(str) rb_enc_from_index(ENCODING_GET(str))
15
50
 
16
51
  #define ASCII_WS_RANGE_MIN 0x09
17
52
  #define ASCII_WS_RANGE_MAX 0x0d
18
53
  #define ASCII_WS_SPACE 0x20
54
+ #define MAX_CTYPE_CODEPOINT 0xFF
19
55
 
20
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; }
21
57
 
@@ -56,6 +92,30 @@ static inline bool is_unicode_blank(unsigned int codepoint) {
56
92
  }
57
93
  }
58
94
 
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
+ }
103
+
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
+ }
118
+
59
119
  /* Returns true if all blank. On false, sets *non_ascii_pos if non-ASCII found. NULL if non-blank ASCII found. */
60
120
  static inline bool scan_ascii_blank(const unsigned char* ptr, size_t len, const unsigned char** non_ascii_pos) {
61
121
  for (size_t i = 0; i < len; i++) {
@@ -71,191 +131,269 @@ static inline bool scan_ascii_blank(const unsigned char* ptr, size_t len, const
71
131
  return true;
72
132
  }
73
133
 
74
- static inline bool scan_ascii_blank_or_null(const unsigned char* ptr, size_t len, const unsigned char** non_ascii_pos) {
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) {
75
136
  for (size_t i = 0; i < len; i++) {
76
- unsigned char c = ptr[i];
77
- if (c >= 0x80) {
78
- *non_ascii_pos = ptr + i;
79
- return false;
80
- }
81
- if (!is_ascii_blank_or_null_char(c)) {
137
+ if (!is_ascii_blank_or_null_char(ptr[i])) {
82
138
  return false;
83
139
  }
84
140
  }
85
141
  return true;
86
142
  }
87
143
 
88
- #ifdef __AVX2__
89
- static bool check_blank_avx2(const unsigned char* ptr, size_t len, const unsigned char** non_ascii_pos) {
90
- const __m256i ws_base = _mm256_set1_epi8(ASCII_WS_RANGE_MIN);
91
- const __m256i four = _mm256_set1_epi8(ASCII_WS_RANGE_MAX - ASCII_WS_RANGE_MIN);
92
- const __m256i space = _mm256_set1_epi8(ASCII_WS_SPACE);
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
+ }
178
+
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);
183
+
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);
190
+
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);
93
196
 
94
197
  size_t i = 0;
95
- for (; i + 31 < len; i += 32) {
96
- __m256i chunk = _mm256_loadu_si256((const __m256i*)(ptr + i));
97
- __m256i adjusted = _mm256_sub_epi8(chunk, ws_base);
98
- __m256i in_range = _mm256_cmpeq_epi8(_mm256_min_epu8(adjusted, four), adjusted);
99
- __m256i is_sp = _mm256_cmpeq_epi8(chunk, space);
100
- __m256i is_blank = _mm256_or_si256(in_range, is_sp);
101
-
102
- int mask = _mm256_movemask_epi8(is_blank);
103
- if (mask != -1) {
104
- int first = __builtin_ctz(~mask);
105
- unsigned char c = ptr[i + first];
106
- if (c >= 0x80) {
107
- *non_ascii_pos = ptr + i + first;
108
- }
109
- return false;
110
- }
198
+ for (; i + 16 <= len; i += 16) {
199
+ if (!sse2_chunk_blank(ptr + i, non_ascii_pos)) return false;
111
200
  }
201
+ if (i == len) return true;
112
202
 
113
- return scan_ascii_blank(ptr + i, len - i, non_ascii_pos);
203
+ return sse2_chunk_blank(ptr + len - 16, non_ascii_pos);
114
204
  }
115
205
 
116
- static bool check_ascii_blank_avx2(const unsigned char* ptr, size_t len, const unsigned char** non_ascii_pos) {
117
- const __m256i ws_base = _mm256_set1_epi8(ASCII_WS_RANGE_MIN);
118
- const __m256i four = _mm256_set1_epi8(ASCII_WS_RANGE_MAX - ASCII_WS_RANGE_MIN);
119
- const __m256i space = _mm256_set1_epi8(ASCII_WS_SPACE);
120
- const __m256i zero = _mm256_setzero_si256();
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);
121
208
 
122
209
  size_t i = 0;
123
- for (; i + 31 < len; i += 32) {
124
- __m256i chunk = _mm256_loadu_si256((const __m256i*)(ptr + i));
125
- __m256i adjusted = _mm256_sub_epi8(chunk, ws_base);
126
- __m256i in_range = _mm256_cmpeq_epi8(_mm256_min_epu8(adjusted, four), adjusted);
127
- __m256i is_sp = _mm256_cmpeq_epi8(chunk, space);
128
- __m256i is_null = _mm256_cmpeq_epi8(chunk, zero);
129
- __m256i is_blank = _mm256_or_si256(_mm256_or_si256(in_range, is_sp), is_null);
130
-
131
- int mask = _mm256_movemask_epi8(is_blank);
132
- if (mask != -1) {
133
- int first = __builtin_ctz(~mask);
134
- unsigned char c = ptr[i + first];
135
- if (c >= 0x80) {
136
- *non_ascii_pos = ptr + i + first;
137
- }
138
- return false;
139
- }
210
+ for (; i + 16 <= len; i += 16) {
211
+ if (!sse2_chunk_ascii_blank(ptr + i)) return false;
140
212
  }
213
+ if (i == len) return true;
141
214
 
142
- return scan_ascii_blank_or_null(ptr + i, len - i, non_ascii_pos);
215
+ return sse2_chunk_ascii_blank(ptr + len - 16);
143
216
  }
144
217
  #endif
145
218
 
146
- #ifdef __SSE2__
147
- static bool check_blank_sse2(const unsigned char* ptr, size_t len, const unsigned char** non_ascii_pos) {
148
- const __m128i ws_base = _mm_set1_epi8(ASCII_WS_RANGE_MIN);
149
- const __m128i four = _mm_set1_epi8(ASCII_WS_RANGE_MAX - ASCII_WS_RANGE_MIN);
150
- const __m128i space = _mm_set1_epi8(ASCII_WS_SPACE);
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
+ }
241
+
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);
246
+
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);
253
+
254
+ return _mm256_movemask_epi8(is_blank) == -1;
255
+ }
256
+
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);
151
259
 
152
260
  size_t i = 0;
153
- for (; i + 15 < len; i += 16) {
154
- __m128i chunk = _mm_loadu_si128((const __m128i*)(ptr + i));
155
- __m128i adjusted = _mm_sub_epi8(chunk, ws_base);
156
- __m128i in_range = _mm_cmpeq_epi8(_mm_min_epu8(adjusted, four), adjusted);
157
- __m128i is_sp = _mm_cmpeq_epi8(chunk, space);
158
- __m128i is_blank = _mm_or_si128(in_range, is_sp);
159
-
160
- int mask = _mm_movemask_epi8(is_blank);
161
- if (mask != 0xFFFF) {
162
- int first = __builtin_ctz(~mask & 0xFFFF);
163
- unsigned char c = ptr[i + first];
164
- if (c >= 0x80) {
165
- *non_ascii_pos = ptr + i + first;
166
- }
167
- return false;
168
- }
261
+ for (; i + 32 <= len; i += 32) {
262
+ if (!avx2_chunk_blank(ptr + i, non_ascii_pos)) return false;
169
263
  }
264
+ if (i == len) return true;
170
265
 
171
- return scan_ascii_blank(ptr + i, len - i, non_ascii_pos);
266
+ return avx2_chunk_blank(ptr + len - 32, non_ascii_pos);
172
267
  }
173
268
 
174
- static bool check_ascii_blank_sse2(const unsigned char* ptr, size_t len, const unsigned char** non_ascii_pos) {
175
- const __m128i ws_base = _mm_set1_epi8(ASCII_WS_RANGE_MIN);
176
- const __m128i four = _mm_set1_epi8(ASCII_WS_RANGE_MAX - ASCII_WS_RANGE_MIN);
177
- const __m128i space = _mm_set1_epi8(ASCII_WS_SPACE);
178
- const __m128i zero = _mm_setzero_si128();
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);
179
271
 
180
272
  size_t i = 0;
181
- for (; i + 15 < len; i += 16) {
182
- __m128i chunk = _mm_loadu_si128((const __m128i*)(ptr + i));
183
- __m128i adjusted = _mm_sub_epi8(chunk, ws_base);
184
- __m128i in_range = _mm_cmpeq_epi8(_mm_min_epu8(adjusted, four), adjusted);
185
- __m128i is_sp = _mm_cmpeq_epi8(chunk, space);
186
- __m128i is_null = _mm_cmpeq_epi8(chunk, zero);
187
- __m128i is_blank = _mm_or_si128(_mm_or_si128(in_range, is_sp), is_null);
188
-
189
- int mask = _mm_movemask_epi8(is_blank);
190
- if (mask != 0xFFFF) {
191
- int first = __builtin_ctz(~mask & 0xFFFF);
192
- unsigned char c = ptr[i + first];
193
- if (c >= 0x80) {
194
- *non_ascii_pos = ptr + i + first;
195
- }
196
- return false;
197
- }
273
+ for (; i + 32 <= len; i += 32) {
274
+ if (!avx2_chunk_ascii_blank(ptr + i)) return false;
198
275
  }
276
+ if (i == len) return true;
199
277
 
200
- return scan_ascii_blank_or_null(ptr + i, len - i, non_ascii_pos);
278
+ return avx2_chunk_ascii_blank(ptr + len - 32);
201
279
  }
202
280
  #endif
203
281
 
204
- #if defined(__ARM_NEON) && defined(__aarch64__)
205
- static bool check_blank_neon(const unsigned char* ptr, size_t len, const unsigned char** non_ascii_pos) {
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) {
206
284
  const uint8x16_t ws_base = vdupq_n_u8(ASCII_WS_RANGE_MIN);
207
- const uint8x16_t four = vdupq_n_u8(ASCII_WS_RANGE_MAX - ASCII_WS_RANGE_MIN);
285
+ const uint8x16_t ws_span = vdupq_n_u8(ASCII_WS_RANGE_MAX - ASCII_WS_RANGE_MIN);
208
286
  const uint8x16_t space = vdupq_n_u8(ASCII_WS_SPACE);
209
287
 
210
- size_t i = 0;
211
- for (; i + 15 < len; i += 16) {
212
- uint8x16_t chunk = vld1q_u8(ptr + i);
213
- uint8x16_t adjusted = vsubq_u8(chunk, ws_base);
214
- uint8x16_t in_range = vceqq_u8(vminq_u8(adjusted, four), adjusted);
215
- uint8x16_t is_sp = vceqq_u8(chunk, space);
216
- uint8x16_t is_blank = vorrq_u8(in_range, is_sp);
217
-
218
- if (vminvq_u8(is_blank) == 0) {
219
- if (!scan_ascii_blank(ptr + i, 16, non_ascii_pos)) return false;
220
- }
221
- }
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);
222
293
 
223
- return scan_ascii_blank(ptr + i, len - i, non_ascii_pos);
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);
224
298
  }
225
299
 
226
- static bool check_ascii_blank_neon(const unsigned char* ptr, size_t len, const unsigned char** non_ascii_pos) {
300
+ static inline bool neon_chunk_ascii_blank(const unsigned char* chunk_ptr) {
227
301
  const uint8x16_t ws_base = vdupq_n_u8(ASCII_WS_RANGE_MIN);
228
- const uint8x16_t four = vdupq_n_u8(ASCII_WS_RANGE_MAX - ASCII_WS_RANGE_MIN);
302
+ const uint8x16_t ws_span = vdupq_n_u8(ASCII_WS_RANGE_MAX - ASCII_WS_RANGE_MIN);
229
303
  const uint8x16_t space = vdupq_n_u8(ASCII_WS_SPACE);
230
- const uint8x16_t zero = vdupq_n_u8(0);
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);
231
317
 
232
318
  size_t i = 0;
233
- for (; i + 15 < len; i += 16) {
234
- uint8x16_t chunk = vld1q_u8(ptr + i);
235
- uint8x16_t adjusted = vsubq_u8(chunk, ws_base);
236
- uint8x16_t in_range = vceqq_u8(vminq_u8(adjusted, four), adjusted);
237
- uint8x16_t is_sp = vceqq_u8(chunk, space);
238
- uint8x16_t is_null = vceqq_u8(chunk, zero);
239
- uint8x16_t is_blank = vorrq_u8(vorrq_u8(in_range, is_sp), is_null);
240
-
241
- if (vminvq_u8(is_blank) == 0) {
242
- if (!scan_ascii_blank_or_null(ptr + i, 16, non_ascii_pos)) return false;
243
- }
319
+ for (; i + 16 <= len; i += 16) {
320
+ if (!neon_chunk_blank(ptr + i, non_ascii_pos)) return false;
321
+ }
322
+ if (i == len) return true;
323
+
324
+ return neon_chunk_blank(ptr + len - 16, non_ascii_pos);
325
+ }
326
+
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;
244
333
  }
334
+ if (i == len) return true;
245
335
 
246
- return scan_ascii_blank_or_null(ptr + i, len - i, non_ascii_pos);
336
+ return neon_chunk_ascii_blank(ptr + len - 16);
247
337
  }
248
338
  #endif
249
339
 
250
- #if !defined(__AVX2__) && !defined(__SSE2__) && !(defined(__ARM_NEON) && defined(__aarch64__))
251
- static bool check_blank_scalar(const unsigned char* ptr, size_t len, const unsigned char** non_ascii_pos) {
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);
343
+
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
348
+
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);
365
+ #else
252
366
  return scan_ascii_blank(ptr, len, non_ascii_pos);
367
+ #endif
253
368
  }
254
369
 
255
- static bool check_ascii_blank_scalar(const unsigned char* ptr, size_t len, const unsigned char** non_ascii_pos) {
256
- return scan_ascii_blank_or_null(ptr, len, non_ascii_pos);
257
- }
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);
258
382
  #endif
383
+ }
384
+
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
+ }
259
397
 
260
398
  static VALUE rb_str_blank(VALUE str) {
261
399
  long len = RSTRING_LEN(str);
@@ -265,31 +403,40 @@ static VALUE rb_str_blank(VALUE str) {
265
403
  const unsigned char* end = ptr + len;
266
404
  rb_encoding* enc = STR_ENC_GET(str);
267
405
 
268
- if (rb_enc_asciicompat(enc)) {
406
+ bool asciicompat = rb_enc_asciicompat(enc) != 0;
407
+ if (asciicompat) {
269
408
  const unsigned char* non_ascii_pos = NULL;
270
- bool is_blank = false;
271
-
272
- #ifdef __AVX2__
273
- is_blank = check_blank_avx2(ptr, (size_t)len, &non_ascii_pos);
274
- #elif defined(__SSE2__)
275
- is_blank = check_blank_sse2(ptr, (size_t)len, &non_ascii_pos);
276
- #elif defined(__ARM_NEON) && defined(__aarch64__)
277
- is_blank = check_blank_neon(ptr, (size_t)len, &non_ascii_pos);
278
- #else
279
- is_blank = check_blank_scalar(ptr, (size_t)len, &non_ascii_pos);
280
- #endif
281
409
 
282
- if (is_blank) return Qtrue;
410
+ if (check_blank(ptr, (size_t)len, &non_ascii_pos)) return Qtrue;
283
411
  if (non_ascii_pos == NULL) return Qfalse;
284
412
 
285
413
  ptr = non_ascii_pos;
286
414
  }
287
415
 
416
+ bool is_unicode = is_unicode_encoding(enc);
288
417
  while (ptr < end) {
289
- int clen;
290
- unsigned int codepoint = rb_enc_codepoint_len((const char*)ptr, (const char*)end, &clen, enc);
291
- if (!is_unicode_blank(codepoint)) return Qfalse;
292
- ptr += clen;
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
+ }
293
440
  }
294
441
 
295
442
  return Qtrue;
@@ -303,37 +450,41 @@ static VALUE rb_str_ascii_blank(VALUE str) {
303
450
  const unsigned char* end = ptr + len;
304
451
  rb_encoding* enc = STR_ENC_GET(str);
305
452
 
306
- if (rb_enc_asciicompat(enc)) {
307
- const unsigned char* non_ascii_pos = NULL;
308
- bool is_blank = false;
309
-
310
- #ifdef __AVX2__
311
- is_blank = check_ascii_blank_avx2(ptr, (size_t)len, &non_ascii_pos);
312
- #elif defined(__SSE2__)
313
- is_blank = check_ascii_blank_sse2(ptr, (size_t)len, &non_ascii_pos);
314
- #elif defined(__ARM_NEON) && defined(__aarch64__)
315
- is_blank = check_ascii_blank_neon(ptr, (size_t)len, &non_ascii_pos);
316
- #else
317
- is_blank = check_ascii_blank_scalar(ptr, (size_t)len, &non_ascii_pos);
318
- #endif
319
-
320
- if (is_blank) return Qtrue;
321
- if (non_ascii_pos == NULL) return Qfalse;
322
-
323
- ptr = non_ascii_pos;
324
- }
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;
325
459
 
326
460
  while (ptr < end) {
327
- int clen;
328
- unsigned int codepoint = rb_enc_codepoint_len((const char*)ptr, (const char*)end, &clen, enc);
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);
329
464
  if (codepoint != 0 && !rb_isspace(codepoint)) return Qfalse;
330
- ptr += clen;
465
+ ptr += MBCLEN_CHARFOUND_LEN(clen);
331
466
  }
332
467
 
333
468
  return Qtrue;
334
469
  }
335
470
 
336
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
+
337
488
  rb_define_method(rb_cString, "blank?", rb_str_blank, 0);
338
489
  rb_define_method(rb_cString, "ascii_blank?", rb_str_ascii_blank, 0);
339
490
  }
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SinFastBlank
4
+ VERSION = '5.0.0'
5
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ class String
4
+ # Explicitly undefine method before redefining to avoid Ruby warnings.
5
+ undef_method(:blank?) if method_defined?(:blank?)
6
+ end
7
+
8
+ case RUBY_ENGINE
9
+ when 'jruby'
10
+ require 'jruby'
11
+ require 'sin_fast_blank/sin_fast_blank.jar'
12
+
13
+ Java::sin_fast_blank::SinFastBlankLibrary.new.load(JRuby.runtime, false)
14
+ else
15
+ # The extension-less require lets Ruby resolve the platform-specific shared library suffix (.bundle on macOS, .so elsewhere) via DLEXT.
16
+ require 'sin_fast_blank/sin_fast_blank'
17
+ end
18
+
19
+ require 'sin_fast_blank/version'