SUMTEC -- There's a thing in my bloglet.

But it's not only one. It's many. It's the same as other things but it exactly likes nothing else...

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::

这篇文章仍然是以前的Blog上面的,不过里面就只有一句话:

编写更快的托管代码:了解开销情况

不要害怕,是中文的。

因为当时没有时间,所以就没有写更多的文字上去。现在借着换地方的机会,写写关于优化的东西吧。(今天太倒霉了!要打电话没电话卡,要放风筝下雨了,要写Blog因为文章太长Cookie超时没有发上去……)

其实在.NET底下,有很多东西和传统的做法是一致的,例如:不要过分扩展数据的宽度,例如用int/Integer就能够完成的事情却用long/Long甚至是double/Double来做。因为long/Long实际上要变成两个int/Integer操作,而double/Double操作也并不见得就像测试里面一样和整型计算一样快。当然,我们没有必要将所有的东西都往小的地方缩减,例如int/Integer就没有必要变成short/Short,因为同样在CPU数据处理长度以内,缩减宽度对于速度的影响非常小。反过来讲,如果算法选择正确,而仍然希望提高速度,那么就可以考虑对数据类型的优化。例如,如果有一幅图片,希望对内部每一个点的R、G、B值分别进行累加并求出平均值,平均值要求达到double精度,那么也许你会写成:

double R, G, B;
int x,  y, w, h;
int count;

w = Picture.Width;
h = Picture.Height;
R = G = B = 0;
count = 0;
for (x = 0; x < w; x++)
{
  for (y = 0; y < h; y++)
  {
    R += Picture.GetPixel(x, y).R;
    G += Picutre.GetPixel(x,y).G;
    B += Picture.GetPixel(x,y).B;
    count++;
  }
}
R /= count;
G /= count;
B /= count;

这段程序看起来没有什么大问题,确实,如果你对于速度不敏感的话完全没关系。但是如果你需要“分秒必争”,那么就得改一下了:

double R, G, B;
int R_tmp, G_tmp, B_tmp;
int x,  y, w, h;
int count;

w = Picture.Width;
h = Picture.Height;
R_tmp = G_tmp = B_tmp = 0;
count = 0;
for (x = 0; x < w; x++)
{
  for (y = 0; y < h; y++)
  {
    Color color;
    color = Picture.GetPixel(x, y);
    R_tmp += color.R;
    G_tmp += color.G;
    B_tmp += color.B;
    count++;
  }
}
count = w * h;
R =  R_tmp / count;
G = G_tmp / count;
B = B_tmp / count;

实际上这是CSDN上面一篇文章所讨论内容的简化,看看原文会看到更多的优化方式。而我所介绍的那篇文章里面,有很多其他方面的建议,这些建议也许是我们所想不到的,例如那个数组和链表的比较。里面的这句话最有趣:“在 100,000 个项目的示例中,处理器要花费全部时间的(平均)大约 (22-3.5)/22 = 84%,来等待从 DRAM 读取某个列表节点的缓存行。这听起来很糟糕,但实际情况可能会比这更糟糕。”也许大家和我一样,会在一开始就认定由于链表需要获取下一个元素的地址,所以会比数组多至少一条指令,因此判定数组快。可是作者却明确的用汇编告诉你,链表比数组少一半的指令(确实是事实),那么你会不会和我一样认为链表也许会表现得更好呢?结果如何还是请诸位看管自己看看原文吧。

实际上那个例子完全是为了体现这个特殊的瓶颈所“作”出来的,作者是想告诉我们,要充分相信CPU的处理能力,但是要怀疑内存的速度,甚至是缓存的速度。如果可能的话,尽可能减少所需要访问数据的数量;如果不行,那就尽可能让访问的数据连续;如果不能够保证数据的连续性,那么尽可能保证一次访问的数据尽量小;如果都不能保证,那么至少要避免大对象的创建——如果出现大对象把其他数据“挤”到硬盘里面去的情况,那么梦魇就开始了:访问一次虚拟内存也许会消耗90M个CPU指令,完全可以抵消所有理论上优秀算法所带来的好处。

