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

我将立即说明文章的标题。最初,计划通过一个简单但现实的示例为加速反射的使用提供良好,可靠的建议,但是在基准测试期间,事实证明,反射的运行速度不像我想的那样慢,LINQ的运行速度比梦night中的梦想要慢。但是最后我发现我在测量方面也犯了一个错误。由于该示例每天都在发生并且在原则上得以实现(就像在企业中通常所做的那样),因此在我看来,这是非常有趣的生活演示:由于外部逻辑:Moq,Autofac,EF Core等,对本文主要主题的速度没有明显影响。 “捆扎”。

我以本文的印象开始了我的工作:反射为什么会变慢

如您所见,作者建议使用编译的委托而不是直接调用反射类型的方法,以极大地加快应用程序的速度。当然会有IL排放,但是我想避免这种情况,因为这是完成任务的最费力的方式,而且充满错误。

考虑到我一直对反射速度坚持类似的看法,所以我无意对作者的结论提出特别怀疑。

我经常在企业中遇到天真的使用反射。输入类型。属性信息已被获取。调用SetValue方法,每个人都很高兴。价值飞跃到目标领域,每个人都很高兴。非常聪明的人-资深人士和团队负责人-基于这样一种幼稚的“通用”映射器在另一种类型上的实现,将扩展写在对象上。这样做的本质通常是:我们采用所有字段,我们采用所有属性,然后对它们进行迭代:如果类型成员的名称重合,则执行SetValue。我们会定期捕获未命中的异常,其中一种类型没有找到某些属性,但是还有一种方法可以提高性能。试着抓。

我看到人们重新发明了解析器和映射器,但没有完全掌握有关自行车在工作之前如何发明的信息。我看到人们将他们的幼稚实现隐藏在策略,接口,注入之后,仿佛这会为以后的酒保辩解。通过这样的实现,我转过头了。实际上,我并没有衡量实际的性能泄漏,如果可以的话,如果可以的话,我只是将实现更改为更“最佳”的实现。因为下面将讨论的第一个测量值,我感到非常尴尬。

我认为,你们中的许多人在阅读Richter或其他意识形态学家时,已经得出了相当公正的断言,即代码反射是一种对应用程序性能产生非常负面影响的现象。

反射调用迫使CLR在汇编中四处寻找正确的汇编,提取其元数据,对其进行解析等。此外,序列遍历期间的反射会导致分配大量内存。我们花了内存,CLR发现了HZ并冻结了竞争。相信我,它应该明显慢一些。现代生产服务器或云计算机的大量内存无法从高处理延迟中节省下来。实际上,内存越多,您将越会注意到HZ的工作方式的可能性就越高。从理论上讲,反思对他来说是额外的一块红布。

但是,我们都使用IoC容器和日期映射器,它们的原理也基于反射,但是,通常不会出现有关其性能的问题。不,不是因为引入依赖和从外部受限上下文模型中抽象是非常必要的事情,所以无论如何我们都必须牺牲性能。一切都更简单-确实不会对性能产生很大影响。

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

如何实现的?实际上,这与平台本身用于生成JIT代码的逻辑没有什么不同。第一次调用该方法时,它将进行编译(是的,此过程并不快),随后进行调用,会将控制权转移到已编译的方法中,并且不会有任何性能下降。

在我们的情况下,您还可以使用JIT编译,然后以与AOT相同的性能使用已编译的行为。在这种情况下,表达将对我们有所帮助。

简而言之,我们可以将问题的原理表述如下:
反射最终结果应以包含已编译函数的委托的形式进行缓存。使用存储在对象外部的类型字段中有关类型的信息来缓存所有必需的对象(工作程序)也很有意义。

这是有逻辑的。常识告诉我们,如果可以编译和缓存某些内容,则应该这样做。

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

现在介绍代码。让我们看一个基于我最近在认真创建一个严肃的信用组织时必须面对的痛苦的例子。所有实体都是虚构的,因此没有人会猜测。

有一定的实体。让它成为联系人。有带有标准化正文的字母,解析器和水化器从中创建这些相同的联系人。收到一封信,我们读了它,分解了键值对,创建了一个联系人,并将其保存在数据库中。

这是基本的。假设联系人具有该属性的名称,年龄和联系电话。这些数据以字母形式传输。此外,企业希望获得支持,以便能够快速添加新键,以将实体属性映射到字母正文中的对。如果有人在模板中留下印记,或者在发布之前,有必要紧急地从新伙伴开始进行映射,以适应新格式。然后,我们可以添加一个新的映射关联作为廉价的数据修补程序。那就是一个生活的例子。

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

我不会提供代码:有很多资源,可以通过文章末尾的链接在GitHub上找到它们。您可以下载它们,对它们进行折磨以至无法识别并对其进行衡量,因为这会影响您的情况。我将只给出区分水化器的两个模板方法的代码,水化器应该快,水化器应该慢。

逻辑如下:模板方法接收由基本解析器逻辑形成的对。 LINQ级别是解析器和水化器的基本逻辑,它向db上下文发出请求,并使用解析器中的成对匹配密钥(对于这些功能,存在不带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;
        }

如我们所见,使用了带有属性设置器的静态集合-调用设置器实体的已编译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。

为了清楚起见和作为参考,我实现了一个朴素的方法,将它们的相关对的值直接写入实体字段。前缀是Manual。

现在,我们使用BenchmarkDotNet,我们将研究生产率。突然...(扰流板不是正确的结果,详细信息如下)



我们在这里看到什么?事实证明,凯旋地使用Fast前缀的方法在几乎所有遍中都比带有Slow前缀的方法要慢。对于分配和速度来说都是如此。另一方面,使用为此目的设计的LINQ方法进行的优美而优雅的映射实现会极大地降低性能。订单差异。趋势不会随着通过次数的不同而改变。区别仅在于规模。随着LINQ速度降低4到200倍,在相同规模下会有更多的碎片。

更新

我无法相信自己的眼睛,但更重要的是,我们的同事-Dmitry Tikhonov 0x1000000都不相信我的眼睛和我的代码。重新检查了我的解决方案后,他出色地发现并指出了由于实现中的许多更改而导致我遗漏的错误。在Moq设置中修复发现的错误后,所有结果均已确定。根据重新测试的结果,主要趋势没有改变-LINQ影响的性能仍然强于反射。但是,编译表达式的工作并非徒劳,而且结果在分配和运行时均可见。初始化静态字段时,第一次运行在“快速”方法中自然会变慢,但情况会进一步变化。

这是重新测试的结果:



结论:在企业中使用反射时,并不需要特别的技巧-LINQ将大大提高性能。但是,在需要优化的高负载方法中,可以以初始化程序和委托编译器的形式保留反射,然后提供“快速”逻辑。因此,您可以保持反射的灵活性以及应用程序的速度。

此处提供带有基准的代码。每个人都可以仔细检查一下我的话:
HabraReflectionTests

PS:代码在测试中使用IoC,在基准测试中使用显式设计。事实是,在最终实现中,我将所有可能影响性能和产生噪音的因素分隔开来。

PPS:多亏了Dmitry Tikhonov @ 0x1000000在Moq设置中检测我的错误,该错误影响了首次测量。如果任何读者有足够的业力,请喜欢。该名男子停下脚步,该名男子阅读,该名男子仔细检查并指出错误。我认为这值得尊重和同情。

PPPS:感谢认真细致的读者,他深入了解了样式和设计。我是为了统一和方便。演讲的外交方式有很多不足之处,但我考虑到了批评。我要求外壳。

All Articles