Как работает RegexOptions.Compiled?

Что происходит за кулисами, когда вы отмечаете регулярное выражение как скомпилированное? Как это сравнивается/отличается от кэшированного регулярного выражения?

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

Ответ 1

RegexOptions.Compiled инструктирует механизм регулярных выражений скомпилировать выражение регулярного выражения в IL с использованием генерации легкого кода (LCG). Эта компиляция происходит во время построения объекта, а сильно замедляет работу. В свою очередь, совпадения с использованием регулярного выражения выполняются быстрее.

Если вы не укажете этот флаг, ваше регулярное выражение считается "интерпретированным".

Возьмем этот пример:

public static void TimeAction(string description, int times, Action func)
{
    // warmup
    func();

    var watch = new Stopwatch();
    watch.Start();
    for (int i = 0; i < times; i++)
    {
        func();
    }
    watch.Stop();
    Console.Write(description);
    Console.WriteLine(" Time Elapsed {0} ms", watch.ElapsedMilliseconds);
}

static void Main(string[] args)
{
    var simple = "^\\d+$";
    var medium = @"^((to|from)\W)?(?<url>http://[\w\.:]+)/questions/(?<questionId>\d+)(/(\w|-)*)?(/(?<answerId>\d+))?";
    var complex = @"^(([^<>()[\]\\.,;:\[email protected]""]+"
      + @"(\.[^<>()[\]\\.,;:\[email protected]""]+)*)|("".+""))@"
      + @"((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}"
      + @"\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+"
      + @"[a-zA-Z]{2,}))$";


    string[] numbers = new string[] {"1","two", "8378373", "38737", "3873783z"};
    string[] emails = new string[] { "[email protected]", "[email protected]", "[email protected]", "[email protected]" };

    foreach (var item in new[] {
        new {Pattern = simple, Matches = numbers, Name = "Simple number match"},
        new {Pattern = medium, Matches = emails, Name = "Simple email match"},
        new {Pattern = complex, Matches = emails, Name = "Complex email match"}
    })
    {
        int i = 0;
        Regex regex;

        TimeAction(item.Name + " interpreted uncached single match (x1000)", 1000, () =>
        {
            regex = new Regex(item.Pattern);
            regex.Match(item.Matches[i++ % item.Matches.Length]);
        });

        i = 0;
        TimeAction(item.Name + " compiled uncached single match (x1000)", 1000, () =>
        {
            regex = new Regex(item.Pattern, RegexOptions.Compiled);
            regex.Match(item.Matches[i++ % item.Matches.Length]);
        });

        regex = new Regex(item.Pattern);
        i = 0;
        TimeAction(item.Name + " prepared interpreted match (x1000000)", 1000000, () =>
        {
            regex.Match(item.Matches[i++ % item.Matches.Length]);
        });

        regex = new Regex(item.Pattern, RegexOptions.Compiled);
        i = 0;
        TimeAction(item.Name + " prepared compiled match (x1000000)", 1000000, () =>
        {
            regex.Match(item.Matches[i++ % item.Matches.Length]);
        });

    }
}

Он выполняет 4 теста на 3 разных регулярных выражениях. Сначала он тестирует одиночное однократное совпадение (скомпилировано vs non compiled). Во-вторых, он проверяет повторяющиеся совпадения, которые повторно используют одно и то же регулярное выражение.

Результаты на моей машине (скомпилированные в версии, без отладчика)

1000 одиночных совпадений (построить Regex, Match и dispose)

Type        | Platform | Trivial Number | Simple Email Check | Ext Email Check
------------------------------------------------------------------------------
Interpreted | x86      |    4 ms        |    26 ms           |    31 ms
Interpreted | x64      |    5 ms        |    29 ms           |    35 ms
Compiled    | x86      |  913 ms        |  3775 ms           |  4487 ms
Compiled    | x64      | 3300 ms        | 21985 ms           | 22793 ms

1 000 000 совпадений - повторное использование объекта Regex

Type        | Platform | Trivial Number | Simple Email Check | Ext Email Check
------------------------------------------------------------------------------
Interpreted | x86      |  422 ms        |   461 ms           |  2122 ms
Interpreted | x64      |  436 ms        |   463 ms           |  2167 ms
Compiled    | x86      |  279 ms        |   166 ms           |  1268 ms
Compiled    | x64      |  281 ms        |   176 ms           |  1180 ms

