Hello World
Spiga

从.NET中委托写法的演变谈开去(下):性能相关

2009-08-10 00:44 by 老赵, 12150 visits

上一篇文章中,我们详细讲述了C# 3.0中Lambda表达式(构造委托)的使用方式,它在语义上的优势及对编程的简化——这些内容已经属于委托的“扩展内容”。不如这次谈得更远一些,就来讨论一下上文中“编程方式”的性能相关话题。

循环分离及其性能

上文的第一个示例中,我们演示了如何使用Lambda表达式配合.NET 3.5中定义的扩展方法来方便地处理集合中的元素(筛选,转化等等)。不过有朋友可能会提出,那个“普通写法”并非是性能最高的实现方法。方便起见,也为了突出“性能”方面的问题,我们把原来的要求简化一下:将序列中的偶数平方输出为一个列表。按照那种“普通写法”可能就是:

static List<int> EvenSquare(IEnumerable<int> source)
{
    var evenList = new List<int>();
    foreach (var i in source)
    {
        if (i % 2 == 0) evenList.Add(i);
    }

    var squareList = new List<int>();
    foreach (var i in evenList) squareList.Add(i * i);

    return squareList;
}

从理论上来说,这样的写法的确比以下的做法在性能要差一些:

static List<int> EvenSquareFast(IEnumerable<int> source)
{
    List<int> result = new List<int>();
    foreach (var i in source)
    {
        if (i % 2 == 0) result.Add(i * i);
    }

    return result;
}

在第二种写法直接在一次遍历中进行筛选,并且直接转化。而第一种写法会则根据“功能描述”将做法分为两步,先筛选后转化,并使用一个临时列表进行保存。在向临时列表中添加元素的时候,List<int>可能会在容量不够的时候加倍并复制元素,这便造成了性能损失。虽然我们通过“分析”可以得出结论,不过实际结果还是使用CodeTimer来测试一番比较妥当:

List<int> source = new List<int>();
for (var i = 0; i < 10000; i++) source.Add(i);

// 预热
EvenSquare(source);
EvenSquareFast(source);

CodeTimer.Initialize();
CodeTimer.Time("Normal", 10000, () => EvenSquare(source));
CodeTimer.Time("Fast", 10000, () => EvenSquareFast(source));

我们准备了一个长度为10000的列表,并使用EvenSquare和EvenSquareFast各执行一万次,结果如下:

Normal
        Time Elapsed:   3,506ms
        CPU Cycles:     6,713,448,335
        Gen 0:          624
        Gen 1:          1
        Gen 2:          0

Fast
        Time Elapsed:   2,283ms
        CPU Cycles:     4,390,611,247
        Gen 0:          312
        Gen 1:          0
        Gen 2:          0

结果同我们料想中的一致,EvenSquareFast无论从性能还是GC上都领先于EvenSquare方法。不过,在实际情况下,我们该选择哪种做法呢?如果是我的话,我会倾向于选择EvenSquare,理由是“清晰”二字。

EvenSquare虽然使用了额外的临时容器来保存中间结果(因此造成了性能和GC上的损失),但是它的逻辑和我们需要的功能较为匹配,我们可以很容易地看清代码所表达的含义。至于其中造成的性能损失在实际项目中可以说是微乎其微的。因为实际上我们的大部分性能是消耗在每个步骤的功能上,例如每次Int32.Parse所消耗的时间便是一个简单乘法的几十甚至几百倍。因此,虽然我们的测试体现了超过50%的性能差距,不过由于这只是“纯遍历”所消耗的时间,因此如果算上每个步骤的耗时,性能差距可能就会变成10%,5%甚至更低。

当然,如果是如上述代码那样简单的逻辑,则使用EvenSquareFast这样的实现方式也没有任何问题。事实上,我们也不必强求将所有步骤完全合并(即仅仅使用1次循环)或完全分开。我们可以在可读性与性能之间寻求一种平衡,例如将5个步骤使用两次循环来完能是更合适的方式。

说到“分解循环”,其实这类似于Martin Fowler在他的重构网站所上列出的重构方式之一:“Split Loop”。虽然Split Loop和我们的场景略有不同,但是它也是为了代码的可读性而避免将多种逻辑放在一个循环内。将循环拆开之后,还可以配合“Extract Method”或“Replace Temp with Query”等方式实现进一步的重构。自然,它也提到拆分后的性能影响:

You often see loops that are doing two different things at once, because they can do that with one pass through a loop. Indeed most programmers would feel very uncomfortable with this refactoring as it forces you to execute the loop twice - which is double the work.

But like so many optimizations, doing two different things in one loop is less clear than doing them separately. It also causes problems for further refactoring as it introduces temps that get in the way of further refactorings. So while refactoring, don't be afraid to get rid of the loop. When you optimize, if the loop is slow that will show up and it would be right to slam the loops back together at that point. You may be surprised at how often the loop isn't a bottleneck, or how the later refactorings open up another, more powerful, optimization.

这段文字提到,当拆分之后,您可能会发现更好的优化方式。高德纳爷爷也认为“过早优化是万恶之源”。这些说法都在“鼓励”我们将程序写的更清晰而不是“看起来”更有效率

扩展方法的延迟特性

对于上面的简化需求,使用Lambda表达式和.NET 3.5中内置的扩展方法便可以写成这样:

static List<int> EvenSquareLambda(IEnumerable<int> source)
{
    return source.Where(i => i % 2 == 0).Select(i => i * i).ToList();
}

应该已经有许多朋友了解了.NET 3.5中处理集合时扩展方法具有“延迟”的效果,也就是说Where和Select中的委托(两个Lambda表达式)只有在调用ToList方法的时候才会执行。这是优点也是陷阱,在使用这些方法的时候我们还是需要了解这些方法的效果如何。不过这些方法其实都没有任何任何“取巧”之处,换句话说,它们的行为和我们正常思维的结果是一致的。如果您想得明白,能够自己写出类似的方法,或者能够“自圆其说”,十有八九也不会有什么偏差。但是如果您想不明白它们是如何构造的,还是通过实验来确定一下吧。实验的方式其实很简单,只要像我们之前验证“重复计算”陷阱那种方法就可以了,也就是观察委托的执行时机和顺序进行判断。

好,回到我们现在的问题。我们知道了“延迟”效果,我们知道了Where和Select会在ToList的时候才会进行处理。不过,它们的处理方式是什么样的,是像我们的“普通方法”那样“创建临时容器(如List<T>),并填充返回”吗?对于这点我们不多作分析,还是通过“观察委托执行的时机和顺序”来寻找答案。使用这种方式的关键,便是在委托执行时打印出一些信息。为此,我们需要这样一个Wrap方法(您自己做试验时也可以使用这个方法):

static Func<T, TResult> Wrap<T, TResult>(
    Func<T, TResult> func,
    string messgaeFormat)
{
    return i =>
    {
        var result = func(i);
        Console.WriteLine(messgaeFormat, i, result);
        return result;
    };
}

Wrap方法的目的是将一个Func<T, TResult>委托对象进行封装,并返回一个类型相同的委托对象。每次执行封装后的委托时,都会执行我们提供的委托对象,并根据我们传递的messageFormat格式化输出。例如:

var wrapper = Wrap<int, int>(i => i + 1, "{0} + 1 = {1}");
for (var i = 0; i < 3; i++) wrapper(i);

则会输出:

0 + 1 = 1
1 + 1 = 2
2 + 1 = 3

那么,我们下面这段代码会打印出什么内容呢?

List<int> source = new List<int>();
for (var i = 0; i < 10; i++) source.Add(i);

var finalSource = source
    .Where(Wrap<int, bool>(i => i % 3 == 0, "{0} can be divided by 3? {1}"))
    .Select(Wrap<int, int>(i => i * i, "The square of {0} equals {1}."))
    .Where(Wrap<int, bool>(i => i % 2 == 0, "The result {0} can be devided by 2? {1}"));

