gpt4 book ai didi

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

转载 作者:行者123 更新时间:2023-12-03 14:59:51 26 4
gpt4 key购买 nike

考虑以下类:

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;
}
}

这样,第一次调用传递的委托(delegate)被初始化一次并在多个 Test 上重用。调用。

然而,第二次调用传递的表达式树没有被重用——在每个 Test 上初始化一个新的 lambda 表达式。称呼。

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

编辑

我想我需要澄清为什么我认为表达式树适合缓存。
  • 生成的表达式树在编译时是已知的(好吧,它是由编译器创建的)。
  • 它们是不可变的。因此,与下面 X39 给出的数组示例不同,表达式树在初始化后无法修改,因此可以安全缓存。
  • 代码库中只能有这么多表达式树 - 同样,我在谈论可以缓存的那些,即使用 lambda 表达式初始化的那些(不是手动创建的那些)而不捕获任何外部状态/变量。字符串文字的自动实习将是一个类似的例子。
  • 它们是用来遍历的——它们可以被编译来创建一个委托(delegate),但这不是它们的主要功能。如果有人想要一个编译委托(delegate),他们可以只接受一个(一个 Func<T> ,而不是一个 Expression<Func<T>> )。接受表达式树表明它将被用作数据结构。因此,“应首先编译它们”并不是反对缓存表达式树的合理论据。

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

    最佳答案

    Why don't non-capturing expression trees that are initialized using lambda expressions get cached?



    我在编译器中编写了该代码,包括原始 C# 3 实现和 Roslyn 重写。

    正如我在被问到“为什么不”时经常说的: 编译器编写者不需要提供他们没有做某事的原因 .做某事需要工作,需要努力,并且要花钱。因此,默认位置总是在不需要工作时不做某事。

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

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

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

    在开发 C# 3.0 时,Visual Studio 有一个特定的日期“发布到制造”,这是一个古怪的术语,当时软件主要分布在 CDROM 上,一旦打印就无法更改。这个日期不是任意的。相反,有一个完整的依赖链紧随其后。比如说,如果 SQL Server 有一个依赖于 LINQ 的功能,那么将 VS 的发布推迟到当年 SQL Server 发布之后是没有任何意义的,因此 VS 的时间表影响了 SQL Server 的时间表,进而影响了其他团队的时间表等。

    因此,VS组织中的每个团队都提交了一份时间表,而该时间表上工作天数最多的团队就是“长杆”。 C#团队是VS的长杆,我是C#编译器团队的长杆,所以 我交付编译器功能迟到的每一天都是 Visual Studio 和每个下游产品都会推迟时间表并让客户失望的一天 .

    这是执行不必要的性能工作的强大抑制因素,尤其是 性能工作可能会使事情变得更糟,而不是更好 .没有过期策略的缓存有一个名字:它是内存泄漏。

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

    我考虑过使用相同的逻辑在表达式树上实现类似的缓存,但意识到这将 (1) 是可行的,这需要时间,而我已经很短了,并且 (2) 我不知道性能影响会是什么缓存这样一个对象。 代表真小 .委托(delegate)是一个单一的对象;如果委托(delegate)在逻辑上是静态的,即 C# 积极缓存的委托(delegate),它甚至不包含对接收器的引用。相比之下,表达式树是潜在的巨大树。它们是小对象的图形,但该图形可能很大。对象图越长,垃圾收集器的工作就越多!

    因此,任何用于证明缓存委托(delegate)的决定的性能测试和指标都不适用于表达式树,因为内存负担完全不同。我不想在我们最重要的新语言功能中创建新的内存泄漏源。风险太高了。

    但是,如果 yield 很大,那么冒险可能是值得的。那么有什么好处呢?首先问自己“表达式树在哪里使用?”在将远程连接到数据库的 LINQ 查询中。 这在时间和内存上都是非常昂贵的操作 .添加缓存不会让您大获全胜,因为您即将完成的工作比获得的成本高出数百万倍;胜利是噪音。

    将其与代表的表现获胜进行比较。 “分配 x => x + 1 ,然后调用它”一百万次与“检查缓存,如果它没有被缓存,则分配它,调用它”之间的区别在于用分配换一个检查,这可以为您节省整个纳秒。这似乎没什么大不了的,但调用也需要纳秒,所以按百分比计算,这很重要。缓存委托(delegate)是一个明显的胜利。缓存表达式树离明显的胜利还很遥远;我们需要数据来证明风险是合理的。

    因此,在 C# 3 中不花任何时间在这种不必要的、可能不引人注目的、不重要的优化上是一个简单的决定。

    在 C# 4 期间,我们有许多比重新审视这个决定更重要的事情要做。

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

    当我在 Roslyn 中重写代码时,建议的优化在我的考虑事项列表中,但我们的首要任务是在我们优化其中的一小部分之前让编译器端到端工作,而我在 2012 年离开了微软,在那之前工作完成了。

    至于为什么我离开后我的同事没有人重新讨论这个问题,你必须问他们,但我很确定他们非常忙于在真实客户要求的真实功能上做实际工作,或者在性能优化上以更小的成本赢得更大的胜利。这项工作包括开源编译器,这并不便宜。

    所以,如果你想完成这项工作,你有一些选择。
  • 编译器是开源的;你可以自己做。如果这听起来像是很多工作对您几乎没有什么好处,那么您现在可以更直观地理解为什么自 2005 年该功能实现以来没有人完成这项工作。

  • 当然,这对编译器团队来说仍然不是“免费”的。有人将不得不花费时间、精力和金钱来审查您的工作。请记住,性能优化的大部分成本不是更改代码所需的五分钟。这是在所有可能的真实世界条件样本下进行的数周测试,证明优化有效并且不会使事情变得更糟!表演工作是我所做的最昂贵的工作。
  • 设计过程是开放的。输入一个问题,并在该问题中给出一个令人信服的理由,说明为什么您认为此增强功能值得。用数据。

  • 到目前为止,您所说的只是为什么它是可能的。可能不会削减它!很多事情都是可能的。给我们一些数字来证明为什么编译器开发人员应该花时间进行这种增强而不是实现客户要求的新功能。

    避免重复分配复杂表达式树的实际好处是避免收集压力,这是一个严重的问题。 C# 中的许多功能旨在避免收集压力,表达式树不是其中之一。 如果你想要这种优化,我给你的建议是专注于它对压力的影响,因为那是你会找到最大的胜利并能够提出最有说服力的论点的地方。

    关于c# - 为什么不缓存使用 lambda 表达式初始化的非捕获表达式树?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/52793106/

    26 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com