
Недавно меня заинтересовала такая задача: как лучше всего определить, что в строке есть гласная?
Казалось бы, тривиальный вопрос, правда?
Но начав разбираться, я осознал, что задача гораздо глубже. Я бросил себе вызов: придумать как можно больше способов обнаружения гласной. Я даже попросил присоединиться ко мне нескольких друзей. Какой способ самый быстрый? Каким никогда не стоит пользоваться? Какой самый умный? Какой самый удобочитаемый?
В этом посте я рассмотрю 11 способов обнаружения гласных, алгоритмический анализ, дизассемблирование байт-кода Python, реализацию CPython и даже исследую опкоды скомпилированного регулярного выражения. Поехали!
def has_vowels(s: str) -> bool:
...
Это каркас функции, который я буду заполнять. Если во входящей строке есть хотя бы одна гласная, возвращаем True, в противном случае — False. Код из этого поста выложен на GitHub.
Способ 1: цикл For
Я начал с самого наивного решения:
def loop_in(s):
for c in s.lower():
if c in "aeiou":
return True
return False
Оно логично, понятно и, предположительно, не вызывает проблем с производительностью.
Только мне не нравится, что она вызывает lower()
, которая всегда создаёт копию (добавление к строке иногда приводит к изменению её самой, но не в случае lower
). Это можно легко исправить:
def loop_in(s):
for c in s:
if c in "aeiouAEIOU":
return True
return False
Способ 2: цикл For в стиле C
Менее Pythonic-вариация нашего цикла for
:
def loop_or(s):
for c in s.lower():
if c == 'a' or c == 'e' or c == 'i' or c == 'o' or c == 'u':
return True
return False
Способ 3: вложенный цикл for
И если мы хотим охватить все варианты, то стоит попробовать и вложенный цикл for
:
def nested_loop(s):
vowels = "aeiouAEIOU"
for c in s:
for v in vowels:
if c == v:
return True
return False
Способ 4: пересечение множеств
А это уже интереснее. Вот моё любимое решение из тех, которые мне удалось придумать:
def set_intersection(s):
return set(s) & set("aeiouAEIOU")
Оно короткое, чистое и чуть более умное.
Помещаем входящую строку в множество, гласные — в другое множество и выполняем пересечение множеств. Множества достаточно эффективны. Вставка в первое множество выполняется за O(n), второе множество имеет постоянную длину, поэтому для его создания требуется O(1), а операция нахождения пересечения множеств пропорциональна наименьшему из двух множеств, имеющему постоянную длину, поэтому O(1), и общая временная сложность составляет O(n).
Однако этот код будет обрабатывать всю входящую строку, а не прекратит работу при нахождении первой гласной. В случае очень длинных строк, в которых гласные встречаются почти в самом начале, эта функция будет выполнять кучу бесполезной работы.
Способ 5: выражение-генератор
def any_gen(s):
return any(c in "aeiouAEIOU" for c in s)
Программисты любят однострочники. Я установил канал связи со своим внутренним пайтонистом и придумал такое выражение-генератор. Думаю, производительность будет эквивалентна показателям первого способа плюс немного оверхеда на объект-генератор.
Способ 6: рекурсия
Теперь, когда мы начинаем мыслить функционально, пришла пора попробовать рекурсию:
def recursion(s):
if not s:
return False
return s[0] in "aeiouAEIOU" or recursion(s[1:])
Это не придётся по душе CPython. Код создаст по новой строке для каждого символа входящей строки и вылетит, если длина строки превысит 1000 (максимальный предел рекурсии).
Способ 7: поиск регулярным выражением
Каждый раз, когда дело касается строк, кто-нибудь обязательно советует использовать регулярные выражения.
import re
def regex(s):
return bool(re.search(r'[aeiouAEIOU]', s))
Я ожидаю, что в лучшем случае производительность этого решения будет на уровне цикла в стиле C с небольшим оверхедом на инициализацию.
Способ 8: удаление регулярным выражением
import re
def regex_replace(s):
return len(re.sub(r'[aeiouAEIOU]', '', s)) != len(s)
Функция удаляет все гласные, а затем сравнивает длины строк, чтобы проверить, пропало ли что-то. Она неэффективна, потому что не выполняет ранний выход и создаёт копию строки.
Способ 9: фильтр
Все очевидные решения закончились. Что делать дальше?
def filter_lambda(s):
return bool(list(filter(lambda x: x in "aeiouAEIOU", s)))
Это сработает, но будет затратно и не обеспечит раннего выхода.
Способ 10: map
Похожее решение, но, возможно, чуть получше:
def map_lambda(s):
return any(map(lambda x: x in "aeiouAEIOU", s))
Использование лямбда-выражений делает вас круче, но будет ли производительность лучше того, что мы придумали раньше? В книге «Effective Python» говорится следующее: «списковые включения (list comprehension) чище, чем map и встроенные функции фильтров, потому что они не требуют лямбда-выражений». Поэтому код менее читаем и, вероятно, менее эффективен, чем в некоторых из предыдущих способов.
Способ 11: простые числа
Один из моих бывших студентов Грегори Кройсдейл придумал очень творческое и неожиданное решение:
primes = [i for i in range(2, 1000) if factorial(i - 1) % i == (i - 1)]
def prime(s: str) -> bool:
vowels = "aeiouAEIOU"
vowel_primes_dict = {c: primes[ord(c)] for c in vowels}
try:
s_num = reduce(lambda acc, v: acc * primes[ord(v)], s, 1)
v_num = reduce(lambda acc, v: acc * vowel_primes_dict[v], vowels, 1)
return gcd(s_num, v_num) != 1
except Exception:
return False
Оно сопоставляет каждый символ входящей строки и каждую гласную с уникальным простым числом, кодирует входящую строку как произведение простых чисел её символов и возвращает True, если наибольший общий делитель этого произведения с произведением простых чисел гласных больше 1 (то есть у них есть хотя бы одно общее простое число гласной). ?
Способ 12: потоки
А может, нам распараллелить поиск при помощи потоков? Разбить строку на n подстроки и использовать один из приведённых выше способов со всеми подстроками. Я попробовал так сделать. Это оказалось о-о-очень медленно. Возможно, мы бы что-то выиграли, если бы строки были огромными (например, больше 1 ГБ) и я бы отключил GIL.
Бенчмарк
Итак, 11 способов — это весь спектр решений, которые мне удалось придумать. Настало время провести их бенчмаркинг!
Я сравнивал все эти функции при помощи случайных строк, с гласными и без них, разной длины. Бенчмарк генерировал 2000 случайных строк (половина из них не содержала гласных) и выполнял каждый из способов для строки пять раз. Выполнялось три таких прогона и фиксировалось наименьшее среди них общее время.
Сначала я попробовал строки с длиной 10, потому что контекст проверки на гласные был связан с именами пользователей.
Функция |
Время (секунды) |
---|---|
loop_in |
0.001219 |
regex |
0.002039 |
any_gen |
0.002735 |
regex_replace |
0.003047 |
map_lambda |
0.003179 |
recursion |
0.004066 |
filter_lambda |
0.004234 |
set_intersection |
0.004465 |
loop_or |
0.008870 |
nested_loop |
0.010895 |
prime |
0.016482 |
Хм, выглядит логично. При такой длине все способы кажутся быстрыми. Можно сделать следующие наблюдения: простая loop_in
победила, но очень похожая loop_or
довольно медленная. Поиск регулярным выражением быстр, намного быстрее, чем я ожидал изначально. Моё любимое пересечение множеств довольно быстрое в реальных сценариях, но в рейтинге выглядит не очень хорошо, его побеждает даже рекурсия. И совершенно неудивительно, что шутка с простыми числами медленная. Но не такая уж медленная
С увеличением длины строк разброс становится гораздо очевиднее. Вот результаты при длине строки 100:
Функция |
Время (секунды) |
---|---|
regex |
0.003778 |
regex_replace |
0.005480 |
loop_in |
0.008526 |
any_gen |
0.015479 |
set_intersection |
0.015495 |
map_lambda |
0.021085 |
filter_lambda |
0.026546 |
recursion |
0.040774 |
prime |
0.077477 |
loop_or |
0.082003 |
nested_loop |
0.104956 |
Оба способа с регулярными выражениями вырвались вперёд! Хм, простые числа теперь не последние? Самыми медленными оказались цикл с or
и вложенные циклы. Ого!
Вот полная таблица для длин 10, 100, 1000 и 10000.
Функция |
Длина 10 |
Длина 100 |
Длина 1000 |
Длина 10000 |
---|---|---|---|---|
regex |
0.002079 |
0.003778 |
0.020088 |
0.181247 |
regex_replace |
0.003012 |
0.005480 |
0.027443 |
0.245306 |
set_intersection |
0.004241 |
0.015495 |
0.082475 |
0.601355 |
loop_in |
0.001170 |
0.008526 |
0.076880 |
0.763442 |
any_gen |
0.002662 |
0.015479 |
0.137218 |
1.356772 |
map_lambda |
0.003090 |
0.021085 |
0.192258 |
1.915244 |
filter_lambda |
0.004305 |
0.026546 |
0.244302 |
2.424177 |
loop_or |
0.007713 |
0.082003 |
0.769124 |
7.814960 |
nested_loop |
0.008718 |
0.104956 |
0.797087 |
8.455867 |
prime |
0.016113 |
0.077477 |
2.619118 |
203.579320 |
recursion |
0.004064 |
0.040774 |
Не работает |
Не работает |
График тех же данных:

