StreamReader和StreamWriter的性能问题

时间:2014-02-27 13:10:37

标签: c# .net performance stream

我刚刚在我的应用程序中发现了与StreamWriterStreamReader的创建相关的性能问题。我一直在测试一个非常简单的应用程序的性能,这些测试已经在同一台机器上本地完成了几次。

客户端应用程序,尝试创建4000个连接并每200ms发送一条消息。服务器是一个接受连接的ECHO服务,并返回输入。

使用此代码in the server to handle the socket connection and return data

StreamReader sr = null;
StreamWriter sw = null;
try
{
    var stream = client.GetStream();
    sr = new StreamReader(stream, Encoding.UTF8);
    sw = new StreamWriter(stream, Encoding.UTF8);
    while (!cancel.IsCancellationRequested && client.Connected)
    {
        var msg = await sr.ReadLineAsync(); ;

        if (msg == null)
            continue;

        _inMessages.Increment();    
        _inBytes.IncrementBy(msg.Length);   

        await sw.WriteLineAsync(msg);   
        await sw.FlushAsync();  

        _outMessages.Increment();   
        _outBytes.IncrementBy(msg.Length);  

    }   
}   
catch (Exception aex)   
{   
    var ex = aex.GetBaseException();    
    Console.WriteLine("Client error: " + ex.Message);   
}   
finally 
{   
    _connected.Decrement(); 
    if(sr != null)  
        sr.Dispose();   

    if(sw != null)  
        sw.Dispose();   
}   

允许非常快速地连接4000个客户端,使用28-30%的CPU每秒处理大约14000(14千)条消息。

另一方面,使用此代码:

 StreamReader sr = null;
 StreamWriter sw = null;
 try
 {
     var stream = client.GetStream();
     while (!cancel.IsCancellationRequested && client.Connected)
     {
         sr = new StreamReader(stream, Encoding.UTF8); // moved
         sw = new StreamWriter(stream, Encoding.UTF8); // moved

         var msg = await sr.ReadLineAsync(); ;

         if (msg == null)
             continue;

         _inMessages.Increment();
         _inBytes.IncrementBy(msg.Length);

         await sw.WriteLineAsync(msg);
         await sw.FlushAsync();

         _outMessages.Increment();
         _outBytes.IncrementBy(msg.Length);

     }
 }
 catch (Exception aex)
 {
     var ex = aex.GetBaseException();
     Console.WriteLine("Client error: " + ex.Message);
 }
 finally
 {
     _connected.Decrement();
     if(sr != null)
         sr.Dispose();

     if(sw != null)
         sw.Dispose();
 }

允许连接4000个客户端,但最后500个客户端需要一段时间才能连接。使用30-32%的CPU,每秒处理大约6000条消息。

在两次测试中,大约有20%-30%的CPU可用且RAM内存充足。

我知道在循环中创建对象效率不高,但这种影响太大了,我想了解这里发生了什么。如果在第二个代码段中,我将srsw放在using语句上,更糟糕的是,只有1500个客户端可以连接,并且每秒只处理大约1000条消息,可能是因为StreamReaderStreamWriter正在处理(或试图处置)基础NetworkStream

仅仅因为StreamReaderStreamWriter对象分配,性能是否会降低很多?或者那些特定的类还有其他东西吗?

完整代码可在此处找到:https://github.com/vtortola/AynchronousTCPListener

在实际代码中,直到我读取Stream(帧头)的第一个字节,我不知道信息是二进制还是文本,所以我不能在手工创建读写器之前。基本上我得到了头,然后我可以决定如何处理消息。什么是更好的方法?

更新

我已启用两个性能计数器来监视服务器应用程序的线程数。我让它跑了5分钟,这些是我得到的数字:

使用第一个代码段(快速代码段):

# of current logical Threads: 108
# of current physical Threads: 106

使用第二个代码段(慢速代码段):

# of current logical Threads: 22
# of current physical Threads: 20

这解释了为什么性能下降,但为什么这会在线程中产生如此大的影响呢?

此外,第一种情况下的内存使用量约为120Mb,而第二种情况下320Mb的内存使用量增加了266%。

1 个答案:

答案 0 :(得分:5)

第二个版本存在一些问题,而较慢的消息处理只是冰山一角。让我们开始吧。

为什么它变慢?

你是对的,创建对象不应该是一个问题,它不是,但在内存中管理它们。如果将GC性能计数器添加到性能监视器,您会发现垃圾收集量急剧增长。看看下面的2张图片:

首先(正确)案例:

enter image description here

第二(错误)案例:

enter image description here

CPU花在垃圾收集上的时间要高得多,从而为代码留下更少的宝贵CPU时间。此外,正如您所指出的,在第二种情况下,内存使用率远高于第一种情况。要理解这一点,您需要了解如何构建GC堆。基本上有3个称为代的内存段(还有大对象堆,但在我们的情况下它不感兴趣):

  • Gen0(最小的)适用于即使在一个GC集合中也没有存活的短期对象
  • Gen1(中等大小)适用于在您的应用程序中存活时间较长且在一个GC集合中存活的对象(因此从Gen0移出)
  • Gen2(最大的)用于长寿命对象 - 生活在这里的对象没有时间限制(因为大小)Gen2垃圾收集是最昂贵的,因此GC的执行次数较少

回到您的应用程序。当您在每个循环中创建StreamReaderStreamWriter类时,您正在快速耗尽Gen0可用空间,从而迫使GC收集此段中的内存。流对象不会立即处理,因为异步任务可能会保留对它们的引用。因此,它们被移动到Gen1段,再次耗尽它并导致GC执行Gen1垃圾收集。最后,他们要么通过Getn1垃圾收集处理,要么在Gen2中登陆。正如我之前所说的,当存储在其中的对象未被处理时Gen2的大小增加,这解释了第二种情况下更高的内存使用量。由于GC不愿意执行Gen2集合,您的网络流(由读取器和写入程序使用)不会快速处理,从而允许您的服务器接收客户端消息。我们正在慢慢转向下一个点:

为什么这是错的?

当您创建读者和编写者时,您正在使用构造函数来强制他们在处置时关闭底层流。这意味着您无法控制何时关闭客户端的网络流。这就是为什么您可能会在客户端观察到许多连接丢失和重试的原因。对于这种情况,更适合的构造函数是:

sr = new StreamReader(stream, Encoding.UTF8, true, 1024, true);
sw = new StreamWriter(stream, Encoding.UTF8, 1024, true);

将未连接的连接打开,让你负责处理它(你应该这样做)。最后,继续使用第一个版本,但更改读者和编写者的构造函数,并将client.Dispose添加到finally块:)