关于加速反射的不成功文章

我会立即解释文章的标题。 最初的计划是通过一个简单但现实的示例来就如何加速反射的使用提供良好、可靠的建议,但在基准测试过程中发现反射并不像我想象的那么慢,LINQ 比我的噩梦中还要慢。 但最后发现我的测量也犯了一个错误……这个人生故事的细节在剪辑和评论中。 由于该示例非常常见,并且原则上按照企业中通常执行的方式实施,因此在我看来,它是一个非常有趣的生活演示:对文章主题速度的影响是由于外部逻辑不明显:Moq、Autofac、EF Core 和其他“条带”。

我是在这篇文章的印象下开始工作的: 为什么反射慢

正如您所看到的,作者建议使用编译委托而不是直接调用反射类型方法,这是大大加快应用程序速度的好方法。 当然,存在 IL 排放,但我想避免它,因为这是执行任务的最劳动密集型方式,而且充满了错误。

考虑到我一直对反思速度持有类似的看法,所以我并没有特别打算质疑作者的结论。

我经常遇到在企业中幼稚地使用反射的情况。 类型已被采用。 有关财产的信息被获取。 SetValue 方法被调用,每个人都欢欣鼓舞。 价值已经到达目标领域,大家都很高兴。 非常聪明的人 - 前辈和团队领导 - 基于这种简单的实现从一种类型到另一种类型的“通用”映射器,编写他们的对象扩展。 本质通常是这样的:我们获取所有字段,获取所有属性,迭代它们:如果类型成员的名称匹配,我们执行 SetValue。 有时,我们会因错误而捕获异常,即我们没有在其中一种类型中找到某些属性,但即使在这里,也有一种提高性能的方法。 试着抓。

我见过人们重新发明解析器和映射器,但没有完全掌握有关他们之前的机器如何工作的信息。 我见过人们将他们幼稚的实现隐藏在策略、接口、注入后面,好像这可以为随后的狂欢找借口。 我对这种认识嗤之以鼻。 事实上,我没有测量真正的性能泄漏,如果可能的话,我只是将实现更改为更“最佳”的实现(如果我能得到它)。 因此,下面讨论的第一个测量结果让我感到非常困惑。

我想你们中的许多人在读过 Richter 或其他思想家的著作后,都遇到过一个完全公平的说法,即代码中的反射是一种对应用程序的性能产生极其负面影响的现象。

调用反射会强制 CLR 遍历程序集以找到所需的程序集、提取其元数据、解析它们等。 另外,遍历序列时的反射会导致分配大量内存。 我们正在耗尽内存,CLR 会发现 GC,并且开始出现条纹。 相信我,它应该明显很慢。 现代生产服务器或云计算机上的大量内存并不能防止高处理延迟。 事实上,内存越多,您就越有可能注意到 GC 的工作原理。 从理论上讲,反思对他来说是一块额外的红布。

然而,我们都使用IoC容器和日期映射器,其运行原理也是基于反射,但通常不会对其性能产生任何疑问。 不,并不是因为引入依赖关系和从外部有限上下文模型中抽象是非常必要的,以至于我们无论如何都必须牺牲性能。 一切都变得更简单——它确实不会对性能产生太大影响。

事实上,基于反射技术的最常见框架使用各种技巧来更优化地使用它。 通常这是一个缓存。 通常,这些是从表达式树编译的表达式和委托。 同一个自动映射器维护一个竞争字典,将类型与函数相匹配,这些函数可以将一种类型转换为另一种类型,而无需调用反射。

这是如何实现的? 本质上,这与平台本身用来生成 JIT 代码的逻辑没有什么不同。 当第一次调用一个方法时,它会被编译(是的,这个过程并不快);在后续调用中,控制权将转移到已经编译的方法,并且不会有明显的性能下降。

在我们的例子中,您还可以使用 JIT 编译,然后使用与 AOT 对应项具有相同性能的编译行为。 在这种情况下,表达式会为我们提供帮助。

所讨论的原则可以简要表述如下:
您应该将反射的最终结果缓存为包含已编译函数的委托。 将所有必要的对象与类型信息缓存在存储在对象外部的类型(即工作人员)的字段中也是有意义的。

这是有逻辑的。 常识告诉我们,如果某些东西可以编译和缓存,那么就应该这样做。

展望未来,应该说,即使您不使用建议的编译表达式的方法,缓存在使用反射时也有其优点。 其实,我在这里只是重复一下我上面提到的文章作者的论点。

现在关于代码。 让我们看一个例子,这个例子是基于我最近在一家严肃的信贷机构的严肃制作中不得不面对的痛苦。 所有实体都是虚构的,因此没有人会猜测。

有一些本质。 要有联系方式。 有些字母具有标准化的正文,解析器和水化器从中创建这些相同的联系人。 一封信到达,我们阅读它,将其解析为键值对,创建一个联系人,并将其保存在数据库中。

这是初级的。 假设联系人具有“全名”、“年龄”和“联系电话”属性。 该数据通过信函传输。 该企业还希望能够快速添加新键,以将实体属性映射到信件正文中的对中。 如果有人在模板中犯了错字,或者在发布之前需要紧急启动新合作伙伴的映射,以适应新的格式。 然后我们可以添加一个新的映射相关性作为廉价的数据修复。 也就是生活中的例子。

