読み込んでいます...

很早之前用C#写一个log processer出了一点问题,这个程序很简单,就是把一些日志文件给导入到数据库里面去,然后在对比日志中的字符串是不是我们想要的,说明白了就是读文件然后用正则表达式去匹配。然后当我们开始写的时候就发现其实并不是那么容易的事情,因为每次运行了超过1,2个小时的时候,就会造成内存过高,连我的开发用的机器的4G内存都内存不够,导致系统强制的清理内存。这个原因找了很长时间,但是都没有找到。那个时候我老板和我说ReadToEnd方法很好,应该用ReadToEnd,因为如果ReadToEnd方法还没ReadToLine性能高的话,那么就说明.NET框架有问题。

后来一直没解决,而这个程序又要Release了,怎么办呢,我偷偷的改成了ReadToLine方法,然后程序就OK了,虽然CPU会偶尔消耗的高一点,但是无伤大雅。

最近忽然无聊起来又想到这个问题,对任何问题也不能放过,而因为这个问题我也专门写了一个CodeTimer,程序性能计数器,可以到这里看到,顺便计算一下我们程序的性能。程序代码如下所示。

static void Main(string[] args)
{
    CodeTimer timer
= new CodeTimer(false);
    timer.Time(
"ReadToEnd", 100, ReadToEnd);
    timer.Time(
"ReadLine",100,ReadLine);
    Console.ReadKey();
}

static void ReadToEnd()
{
    StreamReader reader
= new StreamReader("test.txt");
    
string text = reader.ReadToEnd();
}

static void ReadLine()
{
    StreamReader reader
=new StreamReader("test.txt");
    
while (reader.Peek() > 0)
    {
        
string tempString = reader.ReadLine();
    }
}

上面的代码很简单,就是一个ReadToEnd方法和ReadToLine方法,然后用我们的性能计数器计算执行的过程,这里test.txt是一个大文件,大概有4-5M左右,然后我们用ReadToEnd和ReadToLine方法读文件各100遍,我们可以看看结果。

ReadToEnd
——————–
Time Elapsed:80,490ms
CPU Cycles:559,375,000
Gen 0: 699
Gen 1: 699
Gen 2: 700

ReadLine
——————–
Time Elapsed:47,066ms
CPU Cycles:468,437,500
Gen 0: 31249
Gen 1: 4
Gen 2: 701

我们可以看到ReadToEnd的方法消耗的时间很多,比ReadToLine方法要多一倍出来。但是ReadToLine方法的垃圾回收回收了近3万次,在性能上也几乎差不多,但是从内存的增长上面来说,ReadToEnd方法会造成内存泄漏,这是为什么呢。

这是因为首先string是一个独特的类型,在.NET中是非常特殊的一种数据结构,可以看做为引用类型,在处理变量的时候,.NET也会有一个“池,当我们下次再访问string变量的时候,看有没有这个变量,再进行处理,而所以在回收的时候,并不会完全的回收我们的string变量,特别是一次读4M的文件到内存中,一次回收大约会回收30%的空间,但是在循环体之内的话,回收的就更少了,因为作为告诉交换的变量,string的处理是特别复杂的。

所以在处理大文件的时候,我们最好不要用ReadToEnd方法,那么如果是小文件的话呢,我们可以看看结果。

ReadToEnd
——————–
Time Elapsed:21ms
CPU Cycles:312,500
Gen 0: 2
Gen 1: 0
Gen 2: 1

ReadLine
——————–
Time Elapsed:20ms
CPU Cycles:156,250
Gen 0: 2
Gen 1: 0
Gen 2: 2

我们可以看出来在读小文件的时候,大部分情况ReadToEnd和ReadToLine是没有很大的区别的,只是相差1毫秒左右,但是在结果上,ReadToLine还是少有胜算的(多执行几次)。所以说在读小文件的时候,ReadToEnd或者ReadToLine就没那么重要了。但是这里有一个问题就出现了。在读文件之后,我们必定是要处理一些文字的,比如用正则表达式匹配文字,是不是还是这样的结果呢?因为光比较ReadToLine和ReadToEnd方法在性能上并不会有很大的差异。OK,我们再把代码改一下(注意,这里都匹配最后一行的结果,保证前面全部遍历到了,而且只遍历10次)。

static void Main(string[] args)
{
    CodeTimer timer
= new CodeTimer(false);
    timer.Time(
"ReadToEnd", 10, ReadToEnd);
    timer.Time(
"ReadLine",10,ReadLine);
    Console.ReadKey();
}

static void ReadToEnd()
{
    StreamReader reader
= new StreamReader("test.txt");
    
string text = reader.ReadToEnd();
    
if (Regex.IsMatch(text, "TMID=%229fe92282539b467a8ad1bcc214e6991f
        %22%20MI=%22"))
    {
        
//do something
        string str = string.Empty;
    }
    text
= null;
}

static void ReadLine()
{
    StreamReader reader
=new StreamReader("test.txt");
    
while (reader.Peek() > 0)
    {
        
string tempString = reader.ReadLine();
        
if (Regex.IsMatch(tempString, "TMID=%229fe92282539b467a8ad1bcc214e6991f
            %22%20MI=%22"))
        {
            tempString
= null;
            
break;
        }
    }
}

我们可以来看看结果,结果如下。

ReadToEnd
——————–
Time Elapsed:23,782ms
CPU Cycles:65,625,000
Gen 0: 34
Gen 1: 34
Gen 2: 20

ReadLine
——————–
Time Elapsed:3,346ms
CPU Cycles:33,281,250
Gen 0: 1744
Gen 1: 0
Gen 2: 21

我们可以看到,在时间上,ReadToLine是完胜的,虽然做正则表达式的次数多了,但是在性能上和内存上的优势还是有很显著的效果的,现在我们再来读小文件,看看结果。

ReadToEnd
——————–
Time Elapsed:16ms
CPU Cycles:0
Gen 0: 0
Gen 1: 0
Gen 2: 1

ReadLine
——————–
Time Elapsed:4ms
CPU Cycles:0
Gen 0: 0
Gen 1: 0
Gen 2: 2

我们从结果可以看到,性能上的差别还是很大的,ReadToEnd是ReadToLine的好几倍。

结论

当然这里并不能说明ReadToLine和ReadToEnd哪个的性能更高,因为应用的场合并不相同,但是从运行的结果来说我们已经比较清楚的知道谁的性能高。在处理大型文件的时候,ReadToLine的方法虽然CPU占用会高,但是从时间和性能上来说是非常划得来的,而在处理小型文件的时候,ReadToLine和ReadToEnd方法本质上没有什么性能的优劣。但是当处理我们的字符的时候,ReadToLine又略胜一筹。

当然,不同的场合也有不同的性能,如果你关注逻辑而不关注性能的话,在少量的文件中,这两种方法都是没有很大的区别的。

297路过 2评论 DotNet 阅读全文..
  1. Meta @

    各有各的好处。。

  2. dresses @

    还在学习中,攻克分词

:-D :-? 8) :cry: 8-O :lol: :-x :-| :?: :-P :oops: :roll: :( :) :-o :wink: more »