如果说你因此对于使用链表等其他数据结构产生畏惧的想法,那么你就错了。在不同的场合,不同的数据结构会有不同的效果。例如:如果需要查找两个字符串数组(元素是类似"123"的数值字符串)里面都包含的数值,如果你用一个bool数组abc,用abc[123] = true表示在字符串数组1出现过"123",那还不如用Hashtable呢,因为如果数字的范围是从0到40M,那么你就需要160M的内存,而实际上也许这些字符数组一共就只有100个字符串。

这篇文章大部分的东西都是值得一看的,甚至文章最后“资源”一节里面有好几篇文章都写的非常精彩,如果不怕英文的话还是要看一看的。但是我对于其中一篇文章里面提到的关于循环优化的部分:

 


for(int i = 0; i < myArray.Length; i++)
{

}


int l = myArray.Length;
for(int i = 0; i < l; i++)
{

}

 

表示深切的怀疑,因为就我的经验来说,似乎编译器并没有优化到可以把每次调用Length的操作提前到循环的外面,JIT似乎也没有这个能力。我认为无论编译器还是JIT,目前都没有可能把常量复制传播这一块做得那么好,以至于可以分析出在循环内部不存在修改myArray.Length所对应的私有变量的地方。由于不能够做出这样的判断,所以每一次循环都需要调用myArray.Length,顶多只能把这个调用转变成inline代码。即使是inline的代码,左边的代码仍然每次都需要比右边的代码多至少一条mov语句。实际的情况可能更糟糕,因为有可能某些集合的Length或者Count所包含的IL超过32条,或者使用的是一个接口,这些都使得inline优化变得不可能,因此右边的优化很多时候都能够带来可感觉的效果。也许这种优化对于台式机是不重要的,但对于PDA上的程序却是非常重要的。如果你不能够接受右边的这种优化形式,那么至少也应该注意尽量不要使用foreach而应该用for来代替,foreach是怎样的牺牲效率在同一篇文章有介绍,也请有兴趣知道的各位亲自去看。

上面这一堆的东西,都是别人的劳动成果,下面说说我的一个测试:

如果你想在两个数族之间进行拷贝,例如从arr拷贝到arr2,那么可以有下面几种方式:

1.
arr2 = (int[]) arr.Clone();

2.
arr2 = new int[arr.Length]
arr.CopyTo(arr2, 0);

3.
arr2 = new int[arr.Length]
Array.Copy(arr, arr2, arr.Length)

4.
arr2 = new int[arr.Length]
Array.Copy(arr, 0, arr2, 0, arr.Length)

你猜一下哪一个快?具体的花销关系如何?

呵呵,我告诉你在PC上面的结果吧:
方法(号码)     用时(某种时间单位)
1                        7.2
2                        6.9
3                        5.8
4                        5.0

最快的和罪慢的差别可以达到接近50%,甚至和第二快的也相差16%以上!造成这种差别的原因是什么呢?首先说说2、3、4这三种方式的问题:实际上Array.Copy(arr, arr2, arr.Length)实际上调用的是Array.Copy(arr, 0, arr2, 0, arr.Length),arr.CopyTo则是调用前者的,实际上出了调用之外还有一些额外的代码,一次调用可以占用非常大的CPU额外开销,所以你可以看到2、3、4之间比较明显的、逐渐减少的用时差别。如果你的程序在很多地方都不注意这些细节,那么我可以保证你的程序肯定和最快的方法相差15%以上,这是在没有考虑改变算法的情况下的最保守估计,实际上应该比这个更离谱!

好了,今天先写到这里。


文章来源:http://dotnet.blogger.cn/sumtec/articles/175.aspx
posted on 2004-03-02 11:36  Sumtec  阅读(1419)  评论(0编辑  收藏  举报