Медленное регулярное выражение - PullRequest
1 голос
/ 24 сентября 2019

Я анализирую большое количество данных (более 2 ГБ), и мой поиск по регулярному выражению довольно медленный.Есть ли способ его улучшить?

Slow Code

string file_content = "4980: 01:06:59.140 - SomeLargeQuantityOfLogEntries";
List<string> split_content = Regex.Split(file_content, @"\s+(?=\d+: \d{2}:\d{2}:\d{2}\.\d{3} - )").ToList();

Программа работает следующим образом:

  • Загружает вседанные в строку.
  • Приведенная выше строка кода используется для разбиения строки на записи журнала и сохранения каждой записи в виде записи в списке.(Это медленная часть, которую я хотел бы оптимизировать)
  • Записи в журнале обозначаются шаблоном регулярных выражений, показанным выше.

Ответы [ 2 ]

3 голосов
/ 24 сентября 2019

В ответ ниже я положил пару оптимизаций, которые вы можете использовать.ТЛ; др;Ускорьте синтаксический анализ журнала в 6 раз за счет итерации строк и использования пользовательского метода синтаксического анализа (не Regex)

Измерения

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

Для тестирования производительности я буду использовать Benchmark.NET framework.Создайте консольное приложение:

 static void Main(string[] args)
        {
            BenchmarkRunner.Run<LogReaderBenchmarks>();
            BenchmarkRunner.Run<LogParserBenchmarks>();
            BenchmarkRunner.Run<LogBenchmarks>();
            Console.ReadLine();
            return;
        }

Выполните указанную ниже команду в PackageManagerConsole, чтобы добавить пакет nuget:

Install-Package BenchmarkDotNet -Version 0.11.5

Генератор тестовых данных выглядит следующим образом, запустите его один раз, а затем просто используйте еговременный файл во всех ваших тестах:

public static class LogFilesGenerator {

        public static void GenerateLogFile(string location)
        {
            var sizeBytes = 5121024; // 512MB
            var line = new StringBuilder();
            using (var f = new StreamWriter(location))
            {
                for (long z = 0; z < sizeBytes; z += line.Length)
                {
                    line.Clear();
                    line.Append($"{z}: {DateTime.UtcNow.TimeOfDay.ToString(@"hh\:mm\:ss\.fff")} - ");
                    for (var l = -1; l < z % 3; l++)
                        line.AppendLine(Guid.NewGuid().ToString());
                    f.WriteLine(line);
                }
                f.Close();
            }
        }
    }

Чтение файла

И комментаторы указали - очень неэффективно читать весь файл в память, GC будет очень недоволен, давайте прочтем его строку-by-line.

Самый простой способ добиться этого - просто использовать метод File.ReadLines(), который возвращает вам нематериализованное перечислимое - вы будете читать файл, пока будете его перебирать. *1024*

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

|                Method | buffer |    Mean |       Gen 0 |      Gen 1 |     Gen 2 | Allocated |
|---------------------- |------- |--------:|------------:|-----------:|----------:|----------:|
|      ReadFileToMemory |      ? | 1.919 s | 181000.0000 | 93000.0000 | 6000.0000 |   2.05 GB |
|   ReadFileEnumerating |      ? | 1.881 s | 314000.0000 |          - |         - |   1.38 GB |
| ReadFileToMemoryAsync |   4096 | 9.254 s | 248000.0000 | 68000.0000 | 6000.0000 |   1.92 GB |
| ReadFileToMemoryAsync |  16384 | 5.632 s | 215000.0000 | 61000.0000 | 6000.0000 |   1.72 GB |
| ReadFileToMemoryAsync |  65536 | 3.499 s | 196000.0000 | 54000.0000 | 4000.0000 |   1.62 GB |
    [RyuJitX64Job]
    [MemoryDiagnoser]
    [IterationCount(1), InnerIterationCount(1), WarmupCount(0), InvocationCount(1), ProcessCount(1)]
    [StopOnFirstError]
    public class LogReaderBenchmarks
    {
        string file = @"C:\Users\Admin\AppData\Local\Temp\tmp6483.tmp";

        [GlobalSetup()]
        public void Setup()
        {
            //file = Path.GetTempFileName(); <---- uncomment these lines to generate file first time.
            //Console.WriteLine(file);
            //LogFilesGenerator.GenerateLogFile(file);
        }

        [Benchmark(Baseline = true)]
        public string ReadFileToMemory() => File.ReadAllText(file);

        [Benchmark]
        [Arguments(1024*4)]
        [Arguments(1024 * 16)]
        [Arguments(1024 * 64)]
        public async Task<string> ReadFileToMemoryAsync(int buffer) => await ReadTextAsync(file, buffer);

        [Benchmark]
        public int ReadFileEnumerating() => File.ReadLines(file).Select(l => l.Length).Max();

        private async Task<string> ReadTextAsync(string filePath, int bufferSize)
        {
            using (FileStream sourceStream = new FileStream(filePath,
                FileMode.Open, FileAccess.Read, FileShare.Read,
                bufferSize: bufferSize, useAsync: true))
            {
                StringBuilder sb = new StringBuilder();
                byte[] buffer = new byte[bufferSize];
                int numRead;
                while ((numRead = await sourceStream.ReadAsync(buffer, 0, buffer.Length)) != 0)
                {
                    string text = Encoding.Unicode.GetString(buffer, 0, numRead);
                    sb.Append(text);
                }
                return sb.ToString();
            }
        }
    }