Способы с регулярными выражениями невероятно быстры для любой длины строк. Циклы в стиле C очень медленные. Лично я ожидал, что regex будут с ними сравнимы по скорости! Время способа с простыми числами резко возрастает, а всё остальное работает вполне неплохо.
Но меня интересовало и то, как на результаты повлияет то, что в строках будут редко встречаться гласные. Я снова провёл бенчмарк со строками, в которых гласные всегда располагались в последних 10% символов.
Функция |
Длина 10 |
Длина 100 |
Длина 1000 |
Длина 10000 |
---|---|---|---|---|
regex |
0.002083 |
0.004288 |
0.025301 |
0.235313 |
regex_replace |
0.002950 |
0.005264 |
0.027415 |
0.244298 |
set_intersection |
0.004346 |
0.015110 |
0.080171 |
0.660243 |
loop_in |
0.001444 |
0.011158 |
0.100641 |
0.989452 |
any_gen |
0.003282 |
0.019758 |
0.179111 |
1.770298 |
map_lambda |
0.003757 |
0.026654 |
0.252468 |
2.528173 |
filter_lambda |
0.004106 |
0.026335 |
0.244043 |
2.424267 |
loop_or |
0.011777 |
0.123697 |
1.029399 |
10.184211 |
nested_loop |
0.010682 |
0.106838 |
1.046352 |
10.762563 |
prime |
0.016026 |
0.076423 |
2.605674 |
205.035926 |
recursion |
0.005025 |
0.053011 |
Не работает |
Не работает |
Regex по-прежнему доминируют, но моё любимое пересечение множеств проявило себя гораздо лучше.
Также я сравнил способ re.search с предварительно скомпилированным регулярным выражением (re.compile). Для коротких строк разница достаточно велика (0,009 с против 0,2 с для 100000 вызовов при длине строк 10), но для длинных строк несущественна (0,234 с против 0,235 с для 10000 вызовов при длине строк 1000). На самом деле, CPython всегда компилирует регулярные выражения и кэширует их, даже если мы не вызываем компиляцию в явном виде. [См. логику в re/__init__.py.] Разница в том, включаем ли мы во время бенчмарка работу по компиляции и поиск в кэше.
Подведём итог: для очень коротких строк loop_in
самая быстрая. При длине 25 она на одном уровне с регулярными выражениями. При более длинных строках regex вырываются вперёд. При сравнении loop_in
с set_intersection
, когда гласные встречаются в начале строк, loop_in
побеждает. Если строки длинные (>500 символов) или гласные в них встречаются редко, то set_intersection
побеждает.
Весь код можно найти на GitHub.
Почему регулярные выражения настолько быстрые?
Меня очень удивило, что регулярки настолько быстрее остальных способов. Я ожидал затрат на оверхед, из-за чего они сравнятся по скорости с простым циклом. Мне обязательно нужно было это исследовать.
Что здесь происходит? Давайте исследуем байт-код Python этих способов.
def has_vowel(s):
for c in s:
if c in "aeiouAEIOU":
return True
return False
import re
def has_vowel(s):
return bool(re.search(r'[aeiouAEIOU]', s))
Байт-код способа с циклом:
LOAD_FAST s
GET_ITER
L1 FOR_ITER
STORE_FAST c
LOAD_FAST c
LOAD_CONST 'aeiouAEIOU'
CONTAINS_OP 0
POP_JUMP_IF_TRUE L2
JUMP_BACKWARD L1
L2 LOAD_FAST c
SWAP
POP_TOP
RETURN_VALUE
L3 END_FOR
POP_TOP
RETURN_CONST None
«Мясо» алгоритма состоит из 7 опкодов, начинающихся с метки L1. Давайте разберём их один за другим. FOR_ITER
извлекает итератор из стека и пытается получить следующий символ (если его нет, то выполняется переход к L3). STORE_FAST
сохраняет текущий символ в локальную переменную. LOAD_FAST
помещает символ обратно в стек. LOAD_CONST
записывает строку гласных в стек. CONTAINS_OP
извлекает из стека два элемента, выполняет проверку in
и записывает в стек True или False. POP_JUMP_IF_TRUE
выполняет переход к L2, если символ был гласной, а в противном случае продолжает выполнение. JUMP_BACKWARD
переходит обратно к L1 для повторения процесса.
Эти 7 опкодов выполняются для каждого символа. Они просты (за исключением, возможно, CONTAINS_OP
), хотя избыточная работа и определённо выполняется (например, запись каждый раз строки гласных в стек).
Сравним это с байт-кодом регулярного выражения:
LOAD_GLOBAL re
LOAD_ATTR search
PUSH_NULL
LOAD_CONST '[aeiouAEIOU]'
LOAD_FAST s
CALL 2
RETURN_VALUE
Он просто вызывает функцию C. Изучив реализацию движка регулярных выражений CPython (sre.c и sre_lib.h), можно увидеть, что она создаёт внутреннее представление регулярного выражения, итерирует при помощи единственного цикла for
и использует поиск по таблице (не вложенный цикл). Соответствующий код находится в блоке if
строки 1825 в sre_lib.h:

