Перейти к основному содержимому

Ускорение регулярных выражений V8

· 4 мин. чтения
Якоб Грубер, инженер-программист

Эта публикация рассказывает о недавнем переносе функций встроенных регулярных выражений V8 из реализации на собственном JavaScript в архитектуру нового поколения кода, основанного на TurboFan.

Реализация регулярных выражений в V8 построена на основе Irregexp, который считается одним из самых быстрых движков регулярных выражений. Хотя сам движок выполняет основную логику для сопоставления шаблонов со строками, функции на прототипе RegExp, такие как RegExp.prototype.exec, выполняют дополнительную работу, необходимую для предоставления функциональности пользователю.

Исторически сложилось так, что различные компоненты V8 были реализованы на JavaScript. До недавнего времени regexp.js был одним из них, предоставляя реализацию конструктора RegExp, его свойств, а также свойств его прототипа.

К сожалению, этот подход имеет недостатки, включая непредсказуемую производительность и затраты ресурсов на переходы к C++-runtime для низкоуровневой функциональности. Недавнее введение встроенного подклассифицирования в ES6 (которое позволяет разработчикам JavaScript предоставлять свои собственные пользовательские реализации регулярных выражений) привело к дальнейшему снижению производительности RegExp, даже если встроенный RegExp не подклассифицирован. Эти регрессии не могли быть полностью устранены в реализации на собственном JavaScript.

Поэтому мы решили перенести реализацию RegExp с JavaScript. Однако сохранение производительности оказалось сложнее, чем ожидалось. Первоначальный перенос в полностью C++-реализацию был значительно медленнее, достигая лишь около 70% производительности исходной реализации. После некоторых исследований мы выявили несколько причин:

  • RegExp.prototype.exec содержит несколько критически важных для производительности областей, в том числе переход к базовому движку регулярных выражений и создание результата RegExp с его вызовами substring. Для этих задач JavaScript-реализация опиралась на высокооптимизированные части кода, называемые “stubs”, написанные либо на родном ассемблере, либо с прямым подключением к оптимизирующему компилятору. В C++ доступ к этим stub невозможен, а их runtime-эквиваленты значительно медленнее.
  • Доступ к таким свойствам, как lastIndex у RegExp, может быть дорогостоящим, возможно требующим поиска по имени и прохода по цепочке прототипов. Оптимизирующий компилятор V8 часто может автоматически заменять такие обращения более эффективными операциями, тогда как в C++ эти случаи необходимо обрабатывать явно.
  • В C++ ссылки на объекты JavaScript должны обертываться в так называемые Handle для взаимодействия с сборщиком мусора. Управление Handle создает дополнительные расходы по сравнению с чистой JavaScript-реализацией.

Наш новый подход к миграции RegExp основан на CodeStubAssembler, механизме, который позволяет разработчикам V8 писать платформонезависимый код, который затем будет переводиться в быстрый платформоспецифичный код тем же бэкендом, который используется также для нового оптимизирующего компилятора TurboFan. Использование CodeStubAssembler позволяет нам устранить все недостатки первоначальной C++-реализации. Stubs (например, точка входа в движок RegExp) могут быть легко вызваны из CodeStubAssembler. Хотя быстрый доступ к свойствам все еще нужно явно реализовывать на так называемых быстрых путях, такие обращения крайне эффективны в CodeStubAssembler. Handle просто не существуют за пределами C++. А поскольку реализация теперь работает на очень низком уровне, мы можем использовать дальнейшие оптимизации, такие как пропуск дорогостоящего построения результатов, когда это не требуется.

Результаты были очень положительными. Наш показатель на значительной рабочей нагрузке RegExp улучшился на 15%, что более чем компенсировало наши недавние потери производительности, связанные с наследованием. Микротесты (Рисунок 1) показывают улучшения по всем направлениям: от 7% для RegExp.prototype.exec до 102% для RegExp.prototype[@@split].

Рисунок 1: Ускорение RegExp по функциям

Как вы, как разработчик JavaScript, можете убедиться, что ваши RegExp работают быстро? Если вы не заинтересованы в углублении в внутреннюю реализацию RegExp, убедитесь, что ни экземпляр RegExp, ни его прототип не модифицируются, чтобы достичь наилучшей производительности:

const re = /./g;
re.exec(''); // Быстрый путь.
re.new_property = 'медленно';
RegExp.prototype.new_property = 'тоже медленно';
re.exec(''); // Медленный путь.

И хотя наследование RegExp может быть довольно полезным иногда, имейте в виду, что экземпляры унаследованных RegExp требуют более общего подхода к обработке, что приводит к медленному пути:

class SlowRegExp extends RegExp {}
new SlowRegExp(".", "g").exec(''); // Медленный путь.

Полная миграция RegExp будет доступна в V8 версии 5.7.