Как вы можете видеть ReadFileEnumeratingсамый быстрыйОн выделяет тот же объем памяти, что и ReadFileToMemory, но все это в Gen 0, поэтому GC может собирать его быстрее, максимальное потребление памяти намного меньше, чем ReadFileToMemory.

Асинхронное чтение не дает никакой производительностиусиление.Если вам нужна пропускная способность, не используйте ее.

Разделите записи журнала

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

Также вы можете ввести более эффективный метод, соответствующий вашей строке, например, отметьте customParseMatch.Я не претендую на то, чтобы он был наиболее эффективным, вы можете написать отдельный тест для предиката, но он уже показывает хороший результат по сравнению с Regex - он в 10 раз быстрее.

|                      Method |     Mean | Ratio |       Gen 0 |       Gen 1 |     Gen 2 | Allocated |
|---------------------------- |---------:|------:|------------:|------------:|----------:|----------:|
|                SplitByRegex | 24.191 s |  1.00 | 426000.0000 | 119000.0000 | 4000.0000 |   2.65 GB |
|       SplitByRegexIterating | 16.302 s |  0.67 | 176000.0000 |  88000.0000 | 1000.0000 |   2.05 GB |
| SplitByCustomParseIterating |  2.385 s |  0.10 | 398000.0000 |           - |         - |   1.75 GB |
    [RyuJitX64Job]
    [MemoryDiagnoser]
    [IterationCount(1), InnerIterationCount(1), WarmupCount(0), InvocationCount(1), ProcessCount(1)]
    [StopOnFirstError]
    public class LogParserBenchmarks
    {
        string file = @"C:\Users\Admin\AppData\Local\Temp\tmp6483.tmp";
        string[] lines;
        string text;
        Regex split_regex = new Regex(@"\s+(?=\d+: \d{2}:\d{2}:\d{2}\.\d{3} - )");

        [GlobalSetup()]
        public void Setup()
        {           
            lines = File.ReadAllLines(file);
            text = File.ReadAllText(file);
        }

        [Benchmark(Baseline = true)]
        public string[] SplitByRegex() => split_regex.Split(text);

        [Benchmark]
        public int SplitByRegexIterating() =>
            parseLogEntries(lines, split_regex.IsMatch).Count();

        [Benchmark]
        public int SplitByCustomParseIterating() =>
            parseLogEntries(lines, customParseMatch).Count();

        public static bool customParseMatch(string line)
        {
            var refinedLine = line.TrimStart();
            var colonIndex = refinedLine.IndexOf(':');
            if (colonIndex < 0) return false;
            if (!int.TryParse(refinedLine.Substring(0,colonIndex), out var _)) return false;
            if (refinedLine[colonIndex + 1] != ' ') return false;
            if (!TimeSpan.TryParseExact(refinedLine.Substring(colonIndex + 2,12), @"hh\:mm\:ss\.fff", CultureInfo.InvariantCulture, out var _)) return false;
            return true;
        }

        IEnumerable<string> parseLogEntries(IEnumerable<string> lines, Predicate<string> entryMatched)
        {
            StringBuilder builder = new StringBuilder();
            foreach (var line in lines)
            {
                if (entryMatched(line) && builder.Length > 0)
                {
                    yield return builder.ToString();
                    builder.Clear();
                }
                builder.AppendLine(line);
            }
            if (builder.Length > 0)
                yield return builder.ToString();
        }
    }

Параллелизм

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

Сводка

Поэтому итерации по каждой строке и использование пользовательской функции синтаксического анализа дают нам наилучшие результаты.Давайте сделаем тест и посмотрим, сколько мы получили:

|                      Method |     Mean |       Gen 0 |       Gen 1 |     Gen 2 | Allocated |
|---------------------------- |---------:|------------:|------------:|----------:|----------:|
|     ReadTextAndSplitByRegex | 29.070 s | 601000.0000 | 198000.0000 | 2000.0000 |    4.7 GB |
| ReadLinesAndSplitByFunction |  4.117 s | 713000.0000 |           - |         - |   3.13 GB |
[RyuJitX64Job]
    [MemoryDiagnoser]
    [IterationCount(1), InnerIterationCount(1), WarmupCount(0), InvocationCount(1), ProcessCount(1)]
    [StopOnFirstError]
    public class LogBenchmarks
    {
        [Benchmark(Baseline = true)]
        public string[] ReadTextAndSplitByRegex()
        {
            var text = File.ReadAllText(LogParserBenchmarks.file);
            return LogParserBenchmarks.split_regex.Split(text);
        }

        [Benchmark]
        public int ReadLinesAndSplitByFunction()
        {
            var lines = File.ReadLines(LogParserBenchmarks.file);
            var entries = LogParserBenchmarks.parseLogEntries(lines, LogParserBenchmarks.customParseMatch);
            return entries.Count();
        }
    }
2 голосов
/ 25 сентября 2019

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

enter image description here

Требуется немного работы, чтобы соответствоватьрегулярное выражение.Эта ссылка Как внутренне работает движок Regex объясняет процесс дальше.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...