Я хотел понять, как выглядит внутреннее представление регулярного выражения, поэтому выполнил re.compile("[aeiouAEIOU]", re.DEBUG)
. Вывод был таким:
IN
LITERAL 97
LITERAL 101
LITERAL 105
LITERAL 111
LITERAL 117
LITERAL 65
LITERAL 69
LITERAL 73
LITERAL 79
LITERAL 85
0: INFO 14 0b100 1 1 (to 15)
in
5: CHARSET [0x00000000, 0x00000000, 0x00208222, 0x00208222,
0x00000000, 0x00000000, 0x00000000, 0x00000000]
14: FAILURE
15: IN 11 (to 27)
17: CHARSET [0x00000000, 0x00000000, 0x00208222, 0x00208222,
0x00000000, 0x00000000, 0x00000000, 0x00000000]
26: FAILURE
27: SUCCESS
Литералы — это отдельные гласные в верхнем и нижнем регистре. CHARSET
выполняет единственную операцию поиска, чтобы при помощи битовой карты проверить, является ли текущий символ гласной. Очень умное решение! (Я не знаю, зачем производится вторая проверка, а не просто продолжается выполнение.)
Огромная разница в производительности вызвана сочетанием оверхеда интерпретатора и оптимизированным движком регулярных выражений.
Это было очень интересное исследование. Результаты оказались неожиданными для меня. Регулярные выражения очень быстры, а не-Pythonic код по сравнению с ними может показаться медленным. В конечном итоге, должно ли это повлиять на то, как вы выполняете поиск по строкам? Не обязательно. Пишите код так, как вам проще будет поддерживать, если только вы не работаете с миллионами строк и вам не важны миллисекунды. Но всё равно было очень увлекательно узнать, как это работает.
Теперь я могу ответить на вопрос: как же быстрее всего обнаружить гласную в строке? Похоже, при помощи битовой карты на C.
Дополнение 1: Kranar выяснил, что find()
, языка Python, реализованная в fastsearch.h на C, существенно обгоняет регулярные выражения. Это ещё раз подтверждает тот факт, что причиной большой разницы в производительности оказывается интерпретатор CPython.
def vowel_find(s):
for c in "aeiouAEIOU":
if s.find(c) != -1:
return True
return False
Дополнение 2: a_e_k нашёл ещё более совершенное решение, оказавшееся простым и изящным! Если просто поменять местами циклы, то код будет гораздо быстрее, чем в решениях с regex и find
:
def loop_in_perm(s):
for c in "aeiouAEIOU":
if c in s:
return True
return False
В два с лишним раза быстрее, чем find
для коротких строк и в 16 раз быстрее, чем регулярки для длинных строк.
Перемена местами циклов улучшает любое решение, делая их сравнимыми по скорости с find
:
def any_gen_perm(s):
return any(c in s for c in "aeiouAEIOU")
Комментарии (23)
Jijiki
23.06.2025 13:22но тут нюанс что это в строке(тоесть варианта 2 учесть все случаи regex, или писать самому поиск чото такое - я для себя так понял), перемена местами логически выглядит как пока есть строка сравнение с буквы с гласной, и далее конечная проверка на вхождение в строку типо что-то такое - (так понял, может ошибаюсь), тоесть это ближайшая опора к кмп
еще нюанс может быть при 1000000 символов, строк может быть 100 000Алгоритм_Кнута_—_Морриса_—_Пратта
дальше пойдёт(или не пойдёт, ну я задавался таким вопросом )) сравнение что лучше хэш, лист, дабл лист, или дерево с авто сортировкой) или ничего ну тоесть результаты делаются при проходе (что регекс, что выбранный), ну потомучто конверсия при прогонке поидее тоже ест производительность наверно(регекс кароче более требователен к тонкой настройке, своё разбитие может быть просто читаемее - понятнее)
тема интересная, спасибо, тоже столкнулся
п.с. поиск гласного частный случай поиска кмп вобщем-то ну или чего-то похожего по поиску слов, например какая-нибудь маска битовая слова или буквы(тоесть приводящая к симд наверно косвенно)
Jijiki
23.06.2025 13:22пойду от обратного, только что придумал, если имеем полу кмп поиск первого вхождения), то если иметь угадыватель - разбиватель строк по принципу по 1 вхождению, то полу кмп будет еффективнее)(хотя вроде спорно, но интересно), но тут на букву и слово будут разные угадыватели-разбиватели)
cupraer
23.06.2025 13:22Naïve, Moët, половина немецкого языка и три четверти французского и испанского смотрят на список гласных с недоумением.
MaxAkaAltmer
23.06.2025 13:22Наивно ожидал здесь увидеть решение для всего юникода )
cupraer
23.06.2025 13:22Там всё просто: надо распарсить последнюю спецификацию консорциума, выкусить оттуда гласные… Ах да, консорциум же не озаботился добавить ни в названия, ни в мету информацию гласная/согласная :)
Консорциум не в курсе, что такое гласная/согласная Так что нет, Хрюша сегодня не придёт.
MaxAkaAltmer
23.06.2025 13:22Если кому интересно - есть такая библиотека ICU, с помощью нее можно проверять гласность на уровне всего юникода.
cupraer
23.06.2025 13:22Таких библиотек полно́, проблема в том, что они все до единой созданы вручную.
MaxAkaAltmer
23.06.2025 13:22А как иначе-то? Сама должна появиться? )
cupraer
23.06.2025 13:22В спецификации консорциума есть даже такие мелочи, как правильный casing (чтобы в турецком
"istanbul".to_upper
превращался в"İstanbul"
).Грамотно спроектированные языки — парсят эту спецификацию. Но с гласными/согласными — это не проходит, приходится размечать руками.
S_gray
23.06.2025 13:22Деление звуков на гласные/согласные существует не во всех языках. В некоторых такое разделение просто бессмысленно...
MaxAkaAltmer
23.06.2025 13:22Деление звуков (подчеркиваю - звуков) на гласные и согласные так или иначе есть во всех языках согласно международному фонетическому алфавиту, а вот в плане письменности - да, бывает бессмысленно, но в таком случае символ просто не гласная и не согласная. Кстати гласным символ может быть или не быть в зависимости от диакритических знаков в некоторых языках. Так что все очень неоднозначно, всем надо переходить на фонетический алфавит - а то развели тут зоопарк понимаешь )
S_gray
23.06.2025 13:22Ну в статье идёт речь именно о письменности, а не о фонетике, поскольку мы имеем здесь дело не с языками, а с изображением текстов.
Не всякий письменный язык - фонетический. Насчёт перехода на фонетическую письменность: а вы когда-нибудь задавались вопросом, почему этого не происходит? Это же не просто так всё... Я, когда начинал изучать иврит, тоже думал - "ну, на кой это всё?" А сейчас вот повзрослел, особенно, когда про китайский кое-что понял... Кстати, реформа русского языка после революции очень здорово порубила его базовые смыслы, превратив живой, довольно, правда, сложный, но построенный на определенной логике и имеющий свою историю язык в упрощённый набор звуков...
MaxAkaAltmer
23.06.2025 13:22Это вы сказали про звуки, в статье понятно о чем речь идет.
Не происходит потому, что всем не угодишь, мне например не нравится фонетический алфавит на основе латиницы. Живой язык, на то и живой, чтобы эволюционировать. Иногда нужно избавляться и от архаики, это полезно, вот например зачем нам Ш и Щ? Ведь Щ - это мягкая Ш, там и пресловутые ЧА ЩА и другие правила нормализовать можно, да и нормальное место на клавиатуре для Ё (кстати гораздо более нужной в качестве отдельного символа) нашлось бы сразу. Но это конечно имхо, имеем все равно то что имеем.
Jijiki
23.06.2025 13:22это же зависит от ввода, загруженного языка, тоесть это загрузили язык, храним строку определенного веса, в статье рассматривается просто работа со строкой, тоесть придётся делать больше проверок на соответствие шрифта к набору на ПК(или не делать, но размер строки будет влиять на то какие строки и какое количество символов рисуется, например Китайский 1000 000 символов и 100 000 строк это будет не такой размер как с аски), на дефолтовом уровне проще поддерживать решение с аски, а далее уже смотреть с языками, потомучто решение с Юникодом в купе с отрисовкой будет сьедать ресурс
и придём в итоге к локализации и прописи локали в локализации, если писать свой движок на юникодах(из точек через ttf)
на аски проще если это консоль и/или фреймворк уже имеет в себе все эти проверки
например свинг поидее имеет эти проверки, соотв я просто работаю со строкой, а как она будет рисоваться пока не волнует пока нет такого вопроса
mynameco
23.06.2025 13:22можно собрать статистику по языку и словам. для каждого индекса в слове свой массив букв в порядки частоты вхожления. таким массов будет немного, по слову с макимальным число согласных в начале. так же, возможно, некоторые проверки, заменить больше меньше, чем по всему массиву прлходить.
так же можно собрать статистику слов и найти минимальное по длине слово без гласной. и проверять длинну слова, если оно больше, гласная есть.
чем дальше индекс тем меньше или нулевая вероятность встретить конкретную гласную, если до этого индекса были все согласные
Jijiki
23.06.2025 13:22там обычно чаще суть в том чтобы знать где буква-слово в исходном тексте, соотв нужны они все
Скрытый текст
class kvlist { private Integer k; private String v; public kvlist(int ka,String ve){ this.k=ka; this.v=ve; } public Integer getKa(){ return k; } public String getVe(){ return v; } } public class example { public static void main(String[] args) { String text = "你好 早上好 你好 晚上好 你好吗? 你好 谢谢 你好 不客气 你好 你好 请 你好 对不起 没关系 你好 我叫 你好 再见 你好 我"; String[] parts = text.split(" ");//\n String search = "你好"; List<kvlist> sortedByKeyMap = new ArrayList<>(); int searchStartIndex = 0; for (String word : parts) { String[] parts1 = word.split("(?U)(?<=\\s)(?=\\S)|(?<=\\S)(?=\\s)|(?<=\\w)(?=\\W)|(?<=\\W)");// for(String r:parts1){ int indexInText = text.indexOf(r, searchStartIndex); if (indexInText >= 0) { sortedByKeyMap.add(new kvlist(indexInText,new String(r))); searchStartIndex = indexInText + r.length(); } } } for (kvlist entry: sortedByKeyMap) { if(entry.getVe().equals(search)){ System.out.println(entry.getKa()+" "+entry.getVe()); } } } }
mynameco
23.06.2025 13:22еще можно попробовать поиграться с масками, там буквы гласные в таблице в ряд стоят...
navferty
23.06.2025 13:22Нет, в ASCII буквы идут по алфавиту. Но удачно получилось, что все гласные на нечётных позициях
Так что самый младший бит всегда 1. Дальше можно смотреть на пятый с конца бит, который для всех кроме U равен 0.
kasthack_phoenix
tl;dr: регулярки в сишном движке быстрее любого кода, написанного на интерпретируемом недоязыке.
Скорее всего, даже не биткарта, а какой-нибудь трюк с avx512, чтобы сканировать 64 байта за такт. SIMDJSON это для парсинга JSON использует.