首页 > 解决方案 > 慢正则表达式拆分

问题描述

我正在解析大量数据(超过 2GB),而我的正则表达式搜索速度很慢。有没有办法改进它?

慢代码

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();

该程序的工作方式如下:

标签: c#regexstring

解决方案


在下面的答案中,我提出了一些您可以使用的优化。tl;博士; 通过迭代行并使用自定义解析方法(不是正则表达式)将日志解析速度提高 6 倍

测量

在我们尝试进行优化之前,我建议定义我们将如何衡量它们的影响和价值。

对于基准测试,我将使用Benchmark.NET框架。创建控制台应用程序:

 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 = 512*1024*1024; // 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会很不爽,我们逐行读取。

实现这一点的最简单方法是使用File.ReadLines()返回非物化可枚举的方法 - 您将在迭代文件时读取文件。

您还可以按照此处的说明异步读取文件。这是一种相当无用的方法,因为我仍然将所有内容合并到一行,所以我在这里有点推测什么时候会对结果发表评论:)

|                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,它是最快的。它分配的内存量与 Gen 0 相同,ReadFileToMemory但都在 Gen 0 中,因此 GC 可以更快地收集它,最大内存消耗远小于ReadFileToMemory.

异步读取不会带来任何性能提升。如果您需要吞吐量,请不要使用它。

拆分日志条目

正则表达式很慢而且很耗内存。传递一个巨大的字符串会使您的应用程序运行缓慢。您可以缓解此问题并检查文件的每一行是否与您的正则表达式匹配。您需要重建整个日志条目,尽管它可能是多行的。

您还可以引入更有效的方法来匹配您的字符串,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();
        }
    }

推荐阅读