我们实施、创建测试。 作品。

我不会提供代码:有很多来源,可以通过文章末尾的链接在 GitHub 上获取它们。 您可以加载它们,将它们折磨得面目全非并测量它们,因为这会影响您的情况。 我只会给出两个模板方法的代码,以区分水合器(应该是快的)和水合器(应该是慢的)。

逻辑如下:模板方法接收基本解析器逻辑生成的对。 LINQ 层是解析器和 Hydrator 的基本逻辑,它向数据库上下文发出请求,并将键与解析器中的对进行比较(对于这些函数,有不带 LINQ 的代码进行比较)。 接下来,这些对被传递到主水合方法,并将这些对的值设置为实体的相应属性。

“快速”(基准测试中的前缀“快速”):

 protected override Contact GetContact(PropertyToValueCorrelation[] correlations)
        {
            var contact = new Contact();
            foreach (var setterMapItem in _proprtySettersMap)
            {
                var correlation = correlations.FirstOrDefault(x => x.PropertyName == setterMapItem.Key);
                setterMapItem.Value(contact, correlation?.Value);
            }
            return contact;
        }

正如我们所看到的,使用了具有 setter 属性的静态集合 - 调用 setter 实体的已编译 lambda。 由以下代码创建:

        static FastContactHydrator()
        {
            var type = typeof(Contact);
            foreach (var property in type.GetProperties())
            {
                _proprtySettersMap[property.Name] = GetSetterAction(property);
            }
        }

        private static Action<Contact, string> GetSetterAction(PropertyInfo property)
        {
            var setterInfo = property.GetSetMethod();
            var paramValueOriginal = Expression.Parameter(property.PropertyType, "value");
            var paramEntity = Expression.Parameter(typeof(Contact), "entity");
            var setterExp = Expression.Call(paramEntity, setterInfo, paramValueOriginal).Reduce();
            
            var lambda = (Expression<Action<Contact, string>>)Expression.Lambda(setterExp, paramEntity, paramValueOriginal);

            return lambda.Compile();
        }

总的来说是很清楚的。 我们遍历属性,为它们创建调用 setter 的委托,然后保存它们。 然后我们会在必要时打电话。

“慢”(基准测试中的前缀“慢”):

        protected override Contact GetContact(PropertyToValueCorrelation[] correlations)
        {
            var contact = new Contact();
            foreach (var property in _properties)
            {
                var correlation = correlations.FirstOrDefault(x => x.PropertyName == property.Name);
                if (correlation?.Value == null)
                    continue;

                property.SetValue(contact, correlation.Value);
            }
            return contact;
        }

这里我们立即绕过属性,直接调用SetValue。

为了清楚起见并作为参考,我实现了一种简单的方法,将其相关对的值直接写入实体字段。 前缀 – 手动。

现在让我们使用 BenchmarkDotNet 来检查性能。 突然...(剧透 - 这不是正确的结果,详细信息如下)

关于加速反射的不成功文章

我们在这里看到什么? 事实证明,带有 Fast 前缀的方法几乎在所有传递中都比带有 Slow 前缀的方法慢。 对于工作分配和速度来说都是如此。 另一方面,尽可能使用 LINQ 方法进行漂亮而优雅的映射实现,相反,会大大降低生产力。 区别在于顺序。 趋势不会随着通过次数的不同而改变。 唯一的区别在于规模。 使用 LINQ 时速度要慢 4 - 200 倍,并且在大致相同的规模上会有更多的垃圾。

已更新

我不相信自己的眼睛,更重要的是,我们的同事既不相信我的眼睛,也不相信我的代码—— 德米特里·吉洪诺夫 0x1000000。 在仔细检查了我的解决方案后,他出色地发现并指出了一个错误,该错误是由于从最初到最终的实施过程中的许多变化而导致我错过的。 修复了最小起订量设置中发现的错误后,所有结果都已到位。 根据重新测试的结果,主要趋势没有改变——LINQ仍然比反射更影响性能。 然而,令人高兴的是,表达式编译的工作并没有白费,而且结果在分配和执行时间上都是可见的。 第一次启动时,当初始化静态字段时,对于“快速”方法来说自然会较慢,但随后情况发生了变化。

这是重新测试的结果:

关于加速反射的不成功文章

结论:在企业中使用反射时,没有特别需要诉诸技巧——LINQ会消耗更多的生产力。 然而,在需要优化的高负载方法中,您可以以初始化器和委托编译器的形式保存反射,然后这将提供“快速”逻辑。 这样您就可以同时保持反射的灵活性和应用程序的速度。

基准代码可在此处获取。 任何人都可以仔细检查我的话:
Habra反射测试

PS:测试中的代码使用 IoC,而在基准测试中它使用显式构造。 事实是,在最终的实现中,我切断了所有可能影响性能并使结果变得嘈杂的因素。

PPS:感谢用户 德米特里·吉洪诺夫@0x1000000 发现我在设置起订量时的错误,这影响了第一次测量。 如果读者有足够的缘分,请点赞。 那人停了下来,那人读了一遍,那人仔细检查并指出了错误。 我想这是值得尊重和同情的。

PPPS:感谢细心的读者,他们深入了解了风格和设计。 我是为了统一和方便。 演讲的外交技巧还有很多不足之处,但我考虑了这些批评。 我要弹丸。

来源: habr.com

添加评论