Console.WriteLine("===== Start =====");
foreach (var item in finalSource)
{
    Console.WriteLine("===== Print {0} =====", item);
}

我们准备一个列表,其中包含0到9共十个元素,并将其进行Where…Select…Where的处理,您可以猜出经过foreach之后屏幕上的内容吗?

===== Start =====
0 can be divided by 3? True
The square of 0 equals 0.
The result 0 can be devided by 2? True
===== Print 0 =====
1 can be divided by 3? False
2 can be divided by 3? False
3 can be divided by 3? True
The square of 3 equals 9.
The result 9 can be devided by 2? False
4 can be divided by 3? False
5 can be divided by 3? False
6 can be divided by 3? True
The square of 6 equals 36.
The result 36 can be devided by 2? True
===== Print 36 =====
7 can be divided by 3? False
8 can be divided by 3? False
9 can be divided by 3? True
The square of 9 equals 81.
The result 81 can be devided by 2? False

列表中元素的执行顺序是这样的:

  1. 第一个元素“0”经过Where…Select…Where,最后被Print出来。
  2. 第二个元素“1”经过Where,中止。
  3. 第三个元素“2”经过Where,中止。
  4. 第四个元素“4”经过Where…Select…Where,中止。
  5. ……

这说明了,我们使用.NET框架自带的Where或Select方法,最终的效果和上一节中的“合并循环”类似。因为,如果创建了临时容器保存元素的话,就会在第一个Where中把所有元素都交由第一个委托(i => i % 3 == 0)执行,然后再把过滤后的元素交给Select中的委托(i => i * i)执行。请注意,在这里“合并循环”的效果对外部是隐藏的,我们的代码似乎还是一步一步地处理集合。换句话说,我们使用“分解循环”的清晰方式,但获得了“合并循环”的高效实现。这就是.NET框架这些扩展方法的神奇之处1

在我们进行具体的性能测试之前,我们再来想一下,这里出现了那么多IEnumerable对象实现了哪个GoF 23中的模式呢?枚举器?看到IEnumerable就说枚举器也太老生常谈了。其实这里同样用到了“装饰器”模式。每次Where或Select之后其实都是使用了一个新的IEnumerable对象来封装原有的对象,这样我们遍历新的枚举器时便会获得“装饰”后的效果。因此,以后如果有人问您“.NET框架中有哪些的装饰器模式的体现”,除了人人都知道的Stream之外,您还可以回答说“.NET 3.5中System.Linq.Enumerable类里的一些扩展方法”,多酷。

扩展方法的性能测试

经过上节的分析,我们知道了Where和Select等扩展方法统一了“分解循环”的外表和“合并循环”的内在,也就是兼顾了“可读性”和“性能”。我们现在就使用下面的代码来验证这一点:

List<int> source = new List<int>();
for (var i = 0; i < 10000; i++) source.Add(i);

EvenSquare(source);
EvenSquareFast(source);
EvenSquareLambda(source);

CodeTimer.Initialize();
CodeTimer.Time("Normal", 10000, () => EvenSquare(source));
CodeTimer.Time("Fast", 10000, () => EvenSquareFast(source));
CodeTimer.Time("Lambda", 10000, () => EvenSquareLambda(source));

结果如下:

Normal
        Time Elapsed:   3,127ms
        CPU Cycles:     6,362,621,144
        Gen 0:          624
        Gen 1:          3
        Gen 2:          0

Fast
        Time Elapsed:   2,031ms
        CPU Cycles:     4,070,470,778
        Gen 0:          312
        Gen 1:          0
        Gen 2:          0

Lambda
        Time Elapsed:   2,675ms
        CPU Cycles:     5,672,592,948
        Gen 0:          312
        Gen 1:          156
        Gen 2:          0

从时间上看,“扩展方法”实现的性能介于“分解循环”和“合并循环”两者之间。而GC方面,“扩展方法”实现也优于“分解循环”(您同意这个看法吗?)。因此我们可以得出以下结论:

  性能 可读性
分解循环 No. 3 No. 2
合并循环 No. 1 No. 3
扩展方法 No. 2 No. 1

至于选择哪种方式,就需要您自行判断了。

值得注意的是,无论是“延迟”还是“分解循环”的效果,刚才我们都是针对于Where和Select来谈的。事实上,还有并不是所有的扩展方法都有类似的特性,例如:

  • 非延迟:ToArray、ToList、Any,All,Count……
  • 非分解循环:OrderBy,GroupBy,ToDictionary……

不过别担心,正如上节所说,是否“延迟”,是否“分解循环”都是非常明显的。如果您可以写出类似的方法,或者能够“自圆其说”,一般您的判断也不会有什么错误。例如,OrderBy为什么不是“分解循环”的呢?因为在交由下一步枚举之前,它必须将上一步中的所有元素都获取出来才能进行排序。如果您无法“很自然”地想象出这些“原因”,那么就写一段程序来自行验证一番吧。

其他性能问题

一般来说,这些扩展方法本身不太会出现性能问题,但是任何东西都可能被滥用,这才是程序中的性能杀手。例如:

IEnumerable<int> source = ...;
for (var i = 0; i < source.Count(); i++)
{ 
    ...
}

这段代码的问题,在于每次循环时都需要不断地计算出source中元素的数量,这意味着不断地完整遍历、遍历。对于一些如Count或Any这样“立即执行”的方法,我们在使用时脑子里一定要留有一些概念:它会不会出现性能问题。这些问题的确很容易识别,但是我的确看到过这样的错误。即使在出现这些扩展方法之前,我也看到过某些朋友编写类似的代码,如在for循环中不断进行str.Split(',').Length。

还是再强调一下“重复计算”这个问题吧,它可能更容易被人忽视。如果您“重复计算”的集合是内存中的列表,这对性能来说可能影响不大。但是,试想您每次重复计算时,都要重复去外部数据源(如数据库)获取原始数据,那么此时造成的性能问题便无法忽视了。

总结

这个系列的文章就写到这里,似乎想谈的也谈的差不多了。这三篇文章是由《关于最近面试的一点感想》引起的,我写文章一开始的目的也是希望证明“委托也是个值得一提的内容”。了解委托的写法,了解这些变化并非仅仅是“茴有几种写法”。这里引用钧梓昊逑同学的评论,我认为说得很有道理:

我觉得问某件事物在不同的版本中有什么区别还是可以比较容易判断出他的掌握和理解程度的。新版本中每一个新特性的加入并不是随意加的,为什么要加,加之前有什么不足,加了之后有什么好处,在什么时候什么场景下用这些新特性,用了有什么风险和弊端,等等。如果能非常流利得回答,就算不是很正确也至少说明他去认真思考过了,因为这些东西可能是网上书上所没有的。

所以,我在面试时也会提出“delegate写法演变”或类似的问题。甚至我认为这是一个非常好的入手点,因为在.NET中如此经典的演变并不多见。如果一个人可以把这些内容都理清,我有理由相信他对待技术的态度是非常值得肯定的。反观原文后许多带有嘲讽的评论,在这背后我不知道他们是真正了解了委托而认为这些内容不值一提,还是“自以为”理解了全部内容,却不知道在这看似简单的背后还隐藏着庞大的深层的思维和理念。

我想,我们还是不要轻易地去“轻视”什么东西吧,例如在面试时轻视对方提出的问题,看重框架而轻视语言,看重所谓“底层”而轻视“应用”。最后,我打算引用韦恩卑鄙同学的话来结束全文:

一般说C语言比C#强大的,C语言也就写到Hello World而已。

