Skip to main content
 首页 » 编程设计

c#之慢正则表达式拆分

2024年01月26日29kevingrace

我正在解析大量数据(超过 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(); 

程序的运行方式如下:

  • 将所有数据加载到一个字符串中。
  • 上面的代码行用于将字符串拆分为日志条目,并将每个条目作为条目存储在列表中。 (这是我想优化的慢部分)
  • 日志条目由上面显示的正则表达式模式表示。

请您参考如下方法:

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

测量

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

为了进行基准测试,我将使用 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() 方法,该方法返回非具体化可枚举 - 您将在遍历文件时读取文件。

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

|                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 小得多。

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

拆分日志条目

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

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