С++ vs .NET regex performance

Подсказка из комментария Конрада Рудольфа о связанном вопросе, я написал следующую программу для сравнения производительности регулярных выражений в F #:

open System.Text.RegularExpressions
let str = System.IO.File.ReadAllText "C:\\Users\\Jon\\Documents\\pg10.txt"
let re = System.IO.File.ReadAllText "C:\\Users\\Jon\\Documents\\re.txt"
for _ in 1..3 do
  let timer = System.Diagnostics.Stopwatch.StartNew()
  let re = Regex(re, RegexOptions.Compiled)
  let res = Array.Parallel.init 4 (fun _ -> re.Split str |> Seq.sumBy (fun m -> m.Length))
  printfn "%A %fs" res timer.Elapsed.TotalSeconds

и эквивалент в С++:

#include "stdafx.h"

#include <windows.h>
#include <regex>
#include <vector>
#include <string>
#include <fstream>
#include <cstdio>
#include <codecvt>

using namespace std;

wstring load(wstring filename) {
    const locale empty_locale = locale::empty();
    typedef codecvt_utf8<wchar_t> converter_type;
    const converter_type* converter = new converter_type;
    const locale utf8_locale = locale(empty_locale, converter);
    wifstream in(filename);
    wstring contents;
    if (in)
    {
        in.seekg(0, ios::end);
        contents.resize(in.tellg());
        in.seekg(0, ios::beg);
        in.read(&contents[0], contents.size());
        in.close();
    }
    return(contents);
}

int count(const wstring &re, const wstring &s){
    static const wregex rsplit(re);
    auto rit = wsregex_token_iterator(s.begin(), s.end(), rsplit, -1);
    auto rend = wsregex_token_iterator();
    int count=0;
    for (auto it=rit; it!=rend; ++it)
        count += it->length();
    return count;
}

int _tmain(int argc, _TCHAR* argv[])
{
    wstring str = load(L"pg10.txt");
    wstring re = load(L"re.txt");

    __int64 freq, tStart, tStop;
    unsigned long TimeDiff;
    QueryPerformanceFrequency((LARGE_INTEGER *)&freq);
    QueryPerformanceCounter((LARGE_INTEGER *)&tStart);

    vector<int> res(4);

#pragma omp parallel num_threads(4)
    for(auto i=0; i<res.size(); ++i)
        res[i] = count(re, str);

    QueryPerformanceCounter((LARGE_INTEGER *)&tStop);
    TimeDiff = (unsigned long)(((tStop - tStart) * 1000000) / freq);
    printf("(%d, %d, %d, %d) %fs\n", res[0], res[1], res[2], res[3], TimeDiff/1e6);
    return 0;
}

Обе программы загружают два файла в виде строк в Юникоде (я использую копию Библии), создаю нетривиальное регулярное выражение юникода \w?\w?\w?\w?\w?\w и разделяю строку четыре раза параллельно, используя регулярное выражение, возвращающее сумму длин разделенных строк (во избежание выделения).

Запуск как в Visual Studio (с MP и OpenMP включен для С++) в версии для сборки релизов с 64-битным, С++ занимает 43,5 с, а F # занимает 3,28 с (более 13 раз быстрее). Это меня не удивляет, потому что я считаю, что .NET JIT компилирует регулярное выражение в собственный код, тогда как stdlib С++ интерпретирует его, но я бы хотел, чтобы какой-то экспертный обзор.

Есть ли ошибка в моем коде на С++ или это следствие скомпилированных vs интерпретируемых регулярных выражений?

EDIT: Billy ONeal указал, что .NET может иметь другую интерпретацию \w, поэтому я сделал это явным в новом регулярном выражении:

[0-9A-Za-z_]?[0-9A-Za-z_]?[0-9A-Za-z_]?[0-9A-Za-z_]?[0-9A-Za-z_]?[0-9A-Za-z_]

Это на самом деле делает код .NET существенно быстрее (С++ - то же самое), сокращая время от 3.28 до 2.38s для F # (более чем на 17 раз быстрее).

Ответ 1

Эти тесты на самом деле не сравнимы - С++ и .NET реализуют совершенно разные языки регулярных выражений (ECMAScript против Perl) и работают от совершенно разных движков регулярных выражений..NET(насколько мне известно) использует проект GRETA здесь, который создал абсолютно фантастический механизм регулярных выражений, который был настроен годами. С++ std::regex в сравнении - это недавнее дополнение (по крайней мере, на MSVС++, которое я предполагаю использовать с нестандартными типами __int64 и друзьями).

Вы можете видеть, как GRETA справилась с более зрелой реализацией std::regex, boost::regex, здесь: http://www.boost.org/doc/libs/1_54_0/libs/regex/doc/vc71-performance.html (хотя этот тест был выполнен на Visual Studio 2003).

Вы также должны иметь в виду, что производительность регулярных выражений сильно зависит от исходной строки и вашего регулярного выражения. Некоторые механизмы регулярных выражений проводят много времени, анализируя регулярное выражение, чтобы быстрее проходить через исходный текст; компромисс, который имеет смысл только в том случае, если вы разбираете много текста. Некоторые двигатели регулярных выражений торгуют скоростью сканирования за относительно дорогостоящие матчи (так что количество совпадений будет иметь эффект). Здесь огромное количество компромиссов; одна пара входов действительно затуманит историю.

Итак, чтобы более точно ответить на ваш вопрос: такой вариант является нормальным для двигателей регулярных выражений, будь они скомпилированы или интерпретированы. Глядя на повышающие тесты выше, часто разница между самыми быстрыми и медленными реализациями была в сотни раз различной - 17 раз не все так странно в зависимости от вашего варианта использования.