相关文章

  • 从.NET中委托写法的演变谈开去(上):委托与匿名方法
  • 从.NET中委托写法的演变谈开去(中):Lambda表达式及其优势
  • 从.NET中委托写法的演变谈开去(下):性能相关
  •  

    注1:虽然Where和Select具有“延迟”效果,但是内部实现是“分解循环”还是“合并循环”则是另一种选择。您能否尝试在“延迟”的前提下,提供“分解循环”和“合并循环”两种Where或Select的实现呢?

    Creative Commons License

    本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名赵劼(包含链接),具体操作方式可参考此处。如您有任何疑问或者授权方面的协商,请给我留言

    Add your comment

    101 条回复

    1. MetaWang[未注册用户]
      *.*.*.*
      链接

      MetaWang[未注册用户] 2009-08-10 00:52:00

      验证码:4944

      老赵辛苦了。这么晚还在写文章。

    2. Colin Han
      *.*.*.*
      链接

      Colin Han 2009-08-10 01:35:00

      呵呵,看完老赵的帖子,才终于明白了面试的时候为什么要问这个问题。

      只不过,当时看到那篇文章,我也一下被问蒙了,想了想,似乎几个版本没有什么大的变化啊。于是就想当然的认为作者在自以为是了。我想很多人嘲讽的人也是出于相同的原因吧。

      谢谢老赵这样刨根问底的人。

    3. Grove.Chu
      *.*.*.*
      链接

      Grove.Chu 2009-08-10 02:02:00

      真是辛苦老赵了,
      看完%……

    4. Pandora
      *.*.*.*
      链接

      Pandora 2009-08-10 07:03:00

      看完了· 受到很多启发。
      我只有一点不明:不同版本之间的delegate,起到的作用,性能是否一致呢?如果是一致的,那对于开发人员来说都是一样的。不同版本之间的写法,也只不过是一个语法层面的问题。就好比java和c#的delegate不同的写法一样,都是语法而已。
      至于为什么这么演变以及更深层次的理解,我觉得是没有必要要求必须掌握的。即便掌握了,在具体的coding中又能有多少帮助呢?在我看来,这已经超出了一般软件设计的范畴。
      所以我觉得,这个问题不如变为:不同版本之间的delegate,是否能带来性能上的差异,维护上的差异还比较实际一点。

    5. 钧梓昊逑
      *.*.*.*
      链接

      钧梓昊逑 2009-08-10 08:48:00

      一般说C语言比C#强大的,C语言也就写到Hello World而已。

      这句很有文采

    6. 老赵
      admin
      链接

      老赵 2009-08-10 09:02:00

      @Pandora
      Java没有delegate。
      从一开始就说了,委托其实都是没有变的,变得只是“写法”。
      既然是“语法糖”,则说明本质都是一样的。至于性能在各版本之间即使有变化,和写法无关。
      我认为这些演变规则对于“coding”是很有帮助的,这在每篇文章里都由体现吧。
      如果你觉得写法没什么重要的,可以看看上一篇文章。

    7. 老赵
      admin
      链接

      老赵 2009-08-10 09:03:00

      MetaWang:
      老赵辛苦了。这么晚还在写文章。


      一早来发现已经被踩到后面去了,早知道留在一大早发,呵呵。

    8. 老赵
      admin
      链接

      老赵 2009-08-10 09:09:00

      Colin Han:
      呵呵,看完老赵的帖子,才终于明白了面试的时候为什么要问这个问题。
      只不过,当时看到那篇文章,我也一下被问蒙了,想了想,似乎几个版本没有什么大的变化啊。于是就想当然的认为作者在自以为是了。我想很多人嘲讽的人也是出于相同的原因吧。
      谢谢老赵这样刨根问底的人。


      不要轻易觉得别人自以为是啊,说不定别人就留有后着的。

    9. love .net FrameWork
      *.*.*.*
      链接

      love .net FrameWork 2009-08-10 09:24:00

      好文啊

    10. DiggingDeeply
      *.*.*.*
      链接

      DiggingDeeply 2009-08-10 09:26:00

      老赵,你的EvenSquareLambda是故意写错的吗?不是求偶数的平方吗?

    11. GoGoSonny
      *.*.*.*
      链接

      GoGoSonny 2009-08-10 09:46:00

      要用高级的、人性化的东西就得牺牲点性能。

    12. 老赵
      admin
      链接

      老赵 2009-08-10 09:50:00

      DiggingDeeply:老赵,你的EvenSquareLambda是故意写错的吗?不是求偶数的平方吗?


      改好了。

    13. 老赵
      admin
      链接

      老赵 2009-08-10 09:52:00

      GoGoSonny:要用高级的、人性化的东西就得牺牲点性能。


      不过我认为追究这点细枝末节性能的人,反而是属于不太会搞性能的人。
      因为性能优化,高性能程序的关键点把握不了,最多搞搞一些十几毫秒的提高了。
      性能优化从发现问题到解决问题是个系统化的过程,不是随便“看看”的。

    14. Pandora
      *.*.*.*
      链接

      Pandora 2009-08-10 10:08:00

      @Jeffrey Zhao

      Jeffrey Zhao:
      @Pandora
      Java没有delegate。
      从一开始就说了,委托其实都是没有变的,变得只是“写法”。
      既然是“语法糖”,则说明本质都是一样的。至于性能在各版本之间即使有变化,和写法无关。
      我认为这些演变规则对于“coding”是很有帮助的,这在每篇文章里都由体现吧。
      如果你觉得写法没什么重要的,可以看看上一篇文章。


      我没用过JAVA,只是想表达那么个意思 哈哈 见笑。
      恩,我看过了文章,也同意你的说法。但我觉得就delegate这个问题来说,只要明白了怎么用,在什么情况下用就可以了,至于写法实在不重要。这就好像所谓的思路和语法的关系一样。
      比如说有个人就一直按照1.1的写法来写,也不会带来任何问题。有一天他忽然知道了3.0的写法,甚至可以开发一个工具直接把所有的1.1式的deleagate转换为lamba表达式的。我觉得这和个人水平与能力的关系不大。

    15. 老赵
      admin
      链接

      老赵 2009-08-10 10:13:00

      @Pandora
      你如果说2.0和3.0没有问题我可以接受,但是1.1和2.0差太远了。
      我觉得你还是再仔细看一下(上)和(中)这篇文章比较好吧,写法是委托的关键。
      我不相信一个熟悉.NET的人连.NET 2.0里的写法都不了解。

    16. DiggingDeeply
      *.*.*.*
      链接

      DiggingDeeply 2009-08-10 10:13:00

      满口“性能”人太多了。
      今年年头做个项目,组里来个“JS牛人”。在和我们大PM面对面汇报工作的时候(周会上),竟然口出阙词:“用JS做服务器端程序,可以优化性能”。当是我坐这神旁边,我和大PM同时对眼,不明所以,我想笑又不敢笑。大PM估计也有40岁了,也是技术出身,当时挺“谦虚”的说:“这个我好长时间不接触技术了,对新技术不是很了解,现在JS都可以做服务端的啦?”靠,这厮捅了漏子,还浑然不觉,继续滔滔不绝,说了半天,其实他想说的就是AJAX。我当时那个狂汗啊,唉。。。

      其是你问好多满口“性能”的人,追问到到最后他连个P都说不清楚,他的根据要么就是听别人说,要么就是某篇博客,要么就是自己的感觉。

      给大家推荐另外一个笑话,我原来的同事遇到的:
      http://blog.chinaunix.net/u/12783/showart_1795369.html

    17. AlexLiu
      *.*.*.*
      链接

      AlexLiu 2009-08-10 10:13:00

      好文前排要占座。慢慢看。。

    18. 妖居
      *.*.*.*
      链接

      妖居 2009-08-10 10:17:00

      佩服!老赵的文写的越来越好了,读起来酣畅淋漓。最后谈到的source.Count()再循环中的时候,突然发现自己是不是也有这样的问题,于是赶快去检查一下代码。

      现在的语言发展的越来越高级,越来越智能了,智能到我们这些编写代码的人都很难分清他的本质。越来越喜欢MVC了,它开源啊,看得通透……

    19. 老赵
      admin
      链接

      老赵 2009-08-10 10:18:00

      DiggingDeeply:
      给大家推荐另外一个笑话,我原来的同事遇到的:
      http://blog.chinaunix.net/u/12783/showart_1795369.html


      不正面回答问题的人我现在身边就有一个,我们把这种行为叫做“闪躲”,会轻功的。

    20. dk59[未注册用户]
      *.*.*.*
      链接

      dk59[未注册用户] 2009-08-10 10:21:00

      @Jeffrey Zhao
      感觉博客园只有一个老赵MVP比较活跃在撑着那根柱子,其他人好像销声匿迹了!是该批评还是该?

    21. dk59[未注册用户]
      *.*.*.*
      链接

      dk59[未注册用户] 2009-08-10 10:23:00

      还是期望老赵象这类文章一样细细的剖析一下asp.net mvc

    22. 拼命占便宜
      *.*.*.*
      链接

      拼命占便宜 2009-08-10 10:26:00

      你还真有情调,还在灵格斯上面占了个格子,打了广告哟!

    23. 老赵
      admin
      链接

      老赵 2009-08-10 11:11:00

      妖居:
      现在的语言发展的越来越高级,越来越智能了,智能到我们这些编写代码的人都很难分清他的本质。越来越喜欢MVC了,它开源啊,看得通透……


      开源的本质不是看得到源代码,如果说是看得到源代码的话,.NET都看得到的。

    24. yeml[未注册用户]
      *.*.*.*
      链接

      yeml[未注册用户] 2009-08-10 11:15:00

      我不用3.5,所以不知道这篇文章讲什么

    25. yeml[未注册用户]
      *.*.*.*
      链接

      yeml[未注册用户] 2009-08-10 11:16:00

      2.0 的aciont+func +delegate(){};就已经很好用了
      搞个什么拉姆达干嘛呢

    26. 老赵
      admin
      链接

      老赵 2009-08-10 11:17:00

      @yeml
      你的邮箱多少阿?我用NHibernate的问题联系不到你。

    27. 老赵
      admin
      链接

      老赵 2009-08-10 11:19:00

      yeml:
      2.0 的aciont+func +delegate(){};就已经很好用了
      搞个什么拉姆达干嘛呢


      上一篇文章里写的很清楚了啊,“语义”。

    28. yeml[未注册用户]
      *.*.*.*
      链接

      yeml[未注册用户] 2009-08-10 11:21:00

      我的email circulate#163.com

    29. 韦恩卑鄙
      *.*.*.*
      链接

      韦恩卑鄙 2009-08-10 11:43:00

      hehe wrap 就和 stringbuilder.append一样么

    30. 菜鸟毛
      *.*.*.*
      链接

      菜鸟毛 2009-08-10 11:58:00

      老赵,看了你这一系列的文章,感受颇多.于是决定去啃一下微软的开源CMS:Oxite,不知道以您的观点,值不值得我们去啃呢.

    31. 韦恩卑鄙
      *.*.*.*
      链接

      韦恩卑鄙 2009-08-10 12:00:00

      我觉得 循环分离语义上和性能上 最终还是可以达成妥协的

      关键是怎样把
      {if (i % 2 == 0) result.Add(i * i);}
      这段逻辑抽象化注入IEnumerable.foreach 或者IEnumerable .Aggregate, 如果使用得当 其实不是不可以双赢吧


      另外 关于
      static List<int> EvenSquareFast(IEnumerable<int> source)

      既然已经形成了这样有意义的闭包 内部的实现可读性很重要么?
      我不这么认为哦。

      我的态度是
      在保证主干语义清晰的前提下 鼓
      励用不太清晰高性能的写法实现自己的扩展

    32. 老赵
      admin
      链接

      老赵 2009-08-10 12:02:00

      韦恩卑鄙:hehe wrap 就和 stringbuilder.append一样么


      好像不一样。

    33. 老赵
      admin
      链接

      老赵 2009-08-10 12:04:00

      @韦恩卑鄙
      这个自己权衡吧。
      如果你自己已经使用很好的方法名来封装一段逻辑,那么对外部来说,内部是否清晰可能不太重要。
      对我来说,我倾向于清晰和省事。

    34. 老赵
      admin
      链接

      老赵 2009-08-10 12:05:00

      菜鸟毛:老赵,看了你这一系列的文章,感受颇多.于是决定去啃一下微软的开源CMS:Oxite,不知道以您的观点,值不值得我们去啃呢.


      我没啃过,不好发表意见。

    35. 韦恩卑鄙
      *.*.*.*
      链接

      韦恩卑鄙 2009-08-10 12:13:00

      Jeffrey Zhao:
      @韦恩卑鄙
      这个自己权衡吧。
      如果你自己已经使用很好的方法名来封装一段逻辑,那么对外部来说,内部是否清晰可能不太重要。
      对我来说,我倾向于清晰和省事。


              static List<int> EvenSquareLambda(IEnumerable<int> source)
              {
                  var list=new List<int>();
                  return source.Aggregate<int, List<int>>
                  (
                      list,
                      (l, i) =>
                      {
                          if(i % 2 == 0)
                          l.Add(i*i);
                          return l;
                      }
                  );
              
              }
      


      这段代码表达了我的倾向

    36. 韦恩卑鄙
      *.*.*.*
      链接

      韦恩卑鄙 2009-08-10 12:22:00

      补充:绝对不是延迟自动合并不好 有时候我怕自己也犯糊涂 搞出一些类似重复调用的妖蛾子来。。。

    37. 老赵
      admin
      链接

      老赵 2009-08-10 12:29:00

      @韦恩卑鄙
      如果只是这段,那么我情愿自己写foreach而不是aggregate了。
      如果使用这种函数式编程的风格,我会刻意避免side effect。
      就比如你现在的代码,我觉得反而不清楚,一个重要原因就是出现了side effect。

    38. 韦恩卑鄙
      *.*.*.*
      链接

      韦恩卑鄙 2009-08-10 12:36:00

      -0-
      好像是哦~
      但是没看出来自己写的哪里有side effect了 -0- 老赵帮我指出下来
      // - -/// code review 是我们team最害羞的时刻

    39. vczh[未注册用户]
      *.*.*.*
      链接

      vczh[未注册用户] 2009-08-10 12:40:00

      @韦恩卑鄙
      习惯了就不糊涂了……反正接口就那样子,当一件事情只有一种做法的时候,无需做额外的研究。

    40. 老赵
      admin
      链接

      老赵 2009-08-10 12:50:00

      @韦恩卑鄙
      List<T>.Add就是side effect咯。

    41. 韦恩卑鄙
      *.*.*.*
      链接

      韦恩卑鄙 2009-08-10 12:57:00

      Jeffrey Zhao:
      @韦恩卑鄙
      List<T>.Add就是side effect咯。


      一惊 -0- 果然

    42. 温景良(Jason)
      *.*.*.*
      链接

      温景良(Jason) 2009-08-10 13:02:00

      up

    43. 韦恩卑鄙
      *.*.*.*
      链接

      韦恩卑鄙 2009-08-10 13:16:00

      再三确认了下

      看起来side effect 不大

      public List(IEnumerable<T> collection)
      {
      if (collection == null)
      {
      ThrowHelper.ThrowArgumentNullException(ExceptionArgument.collection);
      }
      ICollection<T> is2 = collection as ICollection<T>;
      if (is2 != null)
      {
      int count = is2.Count;
      this._items = new T[count];
      is2.CopyTo(this._items, 0);
      this._size = count;
      }
      else
      {
      this._size = 0;
      this._items = new T[4];
      using (IEnumerator<T> enumerator = collection.GetEnumerator())
      {
      while (enumerator.MoveNext())
      {
      this.Add(enumerator.Current);
      }
      }
      }
      }

      因为参数几乎不可能是 collection<T>
      所以最后也只能this.Add
      -0-

      不过这太乱了 水太深了 我不玩了




    44. 老赵
      admin
      链接

      老赵 2009-08-10 13:19:00

      @韦恩卑鄙
      遇到你的需求,我就会直接用foreach:
      foreach (var i in source) { ... }

      或者用我自己的扩展:
      source.ForEach(i => { ... });

      甚至,因为有些时候需要用到下标,但是嫌for麻烦,因此:
      source.ForEach((item, index) => { ... });

      都是比较方便的做法。

    45. aaabbbcccdddeeefff[未注册用户…
      *.*.*.*
      链接

      aaabbbcccdddeeefff[未注册用户] 2009-08-10 13:19:00

      什么是side effect?

    46. 老赵
      admin
      链接

      老赵 2009-08-10 13:21:00

      @韦恩卑鄙
      这和大不大无关啊,side effect就是side effect,我不知道啥叫大啥叫小的,呵呵。
      如果是函数式编程的话,它用的List是不同于List<T>的这种,是一种immutable的linked list结构。

    47. 老赵
      admin
      链接

      老赵 2009-08-10 13:25:00

      aaabbbcccdddeeefff:什么是side effect?


      副作用,通俗地理解就是,一个函数会不会因为外部状态而导致运行结果不同,或者会不会影响外部状态。
      这种影响就是side effect,函数式编程之所以叫做“函数”式,就是因为函数f,属于参数x输出就是y,不会改变外部,不会受外部影响。
      如i => i + 1就是no side effect,如i => Console.WriteLine(i)就是有side effect的。

    48. 韦恩卑鄙
      *.*.*.*
      链接

      韦恩卑鄙 2009-08-10 13:29:00

      可是 reflector 不是这么说的 - -b



      public static List<TSource> ToList<TSource>(this IEnumerable<TSource> source)
      {
      if (source == null)
      {
      throw Error.ArgumentNull("source");
      }
      return new List<TSource>(source);
      }



      难道c#编译器又选择性修改了部分代码 —*(·……#—*()!

    49. 麒麟.NET
      *.*.*.*
      链接

      麒麟.NET 2009-08-10 13:30:00

      性能和可读性两者我认为必须是可读性更重要,因为我们的代码不仅仅是写给机器看的,也是写给人看的。
      经常看到园子里“XXX问题最优算法解,比XX的快XXX倍”这样的文章,打开看代码,一脑子卤煮,根本没法读。这样的程序运行再快,又有啥用呢?需求变更时怎么维护?

    50. 韦恩卑鄙
      *.*.*.*
      链接

      韦恩卑鄙 2009-08-10 13:35:00

      Jeffrey Zhao:
      @韦恩卑鄙
      遇到你的需求,我就会直接用foreach:
      foreach (var i in source) { ... }

      或者用我自己的扩展:
      source.ForEach(i => { ... });

      甚至,因为有些时候需要用到下标,但是嫌for麻烦,因此:
      source.ForEach((item, index) => { ... });

      都是比较方便的做法。



      对最后一个扩展比较感兴趣

      老赵贴出来我学习下~~ 嘿嘿

    51. 老赵
      admin
      链接

      老赵 2009-08-10 13:37:00

      @韦恩卑鄙
      这你写不出来也太说不过去了,我不信。

    52. 老赵
      admin
      链接

      老赵 2009-08-10 13:39:00

      @麒麟.NET
      首先可能是你说的原因,其次就是,性能不是这点简单的代码级别变化就能提高的。它占多少分量等因素都是需要考虑的。

    53. 麒麟.NET
      *.*.*.*
      链接

      麒麟.NET 2009-08-10 13:45:00

      Jeffrey Zhao:
      @麒麟.NET
      首先可能是你说的原因,其次就是,性能不是这点简单的代码级别变化就能提高的。它占多少分量等因素都是需要考虑的。


      是的,所以觉得写这类文章的兄弟们都陷入了一个误区,在自己跟自己较劲,所谓的“快XX倍”其实并没有从根本上解决问题,这点“性能”上的提升根本可以忽略不计。

    54. 韦恩卑鄙
      *.*.*.*
      链接

      韦恩卑鄙 2009-08-10 13:59:00

      不全是 起码写游戏的时候。。:P

    55. PXO[未注册用户]
      *.*.*.*
      链接

      PXO[未注册用户] 2009-08-10 14:06:00

      c#+.net谈不上什么性能,所谓的性能提高就是让内存增少一点,它没有真正意义上的性能可谈....总之Net+C#就是性能差,速度要靠硬件来提升
      个人观点

    56. 老赵
      admin
      链接

      老赵 2009-08-10 14:15:00

      @PXO
      是种语言平台就会有高效的或低效的写法。例如数据结构算法本身都是和效率有关,而且和平台无关的东西。
      所以,.net程序不会只能靠硬件来提升性能,你最多说,用一部分性能换取开发的便利,这种说法我是赞同的。
      而且,.net程序性能也和C等程序一样,与很多硬件相关,只是很多人忽视了。比如你看这里:计算机体系结构与程序性能

    57. 路人丁[未注册用户]
      *.*.*.*
      链接

      路人丁[未注册用户] 2009-08-10 14:32:00

      为什么我在C# 2008 express下面测试lambda和normal的版本基本差不多,有时是lambda略高一些?

      Normal
      Time Elapsed: 6,081ms
      CPU Time: 6,078,125,000ns
      Gen 0: 2499
      Gen 1: 0
      Gen 2: 0

      Fast
      Time Elapsed: 4,472ms
      CPU Time: 4,484,375,000ns
      Gen 0: 1249
      Gen 1: 0
      Gen 2: 0

      Lambda
      Time Elapsed: 6,117ms
      CPU Time: 6,125,000,000ns
      Gen 0: 1249
      Gen 1: 0
      Gen 2: 0

      操作系统: windows xp sp2

    58. 老赵
      admin
      链接

      老赵 2009-08-10 14:37:00

      @路人丁
      我发现Normal和Fast的差距也变小了,所以我猜你是不是没有用release build?

    59. 信老赵得永生[未注册用户]
      *.*.*.*
      链接

      信老赵得永生[未注册用户] 2009-08-10 15:39:00

      信老赵得永生

    60. Kevin Dai
      *.*.*.*
      链接

      Kevin Dai 2009-08-10 16:18:00

      IEnumerable<int> source = ...;
      for (var i = 0; i < source.Count(); i++)
      { 
          ...
      }
      
      

      以上例子的观点可以在《Comouter Systems,A Programmer's Perspective》一书找到。我觉得在写程序时,不管什么语言什么平台,保持意识是很重要的。
      这个有关委托的系列非常不错,.NET开发人员应该delegate的有关知识和歌版本中的演变过程,不然真的是民工了。

    61. Michael Tao
      *.*.*.*
      链接

      Michael Tao 2009-08-10 16:24:00

      发现老赵写文章的速度还不是一般的快啊

    62. 韦恩卑鄙
      *.*.*.*
      链接

      韦恩卑鄙 2009-08-10 17:26:00

      他是一气写好分三节放的 oh yeah

    63. 麒麟.NET
      *.*.*.*
      链接

      麒麟.NET 2009-08-10 17:32:00

      Jeffrey Zhao:
      如i => i + 1就是no side effect,如i => Console.WriteLine(i)就是有side effect的。



      Console.WriteLine(i)为什么有副作用呢?

    64. 老赵
      admin
      链接

      老赵 2009-08-10 18:28:00

      麒麟.NET:
      Console.WriteLine(i)为什么有副作用呢?


      它向屏幕输出内容了,也就是改变了外界。

    65. Mainz
      *.*.*.*
      链接

      Mainz 2009-08-10 19:46:00

      最后一句没有考证,建议楼主用c的函数指针实现一遍,然后比较下性能

    66. 腻革命先[未注册用户]
      *.*.*.*
      链接

      腻革命先[未注册用户] 2009-08-10 20:03:00

      上中下全文看毕,也看了始作俑的迈克尔的原文。想了想,恩,事情应该是这样的。先是迈克尔对其“面人不淑”的遭遇在自己博客里发了个小牢骚,其原因可能是那些小c不但无知,而且拿着无知当个性还敢顶嘴,弄的迈克尔下不来台。 谁成想博文发出后,不但没博得同情,还招了一番奚落。紧跟着,老赵连发三文洋洋万言予以力挺,发了个比迈克尔水平更高、知识面更广、条理性更强的系列牢骚,真是不辱迈克尔奉为大牛的美意。老赵岂止大牛简直是牛魔王,三文下来几乎听不到什么反对的声音了。

      而这三文以delegate为切人点,从几个角度说明一个问题,C#从1.x到3.5是进化了,越来越牛了。想想也是,你看那扩展方法,充分顺应了坊间从业者的思维方式,只要你够牛逼你可以一直“点”下去。再看那泛型,有了它ok了,类型不重要了,准确的说在书写逻辑结构的时候不重要了,尤其是对数据集合的操作逻辑。哦,还有匿名方法还有拉姆达啊,拉姆达挺乱的什么ELE,SLE的。反正,有了这些就有了老赵第一文里的那美妙的、优雅的、具备良好可读性的、性能上还相当不错的新的delegate的写法。

      但是,这只说明了C#3.5比1.0牛逼,并没有说明必须学习C#1.0 。这和说C比C#牛逼的人心态是一样的,无非怀旧的程度不同而已。再有,Delegate值不值得一提和知否应该知道Delegate的三种写法(未来可能回顾起来可能会更多),并无因果关系。

      迈克尔君,招聘人才的过程是一个发现优点的过程,而不是一个挑毛病的过程,毕竟你要选人不是宰人。如果老赵去面试,你也未必会要他。

      老赵君嘛,为了文中不知哪句不顺耳的话,你居然能搞出这么多不知所以的东西,除了没证明为啥要学习delegate的三种写法,讲了很多有趣的事,好文。

      BTW:“delegate在.net framework1.1,2.0,3.5各可以怎么写?" 我觉得这是一个巨二无比的问题,基本上起不到任何考核作用,鉴定完毕欢迎讨论。

    67. 老赵
      admin
      链接

      老赵 2009-08-10 20:15:00

      Mainz:最后一句没有考证,建议楼主用c的函数指针实现一遍,然后比较下性能


      最后一句话的意思你理解错了,这里没有比较C的函数指针和.NET中委托的性能。

    68. 老赵
      admin
      链接

      老赵 2009-08-10 20:28:00

      @腻革命先
      我不知道Michal Tao是咋问的有没有后手,不过我问这样的问题的时候,后面会跟着一大堆东西的(基本上就是文章里的这些)。
      因为演变都是有原因的,如果你只知道.net 3.5的写法也不算有问题(虽然我也觉得,你不可能只学.net 3.5),但是知道演变规律那自然更好(而且我倒觉得我对于为什么要了解演变过程讲的很清楚了)。
      但是,如果说自己熟悉.net 3.5但是却不知道这些个东西,实在说不过去。当然我也不会把delegate作为面试的唯一依据,但是我还是认为这是个不错的问题——尤其对于那些“从1.x一路走来”的高手们……

    69. 路人丁[未注册用户]
      *.*.*.*
      链接

      路人丁[未注册用户] 2009-08-10 22:31:00

      @Jeffrey Zhao

      Jeffrey Zhao:
      @路人丁
      我发现Normal和Fast的差距也变小了,所以我猜你是不是没有用release build?



      确实是这个问题 我直接ctrl+f5运行了 去release下目录找出来跑了一下
      这次和你的分析比较接近了 谢谢

    70. Colin Han
      *.*.*.*
      链接

      Colin Han 2009-08-10 23:35:00

      Jeffrey Zhao:
      @韦恩卑鄙
      List<T>.Add就是side effect咯。


      所以没有Side Effect的写法应该是下面了咯

      static List<int> EvenSquareLambda(IEnumerable<int> source)
      {
          return source.Aggregate<int, IEnumerable<int>>
          (
              new int[0],
              (l, i) =>
              {
                  if(i % 2 == 0)
                  l = new List<int>(l);
                  l.Add(i*i);
                  return l;
              }
          );
      
      }
      


      不知道我的Side Efftive理解的对不对?

    71. flyingchen
      *.*.*.*
      链接

      flyingchen 2009-08-10 23:47:00

      我也看到过某些朋友编写类似的代码,如在for循环中不断进行str.Split(',').Length
      ----------
      这样的代码将被长期保留,以示后人

    72. sgzwiz
      *.*.*.*
      链接

      sgzwiz 2009-08-11 01:16:00

      从msil角度来看,delegate基本没变化,所变的也就是语法多了点,更精炼了点,许多工作由编译器来完成了。
      面试是考察应聘者会什么,而不是考察应聘者不会什么。会的东西能不能胜任工作,不会的能不能很快变会。
      面试时想让赵mvp得零分,很简单。但面试官能不能因此而否定赵拿过mvp的事实?说某人孔乙己,原因在此。

    73. 老赵
      admin
      链接

      老赵 2009-08-11 09:39:00

      @sgzwiz
      你说这句话的前提还是“问delegate演变规律”是“在考察应聘者不会什么”。我的文章里一直说的是,演变规律很能反映一个是否思考,是否认真对待技术,而且“孔乙己”式问题的后着,应聘者是否可以正确应对?
      其实就是看待一个技术细节是否可以反映某些事情,我认为可以(但不会把它作为唯一依据),你认为不行,仅此而已。

      至于你说应聘者会什么,这个有待商榷。我不会关注应聘者会什么,我只会关注对方是否会我需要的东西。
      例如,你是Java高手,但我用C#,除非你证明学习能力很强,且“公用”的东西很能令人满意,否则,我要知道你Java水平有多高做什么?
      我知道对方不会C#就够了,至于是否接受对方,再看看他是否有学习能力等等。如果他连学习能力都不足,我是否还要考察对方的“社交能力”(不是说,要考察对方会什么吗)?这有必要吗?
      所以就是A、B、C几点能力列出之后一次考察,而现在的“delegate演变规律”,就是我考察其中某项能力的方式之一。

    74. 腻革命先[未注册用户]
      *.*.*.*
      链接

      腻革命先[未注册用户] 2009-08-11 10:05:00

      @Jeffrey Zhao
      有个古老的理念叫以人为本,任何工具的进化都潜移默化的遵循着这个原则。你我有幸经历了1.x的时代,所以我们对那复杂而抽象的delegate的声明和使用充满了爱恨交织的情感(我想老赵也不是娘胎里就对delegate有透彻的认识吧)。

      我没考证过delegate什么时候出现的,但是这种“代函数”的东西和代数a,亦或变量a一样充满了魅力,但使用起来却非常的矫情。也许有人觉得这才是senior,但我觉得这是扯淡。而3.5通过一系列的进化,集合了最常用的Action,Func..等委托类型,又在应用时起上predicate(谓语)这样的名字,对与English speaker来说这就跟他妈说话一样简单了,主谓宾都有了,主.(谓,宾); 谓语的描述又可以通过拉姆达来在使用的时候直接键入,使得逻辑代码块完整而清晰,省去了眼花缭乱的Go to definition,让老delegate去下地狱吧!

      跑题了。关于争论那道题是否值得考意义不大,总有适合这道题的场景。但老赵作为博客园牛魔王,应该指引小牛犊们的奔跑方向使他们跑的更有效率,能谋得高职养家糊口。而不是跑这里老生常谈这些,人家什么都懂了还跑这里看你的博客干嘛啊? 注:这三文内容丰富确实是好文(不是拍牛屁而是说我没否定这些),但是文章始末的一些导向和酸气实在有失大家风范。

      此文是一篇良好的课外读物,但是老赵如能写一篇《何为委托、为何委托、何时委托、如何委托》来饲我等众小牛犊,我等将翘首以盼。(当然如果已有,还请不吝贴个链接),就到这里不会再写了byebye!

    75. 老赵
      admin
      链接

      老赵 2009-08-11 10:17:00

      @腻革命先
      嗯,你说的有一些道理……呵呵,多谢指正,希望常来玩。

    76. DiggingDeeply
      *.*.*.*
      链接

      DiggingDeeply 2009-08-11 14:02:00

      @腻革命先
      我觉得是好多人不知道怎么去反对,或者根本没想过去反对。

    77. 老赵
      admin
      链接

      老赵 2009-08-11 15:33:00

      @DiggingDeeply
      希望不是第二个原因,估计也不是,看平时的文章也不缺来吵架的人。
      可能是第一个原因,因为“反对”和写一篇文章去证明自己观点很大程度上是一致的,要花心思。

    78. RednaxelaFX
      *.*.*.*
      链接

      RednaxelaFX 2009-08-12 02:16:00

      下面讨论没有特别说明的话都以32位x86上的CLR为背景。

      @Jeffrey Zhao
      刚读完这篇,机器正在卡,懒得在实机上测代码。但老赵留了个陷阱在文中啊:
      >> 而GC方面,“扩展方法”实现也优于“分解循环”(您同意这个看法吗?)。

      我的话会觉得“扩展方法”比“分解循环”对GC构成了更大压力。
      如果文中给出的数据能反映比较稳定的状况的话,比较数据:
      GenN Normal Lambda
      Gen0 624 312
      Gen1 3 156
      Gen2 0 0

      对CLR这种分代式GC来说,GenN的N越大,其中对象已经存活的时间就越长。而在较年老的代中的对象总是从较年轻的代升上来的(LOH(大对象堆)例外,它概念上并不属于Gen2,但与Gen2一样都是在full collection时才收集)。也就是说在不太大(几十K以下)的对象一开始总是在Gen0,然后Gen0的对象如果活得够久会提升到Gen1,再活得够久会提升到Gen2。
      CLR的GC的基本思路是通过“引用跟踪”来判断对象是否存活。应该“活”着的对象越多,发现它们就越耗时间。或者用术语说,标记用的时间与活对象的个数成正比。Gen1的对象都是从Gen0来的,如果数量多的话意味着Gen0收集时在标记阶段消耗的时间也多。假设Gen0的GC算法是mark-and-compact,那么在Gen0收集时压缩阶段消耗的时间与Gen0活对象的总大小成正比。
      Gen0和Gen1收集都很快,但当然,Gen0收集更频繁,因而也被设计得更高效。CLR在.NET Framework 2.0 SP2(也就是3.5 SP1中包含的版本)中每段GC堆有16MB,其中多少用于Gen0、Gen1和Gen2是会在运行时调整的;Gen0的初始大小决定于CPU的cache大小,目标是尽量让Gen0能更多的放在L1和L2缓存中,提高内存访问的效率;相对来说Gen1收集时会遇到更多缓存缺失,内存访问方面的效率就会低些。
      上面Normal和Lambda的数据中可以看到,Lambda版的Gen1收集次数比Normal版多很多,意味着前者中存在许多生命不短不长的对象,而后者中的对象多数都是非常短命的。即便非常短命的对象非常多,也不会对GC构成很大压力——它们在Gen0收集的时候就已经“死”了,标记和压缩都跟它们没关系,所以不会转换为很多的Gen1收集。很明显,Gen1次数比较多比Gen0次数比较多要吃亏些。

      那么多生命不短不长的对象是哪儿来的呢?

      定性来说,分解循环中临时对象(也就是那些List<T>)“活”着的时间比较短,只在分解后的循环的两个“步骤”中(生产数据和使用数据的两个步骤);这样,在Gen0收集的时候就能解决掉比较多的临时对象,非常快。
      Enumerable类提供的扩展方法很多都含有yield语句,也就是说它们不是普通“方法”,而是实例化由编译器生成的IEnumerable<T>实现的语法糖。使用generator会实例化很多IEnumerable<T>的实例,而正如老赵所说,LINQ的这种用IEnumerable<T>的方法可以算是decorator模式,串在一起的这组IEnumerable<T>需要在整个运算过程中都“活”着,使很多对象的寿命撑过了一次Gen0收集,变成了Gen1对象。使用lambda时,背后是由编译器生成的方法,和运行时实例化的Delegate的派生类的实例;这些实例也跟前面说的由generator生成的IEnumerable<T>实现类的实例一样,要在运算过程中一直“活”着,增加了对GC的压力。
      两者的差异在分解步骤比较多的时候应该会变得明显起来吧,我没测试无法确定,只是定性推测。老赵文中给出的数据似乎印证了我的猜测,但又似乎是别的因素导致Gen1收集次数比较多——文中的测试代码分的步骤就两步,在Normal中的evenList其实也得在整个运算过程中“活”着。

      这个可以留着以后好好分析一下。

      其实运行时间还受许多小的因素被多次重复后放大的影响。例如说Normal版里方法调用的开销较少,而Lambda版里方法调用的开销较多。本来CLR会内联一些方法调用,但在当前版本里,有复杂控制流的方法不会被内联,通过委托调用的方法也不会被内联,这些开销虽然很小,重复10000次之后会稍微显现出来。
      然后在构造那一串IEnumerable<T>的时候,它们背后的类里会有IEnumerable<T>类型的成员域,给这些域赋值会涉及到“给引用类型的引用类型成员域赋值”的状况。CLR的分代式GC,使得GC必须跟踪从较年老代到较年轻代的引用;实现这种跟踪的方法是遇到“给引用类型的引用类型成员域赋值”的时候,并不是直接把指针写到一个域里,而是调用“写屏障”(write barrier),判断这里的引用赋值是否涉及较年老到较年轻代的引用,并根据情况更新一个记录表。对数组的写也有类似的问题。Normal版代码涉及大量对数组的写操作(毕竟List<T>背后是T[]),而Lambda版则涉及不少“写屏障”调用。
      这些小地方都会干扰到测试结果,所以如何解读某个小测试的意义挺困难的……

      当然,为了代码简洁,能用LINQ的地方我很少会吝啬的。小地方的性能问题可以回头慢慢解决,大致思路上的性能问题才值得预先敲定。

    79. 青林一霸
      *.*.*.*
      链接

      青林一霸 2009-08-12 08:29:00

      老赵:看了你的About,不去青鸟培训,不知如果需要时能否聘请你为我们的顾问?我们这里几个人都没专门学过编程,但可能会有一个比较大的项目要做,我正在推动。

    80. 老赵
      admin
      链接

      老赵 2009-08-12 09:00:00

      @青林一霸
      “聘用”、“顾问”有些过了,来问一些问题肯定没有问题的。

    81. 老赵
      admin
      链接

      老赵 2009-08-12 09:03:00

      @RednaxelaFX
      赞,我的观点基本和你相同,例如扩展方法“保存”对象的时间相对更长一些,所以Gen1会多。
      // 你就不能把这么大段东西作为博客发表嘛,写成评论实在是太浪费了。

    82. RednaxelaFX
      *.*.*.*
      链接

      RednaxelaFX 2009-08-12 09:48:00

      @Jeffrey Zhao
      嗯,我想发的,但这里面定性的东西太多,有说服力的数据不足,我想先找到能支撑或者否定前面的猜测的论据再发博客。而且关于CLR GC的一些内容那个都是从我博客的草稿箱里抽出来的,只是我要把东西写成文的速度远远比不上老赵,迟迟见不了光 T T
      老赵到底是如何那么快把字码出来的,求经验~

      之前回老家一周,一直没上网,昨天刚回来。堆积了一周的feed和email,花了一下午一晚上才大致掠过一遍,实在是还没缓过劲来……

    83. 钢盅郭子
      *.*.*.*
      链接

      钢盅郭子 2009-08-12 09:48:00

      @腻革命先

      举一反三,温故知新,这是学者的美德
      厚积薄发,深入浅出,这是学者的境界

      题目只是抛砖引玉,过分计较就会钻牛角尖啦

    84. 钢盅郭子
      *.*.*.*
      链接

      钢盅郭子 2009-08-12 09:54:00

      单纯满足于“掌握delegate的三种写法”,这是形而上的
      能够“由delegate写法的演变深入理解C#进化背景和意义”,这是在马列主义***思想邓小平有中国特色社会主义理论***三个代表指引下用科学发展观建设和谐社会的理论素养
      (屏蔽词很有代表性嘛)

    85. DiggingDeeply
      *.*.*.*
      链接

      DiggingDeeply 2009-08-12 10:36:00

      @Jeffrey Zhao
      所以说啊,反驳别人得有充分的证据,而这证据得花很大的精力来获得,或者根本无法获得。
      @RednaxelaFX
      对这位仁兄的严谨态度表示支持。

    86. 韦恩卑鄙
      *.*.*.*
      链接

      韦恩卑鄙 2009-08-12 13:00:00

      楼上 不光是证据 对老赵的观点
      你不花个全天来分析他的逻辑 建立自己的逻辑 是很容易被这厮把有理说成没理的 (形容词 不是说老赵没理)

    87. 韦恩卑鄙
      *.*.*.*
      链接

      韦恩卑鄙 2009-08-12 13:06:00

      仔细学习了RednaxelaFX的回复内容 受益匪浅阿

    88. 老赵
      admin
      链接

      老赵 2009-08-12 14:08:00

      @RednaxelaFX
      GC这方面我一直也只能靠“猜”,没法验证。GC和上下文环境也都太有关系了,而Cache Miss这块都不太容易评测。
      所以,在实际项目中我一般只关注Gen2的回收,Gen1和Gen0从一些资料上来看,都不太会成为问题。

    89. 老赵
      admin
      链接

      老赵 2009-08-12 14:12:00

      @RednaxelaFX
      说到码字,的确很累……话说,平时有想法就录下来,然后找个小蜜帮忙总结码字大家以为如何?

    90. RednaxelaFX
      *.*.*.*
      链接

      RednaxelaFX 2009-08-12 15:05:00

      @Jeffrey Zhao
      嗯……CLR的GC设计太精妙了,不在那个开发组里恐怕是没啥机会能看得出个究竟。在最底下实现了一大堆GC算法,然后上面有一个policy engine来管理到底在哪一代使用什么算法,使用什么参数,等等。CLR GC会监视程序运行中GC的收益状况来调整算法和参数(参数包括收集频率,堆的大小等)。这玩儿是动态的,所以要是以为CLR GC总是有固定行为的话,就很难理解实际状况。我得说这些细节我还不够了解……

      Gen0和Gen1收集都非常快,Gen0一般在1ms以下,Gen1也就是几ms的样子,因为这两代相对来说都很小。Gen2收集才可能导致长的pause time。所以能保持较少的full collection当然是好事,性能上不会有太多影响。

    91. RednaxelaFX
      *.*.*.*
      链接

      RednaxelaFX 2009-08-12 15:07:00

      @Jeffrey Zhao
      平时我有想法或者读到了什么有趣的都会记在本子上或者扔草稿箱,但哪儿来的能把那些稻草堆里的针找出来的小蜜……T T

    92. 老赵
      admin
      链接

      老赵 2009-08-12 18:09:00

      @RednaxelaFX
      记在在线的notes里然后共享给我看看吧……

    93. RednaxelaFX
      *.*.*.*
      链接

      RednaxelaFX 2009-08-12 19:27:00

      @Jeffrey Zhao
      老赵有什么在线note的站推荐?关键是要有访问控制的。园子有这样的功能么?

    94. 老赵
      admin
      链接

      老赵 2009-08-12 20:39:00

      @RednaxelaFX
      最简单的其实就是搞个google docs。

    95. RednaxelaFX
      *.*.*.*
      链接

      RednaxelaFX 2009-08-12 20:41:00

      @Jeffrey Zhao
      啊,Google Docs我有帐号。回头把notes转过去……

    96. timeandtimeagain[未注册用户…
      *.*.*.*
      链接

      timeandtimeagain[未注册用户] 2009-08-12 21:57:00

      @Jeffrey Zhao
      老赵的文章写的好,但老赵的语言表达能力实在不是很好,讲座讲得实在不生动。呵呵,内秀

    97. 老赵
      admin
      链接

      老赵 2009-08-12 22:11:00

      @timeandtimeagain
      呀?我为什么觉得我的语言表达能力不错,能够把问题讲的很清楚的,而且富有激情,呵呵。
      不过讲和听到的确可能不一样,下次有机会去听听我自己的讲座。

    98. 冷傲残痕
      *.*.*.*
      链接

      冷傲残痕 2009-08-19 09:01:00

      三个篇文章 让人学习了 使用Lambda表达式确实不错 语义简单明了

    99. 好俊的功夫啊
      *.*.*.*
      链接

      好俊的功夫啊 2010-01-03 19:32:00

      受教了,学习了很多

    100. 链接

      cwzyj157 2011-12-18 10:36:45

      工作两年多觉得自己没什么长进.公司产品目前还是基于.Net2.0的,对一些.Net2.0之后的一些语言特性都不是很了解.最近在看clr via c#(third edition).以前看过很多您的博客,老赵还是非常提倡写博客!这样可以把自己知道的或学习的东西很有条理的记录下来并加深理解.最近一直想写一篇关于delegate基础的!经过了长时间的思想斗争,终于决定一定要写出来!不管写得好还是不好!有了开始我相信以后就不会有如此强的心理抵抗!在写之前先看下园子里面都是怎么写的,原后就学习了老赵的这个系列,讲得非常的好.只是时间差太久了!都超过2年了!想想2年前才毕业参加工作,才开始接触这门语言C#,最开始是读C井,想想挺有喜感.不知道2年后的阅读晚不晚?

    101. 流光小弟
      223.68.162.*
      链接

      流光小弟 2014-04-26 15:05:25

      在以前的文章里提问怕前辈看不到,所以在这里问了:@Html.Input(k => k.Alb_Name).Validate(k => k.Request().LengthRange(1, 12)),这样一个表单的验证设计,它有JS的验证,也有后台验证,问题是:网站在服务器开启了多进程的访问的时候,会有问题吗?(委托链去实现一个东西,会不会发生类似在多个线程共享Session的问题)

    发表回复

    登录 / 登录并记住我 ,登陆后便可删除或修改已发表的评论 (请注意保留评论内容)

    昵称:(必填)

    邮箱:(必填,仅用于Gavatar

    主页:(可选)

    评论内容(大于5个字符):

    1. Your Name yyyy-MM-dd HH:mm:ss

    使用Live Messenger联系我