Почему скомпилированная производительность RegEx медленнее, чем Intrepreted RegEx?
Я столкнулся с этой статьей:
Производительность: скомпилированные или интерпретированные регулярные выражения, я модифицировал образец кода для компиляции 1000 Regex, а затем запускаю каждые 500 раз, чтобы воспользоваться преимуществами прекомпиляции, однако даже в этом случае интерпретируемые RegExes выполняются в 4 раза быстрее!
Это означает, что параметр RegexOptions.Compiled
полностью бесполезен, а тем более хуже - медленнее! Большая разница была связана с JIT, после решения JIT скомпилированного регулярного выражения в следующем коде все еще выполняется немного медленнее и не имеет смысла для меня, но @Jim в ответах предоставил гораздо более чистую версию, которая работает как ожидается.
Может кто-нибудь объяснить, почему это так?
Код, принятый и измененный из сообщения в блоге:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
namespace RegExTester
{
class Program
{
static void Main(string[] args)
{
DateTime startTime = DateTime.Now;
for (int i = 0; i < 1000; i++)
{
CheckForMatches("some random text with email address, [email protected]" + i.ToString());
}
double msTaken = DateTime.Now.Subtract(startTime).TotalMilliseconds;
Console.WriteLine("Full Run: " + msTaken);
startTime = DateTime.Now;
for (int i = 0; i < 1000; i++)
{
CheckForMatches("some random text with email address, [email protected]" + i.ToString());
}
msTaken = DateTime.Now.Subtract(startTime).TotalMilliseconds;
Console.WriteLine("Full Run: " + msTaken);
Console.ReadLine();
}
private static List<Regex> _expressions;
private static object _SyncRoot = new object();
private static List<Regex> GetExpressions()
{
if (_expressions != null)
return _expressions;
lock (_SyncRoot)
{
if (_expressions == null)
{
DateTime startTime = DateTime.Now;
List<Regex> tempExpressions = new List<Regex>();
string regExPattern =
@"^[a-zA-Z0-9]+[a-zA-Z0-9._%-]*@{0}$";
for (int i = 0; i < 2000; i++)
{
tempExpressions.Add(new Regex(
string.Format(regExPattern,
Regex.Escape("domain" + i.ToString() + "." +
(i % 3 == 0 ? ".com" : ".net"))),
RegexOptions.IgnoreCase));// | RegexOptions.Compiled
}
_expressions = new List<Regex>(tempExpressions);
DateTime endTime = DateTime.Now;
double msTaken = endTime.Subtract(startTime).TotalMilliseconds;
Console.WriteLine("Init:" + msTaken);
}
}
return _expressions;
}
static List<Regex> expressions = GetExpressions();
private static void CheckForMatches(string text)
{
DateTime startTime = DateTime.Now;
foreach (Regex e in expressions)
{
bool isMatch = e.IsMatch(text);
}
DateTime endTime = DateTime.Now;
//double msTaken = endTime.Subtract(startTime).TotalMilliseconds;
//Console.WriteLine("Run: " + msTaken);
}
}
}
Ответы
Ответ 1
Скомпилированные регулярные выражения быстрее совпадают при использовании по назначению. Как отмечали другие, идея состоит в том, чтобы собрать их один раз и использовать их много раз. Время сборки и инициализации amortized за эти многие прогоны.
Я создал гораздо более простой тест, который покажет вам, что компилируется регулярные выражения, безусловно, быстрее, чем не компилируется.
const int NumIterations = 1000;
const string TestString = "some random text with email address, [email protected]";
const string Pattern = "^[a-zA-Z0-9]+[a-zA-Z0-9._%-]*@domain0\\.\\.com$";
private static Regex NormalRegex = new Regex(Pattern, RegexOptions.IgnoreCase);
private static Regex CompiledRegex = new Regex(Pattern, RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static Regex DummyRegex = new Regex("^.$");
static void Main(string[] args)
{
var DoTest = new Action<string, Regex, int>((s, r, count) =>
{
Console.Write("Testing {0} ... ", s);
Stopwatch sw = Stopwatch.StartNew();
for (int i = 0; i < count; ++i)
{
bool isMatch = r.IsMatch(TestString + i.ToString());
}
sw.Stop();
Console.WriteLine("{0:N0} ms", sw.ElapsedMilliseconds);
});
// Make sure that DoTest is JITed
DoTest("Dummy", DummyRegex, 1);
DoTest("Normal first time", NormalRegex, 1);
DoTest("Normal Regex", NormalRegex, NumIterations);
DoTest("Compiled first time", CompiledRegex, 1);
DoTest("Compiled", CompiledRegex, NumIterations);
Console.WriteLine();
Console.Write("Done. Press Enter:");
Console.ReadLine();
}
Настройка NumIterations
до 500 дает мне следующее:
Testing Dummy ... 0 ms
Testing Normal first time ... 0 ms
Testing Normal Regex ... 1 ms
Testing Compiled first time ... 13 ms
Testing Compiled ... 1 ms
С 5 миллионами итераций я получаю:
Testing Dummy ... 0 ms
Testing Normal first time ... 0 ms
Testing Normal Regex ... 17,232 ms
Testing Compiled first time ... 17 ms
Testing Compiled ... 15,299 ms
Здесь вы видите, что скомпилированное регулярное выражение не менее чем на 10% быстрее, чем не скомпилированная версия.
Интересно отметить, что если вы удалите RegexOptions.IgnoreCase
из своего регулярного выражения, результаты из 5 миллионов итераций еще более поразительны:
Testing Dummy ... 0 ms
Testing Normal first time ... 0 ms
Testing Normal Regex ... 12,869 ms
Testing Compiled first time ... 14 ms
Testing Compiled ... 8,332 ms
Здесь скомпилированное регулярное выражение на 35% быстрее, чем не скомпилированное регулярное выражение.
На мой взгляд, сообщение в блоге, на котором вы ссылаетесь, является просто ошибочным тестом.
Ответ 2
http://www.codinghorror.com/blog/2005/03/to-compile-or-not-to-compile.html
Скомпилировано помогает только в том случае, если вы создаете экземпляр его один раз и повторно используете его несколько раз. Если вы создаете скомпилированное регулярное выражение в цикле for, то он, очевидно, будет работать хуже. Можете ли вы показать нам свой пример кода?
Ответ 3
Проблема с этим эталоном заключается в том, что скомпилированные Regexes имеют накладные расходы на создание совершенно новой сборки и загрузку ее в AppDomain.
Сценарий, в котором скомпилирован Regex был разработан (я считаю, что я его не проектировал), имеет десятки Regexes, выполненных миллионы раз, а не тысячи Regexes, выполняемых тысячи раз. Если вы не собираетесь выполнять Regex в области миллион раз, вы, вероятно, даже не воспользуетесь временем, чтобы JIT скомпилировать его.
Ответ 4
Это почти наверняка указывает на то, что ваш тестовый код написан неправильно или скомпилированное регулярное выражение медленнее, чем интерпретированное. Существует много работы, которая заключалась в создании скомпилированного регулярного выражения.
Теперь, когда у нас есть код, вы можете посмотреть несколько конкретных вещей, требующих обновления
- Этот код не учитывает затраты JIT метода. Он должен запустить код один раз, чтобы получить затраты на JIT, а затем запустить его снова и измерить
- Почему
lock
используется вообще? Это совершенно необязательно
- В тестах следует использовать
StopWatch
not DateTime
- Чтобы получить хорошее сравнение между скомпилированными и не скомпилированными, вы должны проверить производительность одного скомпилированного
Regex
и одного некомпилированного Regex
, совпадающего с N раз. Не N каждого совпадения не более одного раза в регулярном выражении.