代码之家  ›  专栏  ›  技术社区  ›  Şafak Gür

为什么不缓存使用lambda表达式初始化的非捕获表达式树?

  •  16
  • Şafak Gür  · 技术社区  · 6 年前

    考虑以下类别:

    class Program
    {
        static void Test()
        {
            TestDelegate<string, int>(s => s.Length);
    
            TestExpressionTree<string, int>(s => s.Length);
        }
    
        static void TestDelegate<T1, T2>(Func<T1, T2> del) { /*...*/ }
    
        static void TestExpressionTree<T1, T2>(Expression<Func<T1, T2>> exp) { /*...*/ }
    }
    

    轻微地 可读性较差):

    class Program
    {
        static void Test()
        {
            // The delegate call:
            TestDelegate(Cache.Func ?? (Cache.Func = Cache.Instance.FuncImpl));
    
            // The expression call:
            var paramExp = Expression.Parameter(typeof(string), "s");
            var propExp = Expression.Property(paramExp, "Length");
            var lambdaExp = Expression.Lambda<Func<string, int>>(propExp, paramExp);
            TestExpressionTree(lambdaExp);
        }
    
        static void TestDelegate<T1, T2>(Func<T1, T2> del) { /*...*/ }
    
        static void TestExpressionTree<T1, T2>(Expression<Func<T1, T2>> exp) { /*...*/ }
    
        sealed class Cache
        {
            public static readonly Cache Instance = new Cache();
    
            public static Func<string, int> Func;
    
            internal int FuncImpl(string s) => s.Length;
        }
    }
    

    Test 电话。

    打电话。

    如果它不捕获任何内容,并且表达式树是不可变的,那么缓存表达式树也会有什么问题?

    编辑

    1. 由编译器创建)。
    2. 在一个代码库中只能有这么多的表达式树——同样,我说的是可以缓存的表达式树,即使用lambda表达式初始化的表达式(不是手动创建的表达式),而不捕获任何外部状态/变量。字符串文本的自动内联就是一个类似的例子。
    3. 它们注定要被穿越-它们 可以 被编译以创建委托,但这不是它们的主要功能。如果有人想要一个已编译的委托,他们可以只接受一个(a) Func<T> ,而不是 Expression<Func<T>>

    我要问的是缓存这些表达式树的潜在缺点。svick提到的内存需求是一个更可能的例子。

    2 回复  |  直到 6 年前
        1
  •  9
  •   Eric Lippert    6 年前

    为什么不缓存使用lambda表达式初始化的非捕获表达式树?

    我在编译器中编写了这些代码,包括最初的C#3实现和Roslyn重写。

    当被问到“为什么不”的问题时,我总是这么说: 编译器编写器不是 必修的 提供他们这样做的原因 做点什么 . 做某事需要工作,需要努力,而且要花钱。因此,默认位置始终为 当工作不必要时做某事。

    相反,想要完成工作的人需要证明为什么这项工作是值得的 成本。事实上,这个要求比这更强烈。想要完成工作的人必须证明为什么不必要的工作是必要的 一个更好的方式来花费时间,精力和金钱比任何其他可能使用开发人员的时间 . 实际上有无数种方法可以提高编译器的性能、特性集、健壮性、可用性等等。是什么让这个如此伟大?

    现在,每当我给出这样的解释时,我都会说“微软很有钱,诸如此类”。拥有大量资源并不等于拥有无限的资源,而且编译器已经非常昂贵。我也有人反驳说“开源使劳动力免费”,但它绝对没有。

    我注意到时间是一个因素。进一步扩展这一点可能会有所帮助。

    在开发C#3.0时,VisualStudio有一个特定的日期,它将“发布到制造业”,这是一个奇怪的术语,从那时起,软件主要在CD上分发,一旦打印出来就不能更改。这个日期不是任意的;相反,它后面有一个完整的依赖链。比如说,如果SQL Server有一个依赖于LINQ的特性,那么将VS发布推迟到当年的SQL Server发布之后是没有任何意义的,因此VS调度会影响SQL Server调度,进而影响其他团队的调度,依此类推。

    因此,VS组织中的每个团队都提交了一个时间表,在该时间表上工作天数最多的团队就是“长杆”。C团队是VS的长柱,而我是C编译器团队的长柱,所以 .

    这是一个强大的抑制做不必要的性能工作,特别是 绩效工作可能会让事情变得更糟,而不是更好 . 没有过期策略的缓存有一个名称:它是一个

    正如您所注意到的,匿名函数是缓存的。当我实现lambdas时,我使用了与匿名函数相同的基础结构代码,因此缓存是(1)“沉没成本”--这项工作已经完成,关闭它比保持它打开要多,而且(2)已经过我的前任的测试和审查。

    我考虑使用相同的逻辑在表达式树上实现一个类似的缓存,但意识到这将(1)是工作,需要时间,我已经很短了,(2)我不知道缓存这样的对象会对性能产生什么影响。 代表真的很小 . 委托是单个对象;如果委托在逻辑上是静态的,而C#主动缓存的是静态的,那么它甚至不包含对接收者的引用。相比之下,表达式树是

    但如果收益很大,冒这个风险可能是值得的。那有什么好处呢?在将要远程到数据库的LINQ查询中,先问问自己“表达式树在哪里使用?”。 这是一个时间和内存都非常昂贵的操作

    将其与代表们赢得的绩效进行比较。“分配”的区别 x => x + 1 ,然后称之为“一百万次”和“检查缓存,如果它没有被缓存分配它,称之为”是用一个分配换取一个检查,这可以节省你整个纳秒。看起来没什么大不了的,但是

    因此,在C#3中不花费任何时间在这个不必要的、可能不引人注意的、不重要的优化上是一个很容易的决定。

    在C#4期间,我们要做的事情比重新考虑这个决定更重要。

    在C#4之后,团队分成两个子团队,一个重写编译器“Roslyn”,另一个在原始编译器代码库中实现async await。AsyncAwait团队完全被实现这个复杂而困难的特性所消耗,当然,团队比平常要小。他们知道,他们所有的工作最终都会在罗斯林复制,然后扔掉;那个编译器已经到了生命的尽头。因此,没有动机花费时间或精力来添加优化。

    当我在Roslyn重写代码时,建议的优化在我的清单上,但我们的最高优先权是在优化编译器的一小部分之前让编译器端到端地工作,在这项工作完成之前,我在2012年离开了微软。

    至于为什么我的同事在我离开后都没有再讨论这个问题,你得问问他们,但我很肯定他们非常忙,在真正的客户要求的真正的功能上,或者在性能优化上,用较小的成本获得更大的收益。这项工作包括开源编译器,这并不便宜。

    所以,如果你想完成这项工作,你有一些选择。

    • 编译器是开源的;你可以自己做。如果这听起来像是大量的工作,但对您没有什么好处,那么您现在可以更直观地理解为什么自2005年实现该功能以来没有人做过这项工作。

    当然,这对编译器团队来说仍然不是“免费的”。有人将不得不花费时间、精力和金钱来审查你的工作。请记住,性能优化的大部分成本不是更改代码所需的五分钟。这是在所有可能的现实世界条件的样本下进行的数周测试,证明了优化是有效的,不会让事情变得更糟!表演工作是我做的最贵的工作。

    • 设计过程是开放的。输入一个问题,并在该问题中,给出一个令人信服的理由,说明为什么您认为此增强是值得的。有数据。

    到目前为止你所说的都是为什么 可能的 . 可能不会切断它!很多事情都是可能的。请给出一些数字,说明为什么编译器开发人员应该把时间花在增强功能上,而不是实现客户要求的新功能上。

    避免重复分配复杂表达式树的真正好处是避免 ,这是一个严重的问题。C#中的许多特性都是为了避免收集压力而设计的,表达式树不是其中之一。 如果你想进行这种优化,我给你的建议是把注意力集中在它对压力的影响上,因为在那里你会找到最大的胜利,并且能够提出最有说服力的论点。

        2
  •  0
  •   X39    6 年前

    要意识到这种情况总是发生,请考虑将新数组传递给方法。

    this.DoSomethingWithArray(new string[] {"foo","bar" });
    

    IL_0001: ldarg.0
    IL_0002: ldc.i4.2
    IL_0003: newarr    [mscorlib]System.String
    IL_0008: dup
    IL_0009: ldc.i4.0
    IL_000A: ldstr     "foo"
    IL_000F: stelem.ref
    IL_0010: dup
    IL_0011: ldc.i4.1
    IL_0012: ldstr     "bar"
    IL_0017: stelem.ref
    IL_0018: call      instance void Test::DoSomethingWithArray(string[])
    

    同样的情况或多或少也适用于表达式,只是在这里编译器正在为您生成树,这意味着最终您需要知道何时需要缓存并相应地应用它。

    要获取缓存版本,请使用以下命令:

    private static System.Linq.Expressions.Expression<Func<object, string>> Exp = (obj) => obj.ToString();