公司里新上了一个项目,在做性能测试的时候发现一个奇怪的问题,跑同一个流程,在一个48核(HP580 G7 PC server)的服务器上耗时120秒,而在一个4核心的PC机上只要90秒,带着这样的疑问,公司请了微软的相关工程师来解决此问题。
经过一天的跟踪调试和优化,把耗时降至70几秒,这其中过程包含几个.net对象的优化,确实效果很明显。此文是阶段性结论的一个笔记,也蛮具有指导意义。
一、DataView.ToTable()后的性能问题
工程师在跟踪代码的时候发现处理这个方法的时候很慢,占居了整个逻辑代码的大部分时间。优化这个方法后效果很明显。
查了MSDN,官方并没有给出DataView.ToTable()方法关于性能方面的提示,但是要MSDN看这个方法时,发现老外在06年的时候已经在下面回了一段代码,通过三个方式执行这个方法,发现效率上差异很大。 (MSDN:DataView.ToTable())。
我复制代码,创建了一个新的工程,确实可以重现问题。整个过程大概如下:
背景是有一个有500000条数据的DataTable,然后把这个DataTable中的数据赋给DataView,然后从这个DataView通过ToTable()方法,把数据转给另外一个同架构的DataTable。
第一种方式 创建目标DataTable对象,直接通过toTable()方法转换。过程耗时:27.0504秒。
第二种方式 创建目标DaTaTable后,对其设置PrimaryKey。然后通过下面代码向目标表添加数据。
foreach (DataRow dr in ds.Tables[0].Rows)
{
DataRow[] drrepetido = dsRes.Tables[0].Select("valor=" + dr["Valor"]);
if (drrepetido.Length == 0)
dsRes.Tables[0].ImportRow(dr);
}
过程耗时:11.7624秒。
第三种方式 不根据原架构创建目标DataTable,全新实例化DataTable对象,然后向其添加对应列,然后创建哈希表,循环写入目标表,代码如下:
DataTable dt = new DataTable();
dt.Columns.Add("valor", ds.Tables[0].Columns["valor"].DataType);
Hashtable ht = new Hashtable();
foreach (DataRow dr in ds.Tables[0].Rows)
{
if (!ht.ContainsKey(dr[0]))
{
ht.Add(dr[0], null);
DataRow newRow = dt.NewRow();
newRow[0] = dr[0];
dt.Rows.Add(newRow);
}
}
过程耗时:0.2184秒。
三种方式对比,发现非常恐怖。但是微软好像还没有给出原因。
二、StringBuilder()使用不档带来的性能问题
关于StringBuilder之前也看过很多文章,这次开发商程序中的问题和以前的类似,实例化时没有设置一个比较推荐的长度值,导致在循环体内不断appand()后,StringBuilder对象不断被GC处理。从而消耗了很多时间。
关于StringBuilder()对象,还是再多加几句吧。其默认维护的Capacity值是16。
因为StringBuilder对象的创建代价较大,在字符串连接目标较少的情况下,过度滥用StringBuilder会导致性能的浪费而非节约。只有大量的或者无法预知次数的字符串操作,才考虑以StringBuilder来实现。 String类型的“+”连接操作,实际上是重载操作符“+”调用String.Concat来操作,而编译器则会优化这种连接操作的处理,编译器根据其传入参数的个数,一次性分配相应的内存,并依次拷入相应的字符串。 StringBuilder在使用上,最好指定合适的容量值,否则由于默认容量不足而频繁的进行内存分配操作,是不妥的实现方法。 通常情况下,进行简单字符串连接时,应该优先考虑使用String.Concat和String.Join等操作来完成字符串的连接,但是应该留意String.Concat可能存在的装箱操作。
三、拆装箱带来性能问题
如果用StringBuilder.toString().trim()来判断StringBuilder是否有值的情况,可以用StringBuilder.Lenth()来代替。