由c# string += 引发的性能问题
同学让帮忙调试一个程序。程序主体是一个生产者消费者模式。生产者不断监听指定端口上的udp消息,收到消息就放到消息队列。工作线程则负责处理消息队列中的消息。问题是通过记录下来的时间来看,从收到消息到消息被处理时间过去了几百毫秒。
我首先改了下一些多线程中需要注意的同步事项。比如对于变量的修改,要加锁或者Interlocked。
c#中有一点要注意的,和c++不同,c#中的AutoResetEvent虽然和c++中的condition_variable类似,但是c++中的condition_vairable是在lock块内执行的,当sleep的时候会自动释放锁,唤醒的时候会自动加锁。但是c#中的AutoResetEvent则不是这个逻辑,这个和锁无关,不需要放到lock块内。
然后用jetbrain公司的vs resharp插件来做profile,收集好数据以后用dottrace打开。通过分析工作线程的cpu执行情况,发现工作线程是满负荷的,所以应该不是由于死锁或者锁竞争导致的延时。进一步发现,工作线程99.9%的时间都花在一个叫做bytetohexstr的函数上。这个函数顾名思义,就是把类似\x11\x22的bytes数组转为”1122“这样的string
那么这个函数是怎么写的呢?如下
public string bytetohexstr(byte[] bytes, int length) //字节转化为二进制
{
string returnstr = "";
if (bytes != null)
{
for (int i = 0; i < length; i++)
{
returnstr += bytes[i].ToString("X2");
}
}
return returnstr;
}
看到这问题就很明显了,由于c#中string是不可变对象,每次+=都要分配内存给一个新对象,并且把string的内容拼接复制到新对象。这样这个函数在内存层面上其实是个O(n^2)级别的时间复杂度。同时大量临时对象的产生,也加重了gc的压力。
我把这个函数改成如下:
public string bytetohexstr(byte[] bytes, int length) //字节转化为二进制
{
if (bytes == null)
return "";
char[] r = new char[2 * length];
for(int i=0;i<length;i++)
{
int high = bytes[i] / 16;
int low = bytes[i] % 16;
r[i*2] = idx_to_hex[high];
r[i*2 + 1] = idx_to_hex[low];
}
string ret = new string(r);
return ret;
}
因为我对c#不是很熟悉,后来发现用c#中的stirnbuilder应该效果也差不多。
再调试发现程序一下子飞快,根据profile的结果,四个线程处理十个消息,只需要不到0.7ms。
考虑到根据profile的结果,没必要这么多线程,我就改成了一个生产者,一个消费者的模式,测试下来效果也很好,每次处理10个消息都在1ms以内。
这次调试调试让我感受到代码不注意的话,效率真的可能会相差千倍乃至完备。这个函数的输入字节一般是两千多,按照原来那种写法得有2000*2000/2 = 2million的内存写入,这还没算不断申请小内存以及释放内存消耗的时间。而修改过的代码,内存写入只有2000次,效率相差一千倍。所以造成几百毫秒和零点几毫秒的极端差距。