Эти результаты показывают, что скомпилированные регулярные выражения могут быть до 60% быстрее для случаев, когда вы повторно используете объект Regex. Однако в некоторых случаях может быть более на 3 порядка медленнее.

Он также показывает, что x64 версия для .NET может быть в 5-6 раз медленнее, когда дело доходит до компиляции регулярных выражений.


Рекомендация заключалась бы в использовании скомпилированной версии в тех случаях, когда

  • Вы не заботитесь об стоимости инициализации объекта и нуждаетесь в дополнительном повышении производительности. (обратите внимание, что мы говорим о долях миллисекунды здесь).
  • Вы немного заботитесь об стоимости инициализации, но повторно используете объект Regex столько раз, что он будет компенсировать его во время жизненного цикла приложения.

Гаечный ключ в работе, кеш регулярных выражений

Механизм регулярных выражений содержит кеш LRU, который содержит последние 15 регулярных выражений, которые были протестированы с использованием статических методов класса Regex.

Например: Regex.Replace, Regex.Match и т.д. все используют кэш Regex.

Размер кеша можно увеличить, установив Regex.CacheSize. Он принимает изменения в размере в любое время в течение жизненного цикла приложения.

Новые регулярные выражения кэшируются статическими помощниками в классе Regex. Если вы построите свои объекты, кеш будет проверен (для повторного использования и наброска), однако созданное вами регулярное выражение не добавлено в кэш.

Этот кеш является тривиальным LRU кешем, он реализован с использованием простого двойного списка. Если вам удастся увеличить его до 5000 и использовать 5000 разных вызовов для статических помощников, каждая конструкция регулярных выражений будет обходить 5000 записей, чтобы увидеть, было ли они ранее кэшированы. Вокруг проверки есть блокировка, поэтому проверка может уменьшить parallelism и ввести блокировку потока.

Число установлено достаточно низким, чтобы защитить себя от таких случаев, хотя в некоторых случаях у вас может не быть выбора, кроме как увеличить его.

передать RegexOptions.Compiled статическому помощнику.

Например:

\\ WARNING: bad code
Regex.IsMatch(@"\\d+", "10000", RegexOptions.Compiled)

Причина заключается в том, что вы сильно рискуете пропустить кеш LRU, который вызовет компиляцию супердорогая. Кроме того, вы не представляете, чем занимаются библиотеки, потому что у вас мало возможностей контролировать или прогнозировать размер наилучшего кеша.

Смотрите также: Блог команды BCL

Примечание: это относится к .NET 2.0 и .NET 4.0. Есть некоторые ожидаемые изменения в 4.5, которые могут привести к его пересмотру.

Ответ 2

Эта запись в блоге команды BCL дает хороший обзор: "" Эффект регулярного выражения".

Короче говоря, существует три типа регулярных выражений (каждый из которых выполняется быстрее предыдущего):

  • интерпретируется

    быстро создавать "на лету", медленно выполнять

  • скомпилированный (тот, о котором вы, кажется, спрашиваете)

    медленнее создавать "на лету", быстро выполнять (полезно для выполнения в циклах)

  • прекомпилируются

    создавать во время компиляции вашего приложения (без создания времени исполнения), быстро выполнить

Итак, если вы собираетесь выполнять регулярное выражение только один раз или в критическом для вас разделе вашего приложения (например, подтверждение ввода пользователя), вы в порядке с опцией 1.

Если вы собираетесь запускать регулярное выражение в цикле (т.е. поэтапный разбор файла), вы должны пойти с опцией 2.

Если у вас много регулярных выражений, которые никогда не будут изменяться для вашего приложения и будут интенсивно использоваться, вы можете пойти с опцией 3.

Ответ 3

Следует отметить, что производительность регулярных выражений с .NET 2.0 была улучшена с помощью кэша MRU для несжатых регулярных выражений. Код библиотеки Regex больше не переинтерпретирует одно и то же не скомпилированное регулярное выражение каждый раз.

Таким образом, существует потенциально большее ограничение производительности при компиляции и регулярном выражении на лету. В дополнение к более медленному времени загрузки система также использует больше памяти для компиляции регулярного выражения для кодов операций.

По существу, текущий совет либо не компилирует регулярное выражение, либо компилирует их заранее в отдельную сборку.

Ссылка: Блог группы BCL Эффект регулярного выражения [David Gutierrez]