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.
@@ -1,35 +1,65 @@
1
1
  #include <ruby.h>
2
2
  #include <ruby/encoding.h>
3
3
  #include <stdbool.h>
4
- #ifdef __SSE2__
5
- #include <emmintrin.h>
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
- #ifdef __AVX2__
8
- #include <immintrin.h>
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 ASCII_BLANK_TAB 0x09
17
- #define ASCII_BLANK_LF 0x0a
18
- #define ASCII_BLANK_VT 0x0b
19
- #define ASCII_BLANK_FF 0x0c
20
- #define ASCII_BLANK_CR 0x0d
21
- #define ASCII_BLANK_SPACE 0x20
22
-
23
- static inline bool is_ascii_blank_char(unsigned char c) {
24
- return c == ASCII_BLANK_SPACE ||
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 int is_unicode_blank(unsigned int codepoint) {
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 1;
89
+ return true;
60
90
  default:
61
- return 0;
91
+ return false;
62
92
  }
63
93
  }
64
94
 
65
- #ifdef __AVX2__
66
- static bool check_blank_avx2(const unsigned char *ptr, size_t len, const unsigned char **non_ascii_pos) {
67
- const __m256i ascii_mask = _mm256_set1_epi8(0x80);
68
- const __m256i space = _mm256_set1_epi8(ASCII_BLANK_SPACE);
69
- const __m256i tab = _mm256_set1_epi8(ASCII_BLANK_TAB);
70
- const __m256i lf = _mm256_set1_epi8(ASCII_BLANK_LF);
71
- const __m256i vt = _mm256_set1_epi8(ASCII_BLANK_VT);
72
- const __m256i ff = _mm256_set1_epi8(ASCII_BLANK_FF);
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
- __m256i is_space = _mm256_cmpeq_epi8(chunk, space);
91
- __m256i is_tab = _mm256_cmpeq_epi8(chunk, tab);
92
- __m256i is_lf = _mm256_cmpeq_epi8(chunk, lf);
93
- __m256i is_vt = _mm256_cmpeq_epi8(chunk, vt);
94
- __m256i is_ff = _mm256_cmpeq_epi8(chunk, ff);
95
- __m256i is_cr = _mm256_cmpeq_epi8(chunk, cr);
96
-
97
- __m256i is_blank = _mm256_or_si256(is_space, is_tab);
98
- is_blank = _mm256_or_si256(is_blank, is_lf);
99
- is_blank = _mm256_or_si256(is_blank, is_vt);
100
- is_blank = _mm256_or_si256(is_blank, is_ff);
101
- is_blank = _mm256_or_si256(is_blank, is_cr);
102
-
103
- if (_mm256_movemask_epi8(is_blank) != -1) {
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
- for (; i < len; i++) {
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
- #ifdef __SSE2__
133
- static bool check_blank_sse2(const unsigned char *ptr, size_t len, const unsigned char **non_ascii_pos) {
134
- const __m128i ascii_mask = _mm_set1_epi8(0x80);
135
- const __m128i space = _mm_set1_epi8(ASCII_BLANK_SPACE);
136
- const __m128i tab = _mm_set1_epi8(ASCII_BLANK_TAB);
137
- const __m128i lf = _mm_set1_epi8(ASCII_BLANK_LF);
138
- const __m128i vt = _mm_set1_epi8(ASCII_BLANK_VT);
139
- const __m128i ff = _mm_set1_epi8(ASCII_BLANK_FF);
140
- const __m128i cr = _mm_set1_epi8(ASCII_BLANK_CR);
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
- size_t i = 0;
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
- for (; i + 15 < len; i += 16) {
145
- __m128i chunk = _mm_loadu_si128((const __m128i *)(ptr + i));
146
-
147
- __m128i non_ascii = _mm_and_si128(chunk, ascii_mask);
148
- if (_mm_movemask_epi8(non_ascii) != 0) {
149
- for (size_t j = 0; j < 16; j++) {
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
- __m128i is_space = _mm_cmpeq_epi8(chunk, space);
158
- __m128i is_tab = _mm_cmpeq_epi8(chunk, tab);
159
- __m128i is_lf = _mm_cmpeq_epi8(chunk, lf);
160
- __m128i is_vt = _mm_cmpeq_epi8(chunk, vt);
161
- __m128i is_ff = _mm_cmpeq_epi8(chunk, ff);
162
- __m128i is_cr = _mm_cmpeq_epi8(chunk, cr);
163
-
164
- __m128i is_blank = _mm_or_si128(is_space, is_tab);
165
- is_blank = _mm_or_si128(is_blank, is_lf);
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
- for (; i < len; i++) {
185
- unsigned char c = ptr[i];
186
- if (c >= 0x80) {
187
- *non_ascii_pos = ptr + i;
188
- return false;
189
- }
190
- if (!is_ascii_blank_char(c)) {
191
- return false;
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 true;
215
+ return sse2_chunk_ascii_blank(ptr + len - 16);
196
216
  }
197
217
  #endif
198
218
 
199
- #ifdef __ARM_NEON
200
- static bool check_blank_neon(const unsigned char *ptr, size_t len, const unsigned char **non_ascii_pos) {
201
- const uint8x16_t ascii_mask = vdupq_n_u8(0x80);
202
- const uint8x16_t space = vdupq_n_u8(ASCII_BLANK_SPACE);
203
- const uint8x16_t tab = vdupq_n_u8(ASCII_BLANK_TAB);
204
- const uint8x16_t lf = vdupq_n_u8(ASCII_BLANK_LF);
205
- const uint8x16_t vt = vdupq_n_u8(ASCII_BLANK_VT);
206
- const uint8x16_t ff = vdupq_n_u8(ASCII_BLANK_FF);
207
- const uint8x16_t cr = vdupq_n_u8(ASCII_BLANK_CR);
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
- size_t i = 0;
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
- for (; i + 15 < len; i += 16) {
212
- uint8x16_t chunk = vld1q_u8(ptr + i);
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
- uint8x16_t non_ascii = vandq_u8(chunk, ascii_mask);
215
- uint8x16_t has_non_ascii = vceqq_u8(non_ascii, ascii_mask);
254
+ return _mm256_movemask_epi8(is_blank) == -1;
255
+ }
216
256
 
217
- if (vmaxvq_u8(has_non_ascii) != 0) {
218
- for (size_t j = 0; j < 16; j++) {
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
- uint8x16_t is_space = vceqq_u8(chunk, space);
227
- uint8x16_t is_tab = vceqq_u8(chunk, tab);
228
- uint8x16_t is_lf = vceqq_u8(chunk, lf);
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
- for (; i < len; i++) {
254
- unsigned char c = ptr[i];
255
- if (c >= 0x80) {
256
- *non_ascii_pos = ptr + i;
257
- return false;
258
- }
259
- if (!is_ascii_blank_char(c)) {
260
- return false;
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 true;
278
+ return avx2_chunk_ascii_blank(ptr + len - 32);
265
279
  }
266
280
  #endif
267
281
 
268
- static bool check_blank_scalar(const unsigned char *ptr, size_t len, const unsigned char **non_ascii_pos) {
269
- for (size_t i = 0; i < len; i++) {
270
- unsigned char c = ptr[i];
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
- if (c >= 0x80) {
273
- *non_ascii_pos = ptr + i;
274
- return false;
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
- if (!is_ascii_blank_char(c)) {
278
- return false;
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 true;
324
+ return neon_chunk_blank(ptr + len - 16, non_ascii_pos);
283
325
  }
284
326
 
285
- static VALUE rb_str_blank(VALUE str) {
286
- long len = RSTRING_LEN(str);
287
- if (len == 0) {
288
- return Qtrue;
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
- const unsigned char *ptr = (const unsigned char *)RSTRING_PTR(str);
292
- const unsigned char *end = ptr + len;
293
- rb_encoding *enc = STR_ENC_GET(str);
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
- if (rb_enc_asciicompat(enc)) {
296
- const unsigned char *non_ascii_pos = NULL;
297
- bool is_blank = false;
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
- #ifdef __AVX2__
300
- is_blank = check_blank_avx2(ptr, len, &non_ascii_pos);
301
- #elif defined(__SSE2__)
302
- is_blank = check_blank_sse2(ptr, len, &non_ascii_pos);
303
- #elif defined(__ARM_NEON)
304
- is_blank = check_blank_neon(ptr, len, &non_ascii_pos);
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
- is_blank = check_blank_scalar(ptr, len, &non_ascii_pos);
366
+ return scan_ascii_blank(ptr, len, non_ascii_pos);
307
367
  #endif
368
+ }
308
369
 
309
- if (is_blank) {
310
- return Qtrue;
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
- if (non_ascii_pos == NULL) {
314
- return Qfalse;
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
- ptr = (const unsigned char *)non_ascii_pos;
318
- }
398
+ static VALUE rb_str_blank(VALUE str) {
399
+ long len = RSTRING_LEN(str);
400
+ if (len == 0) return Qtrue;
319
401
 
320
- while ((const char *)ptr < (const char *)end) {
321
- int clen;
322
- unsigned int codepoint = rb_enc_codepoint_len((const char *)ptr, (const char *)end, &clen, enc);
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
- if (!is_unicode_blank(codepoint)) {
325
- return Qfalse;
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
- ptr += clen;
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
- if (rb_enc_asciicompat(enc)) {
345
- for (; ptr < end; ptr++) {
346
- unsigned char c = (unsigned char)*ptr;
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
- if (c >= 0x80) {
349
- goto FULL_CHECK;
350
- }
351
-
352
- if (!rb_isspace(c) && c != 0) {
353
- return Qfalse;
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
- unsigned int codepoint = rb_enc_codepoint_len(ptr, end, &clen, enc);
364
-
365
- if (codepoint != 0 && !rb_isspace(codepoint)) {
366
- return Qfalse;
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
  }