Движок V8 JavaScript использует Irregexp, один из самых быстрых движков RegExp. Однако, чтобы максимально использовать эту мощь, разработчикам JS все еще необходимо избегать определенных ловушек, которые, к сожалению, не являются ни очевидными, ни хорошо документированными и могут тратить драгоценное время выполнения на встроенные функции RegExp, прежде чем когда-либо достигнут сгенерированный код сопоставления с образцом Irregexp.
Например, недавно я столкнулся с проблемой на трекере UglifyJS2, в которой разработчик обнаруживал ошибку корректности в старой версии Chrome. Сам исходный код казался достаточно невинным:
$ browserify test.js > bundle.js
$ uglifyjs bundle.js -c reduce_vars=true | node
[ 'fooBar' ] // Wrong!
$ uglifyjs bundle.js -c reduce_vars=false | node
[ 'foo', 'Bar' ] // This is what we expect.
Расследование привело к простой ошибке в медленном пути RegExp.prototype[@@split]
, которая была исправлена несколько месяцев назад - проблема решена.
Либо это? Почему мы вообще идем медленным путем в этом тривиальном примере?
Возможно, вы не знаете об этом, но медленный путь может иметь огромное влияние на производительность. В частности, в случае @@split
медленный путь может быть до 40 раз медленнее, чем быстрый.
Встроенные функции RegExp предъявляют несколько требований, которые необходимо выполнить для быстрого перехода. Рассмотрим regexp[@@split](string)
(вызываемый внутри string.split(regexp)
). Чтобы пойти по быстрому пути:
regexp
не должно быть изменено (не должно быть добавленных, удаленных или измененных свойств)regexp.prototype
не должно быть измененоregexp.lastIndex
должен быть простым неотрицательным целым числом - не объектом, не получателем.
В нашем примере выше RegExp.prototype
фактически изменяется, когда uglifyjs
запускается с reduce_vars=true
. Сама модификация выполняется с помощью es6.regexp.constructor
polyfill Babel, что является плохой новостью, поскольку это может означать, что могут быть затронуты более крупные части Интернета.
Так почему вообще существует различие между медленным и быстрым путями?
Ответ - производительность. Чтобы быть эффективным, быстрый путь должен иметь возможность делать определенные предположения о форме объекта RegExp. Например, ему необходимо знать, что к lastIndex
можно получить доступ по определенному смещению в области памяти объекта, и что доступ к lastIndex
не имеет побочных эффектов (т. Е. Нет установленных геттеров, нет преобразований объекта в целое число) .
Используя эти предположения, быстрый путь может генерировать очень компактный код, превращая lastIndex
доступ в простые загрузки / сохранения при определенных смещениях. Он также может пропускать ToLength(lastIndex)
операций, поскольку lastIndex
уже известно как неотрицательное целое число.
С другой стороны, медленный путь должен иметь возможность обрабатывать более общие формы:
Обратите внимание, что преобразование ToLength
выше вызывает указанную пользователем функцию, которая может выполнять произвольные действия, включая изменение экземпляра RegExp во время работы встроенных функций (например, RegExp.prototype[@@split]
).
Стоит отметить, что нынешняя ситуация не высечена на камне, и вполне вероятно, что быстрый путь станет более снисходительным в будущем. Но на данный момент следует избегать изменений экземпляров RegExp и их прототипов, если это вообще возможно.