zoukankan      html  css  js  c++  java
  • C# 之 反射性能优化3

    在前二篇博客中,我分别介绍了二种优化反射的方法:
    1. Delegate:委托。
    2. CodeDOM:动态代码生成。
    这是二种截然不同的方法,性能的差距也很大。
    今天的博客将着重比较它们的优缺点,以及给出它们的使用建议。

    用Delegate优化反射的缺点

    在评价委托方案时,我认为有必要细分一下委托方案:
    1. 强类型委托,例如:Action<TTarget, TValue>
    2. 弱类型委托,例如:Action<object, object>

    它们的优点分别是:
    强类型委托:速度快,已经最接近直接调用的性能,然而它的缺点是 不通用。
    弱类型委托:比较通用,且经过一些代码封装后,使用方便,但是 封装后的性能会变差。

    用Delegate优化反射的优点

    优点有二个:
    1. 实现简单,不管是使用Emit, ExpressionTree还是CreateDelegate,代码量都不大。
    2. 方法通用,使用弱类型委托,我们可以封装出很容易使用的API,且适用于任何项目。

    用CodeDOM优化反射的优点

    最大的,也是唯一的优点就是:性能好。
    由于生成的是直接调用的代码,因此最终运行的是直接调用的代码,所以没有性能损耗。
    另外,代码生成器可以决定最终生成的代码质量,代码生成器越优秀,代码的性能也会更优秀。

    注意:当使用这种技术时,不同人可能会有不同的使用方法,最终可以得到性能不同的结果, (理论上)最坏情况下可能比委托还差。

    如果希望借助这种优化方式实现最好的性能,需要做好二件事情:
    1. 保证最终生成的代码质量是最优的。
    2. 编译方式的设计要合理(用好CodeDOM)。

    如何保证最终生成的代码质量是最优的,我给不了建议,需要您自己去思考,
    我们接着讨论第2点。

    如何用好CodeDOM?

    虽然采用动态编译技术,我们可以生成直接调用的代码来代替反射调用,这样就不会有任何性能损失。
    但是,还有一个问题也是需要考虑的:我该以什么粒度去生成代码?
    1. 是为每个反射调用生成代码?
    2. 还是为每个类型批量生成一段代码?
    3. 还是为一堆类型大批量的生成一批代码?

    由于动态编译的结果并不能直接调用,我们只能借助委托或者接口的方式去调用,
    所以如果每次代码生成的粒度较小,将会产生大量的程序集,也会消耗较多的编译器启动时间,
    因此,这并不是高效的做法。高效的做法应该是一次尽可能生成较多的代码。

    除此之外,还有一个问题也要考虑:当需要循环调用编译结果时,该怎么办?
    对于这类场景,我建议在生成代码时,把循环过程直接生成出来,最终只用一次调用编译结果完成整个调用过程。
    例如我们可以为数据访问层生成这样类似的代码,把循环、创建实体对象,以及给属性赋值的所有操作全部包含进来:

    public static List<Product> LoadProduct(DbDataReader reader)
    {
        List<Product> list = new List<Product>();
    
        while( reader.Read() ) {
            Product p = new Product();
            p.ProductID = (int)reader["ProductID"];
            p.ProductName = reader["ProductName"].ToString();
            p.CategoryID = (int)reader["CategoryID"];
            p.Unit = reader["Unit"].ToString();
            p.UnitPrice = (decimal)reader["UnitPrice"];
            p.Remark = reader["Remark"].ToString();
            p.Quantity = (int)reader["Quantity"];
            list.Add(p);
        }
        return list;
    }
    

    如果我们生成了这样的代码,最后只需要一次调用,就可以代替以前上百次的委托调用以及缓存查找,锁的冲突也会减少到最低。

    用CodeDOM优化反射的缺点

    缺点有三个:
    1. 方法不通用,需要针对不同的类型,不同的数据源生成不同的直接调用代码,因此难以通用化。
    2. 复杂性较高,由于是生成直接调用的代码,且数据类型及格式未知,因此需要周密的考虑各种情况,复杂性也随之增高。
    3. 难以封装,由于编译的结果是一个程序集,它并不能直接调用,还需要借助其它的方式来调用,所以难以实现较为通用的封装。

    能不能不使用委托?

    既然我们可以在运行时动态生成代码并编译它们,达到代替反射的目标,因此也就不需要委托调用的优化方法了。
    那么,委托还有意义吗? 或者说:优化反射时能不能不使用委托?

    在上篇博客中,我演示过动态编译的方法。
    由于动态编译的结果是一个程序集,它本身是不能直接调用,我们需要采用其它的方法去调用它。
    那篇博客给大家介绍了二种方法,其中一种方法就是用委托去调用程序集中的方法。
    由于那些在运行时生成的代码是由我们的代码生成的,方法的签名我们可以控制,
    所以,这时调用 Delegate.CreateDelegate 方法您不会遇到任何麻烦,
    因此,通过强类型的委托来调用CodeDOM的编译结果,这种配合会非常方便。
    正是由于这个原因,当您选择生成static类型的方法时,委托还是必须的,此时委托和CodeDOM将是一种共存关系。

    如果您在生成代码时采用了接口的设计方案,那么委托就没有必要使用了。

    根据反射密集程度选择优化方法

    优化反射,到底是选择CodeDOM,还是选择Delegate ?
    我认为要按不同的反射密集程度分开讨论。

    1. 反射密集程度低:例如:一次HTTP请求过程,我们的代码只需要一二次反射操作,
    或者对于桌面程序来说,在响应用户点击事件时,使用了几次反射调用。
    在这类场景中,反射的密集程度就可认为是很低的。那么这种情况下该如何优化呢?
    我的答案是:优不优化都无所谓,因为反射并不是慢得不能接受。
    反射的速度到底有多慢? 我们还是来看一下以前做过的测试吧:

    从这张图片(来源于本系列的第一篇)可以看出,用反射的方式执行属性赋值操作,就算运行1000000次,也只花了1.2秒! 要知道我的测试机器是3年前买的笔记本电脑,如果换成目前专业的服务器,消耗的时间会更少, 因此,这类反射的优化价值不大。 当然了,如果您愿意优化它们那也不是件坏事。

    2. 反射密集程度高:例如,数据访问层的应用中, 当一次加载一个实体列表时,反射次数是分页数量乘以字段数量,再加上创建实体对象数量。 这个数量很容易达到百次级别,而且一次HTTP请求过程中,可能需要加载多种数据,那么反射次数就很可观了。 我们经常感觉各种序列化和反序列化程序的执行效率不高,这与反射有着很直接的关系。 不过,我们通常不需要编写序列化反序列化程序,也只能被迫接受它们的性能了。 因此,对于反射密集程度很高的代码,如果优化手段不理想,肯定会影响性能。

    3. 当处于前二者之间的密集程度。由于这类场景实在是无法定性衡量, 而且不同人对性能敏感程度也不一样,或者由于不同的应用对性能的要求也不同。
    因此,这类场景的范围只能靠自己去评估了,优化方式也只能是自行选择了:
    1. 关注性能的话,就选择CodeDOM,
    2. 否则就选择Delegate吧,毕竟这种方法使用简单。

    CodeDOM优化的误区

    1. CodeDOM真能让程序的性能提升千倍吗?
    根据前面的截图,我们知道直接调用比反射调用的性能要提升千倍, 因此是不是可以认为采用动态编译的方法,程序的性能就能提升千倍?
    答案是否定的。举例来说,拿创建实体对象的场景来说,虽然反射调用所花时间和直接调用时间差了千倍, 即使我们用动态编译代替了反射,但是给属性赋值前,我们需要为那些属性获取数据。 然而,获取数据的操作极有可能比反射更慢,因此,对于整个过程来说,我们能优化的只是其中的一小部分, 所以,当我们测试整个过程时,性能不会提升到千倍。 性能提升多少倍,取决于反射在整个过程中所花时间的比例。

    2. CodeDOM方案一定比Delegate方案快。
    答案也是否定的,前面已经解释过了,如果您为每个反射调用去生成一个方法(委托的思路),那么最后还是需要一个委托或者一个接口来调用, 而且此时还要加上编译器的启动时间,最终的性能将比委托更慢。

    反射优化的总结

    反射优化的根本方法只有一条路:避开反射。
    然而,避开的方法可分为二种:
    1. 用委托去调用。(绕弯子)
    2. 生成直接调用代码,替代反射调用。(直截了当)

    这二种方法都有优缺点,我认为选择哪种方法应该根据反射场景来决定:
    1. 调用目标明确(名称和类型都是已知):强类型委托方法是较好的选择。
    2. 调用目标不明确,且调用程度密集:动态编译方法是最好的选择。
    3. 其它情况:可以用弱类型委托,或者不优化。

  • 相关阅读:
    yum源服务器搭建
    hadoop常用的端口配置
    yarn组建端口
    linux上go环境配置
    mysql主从复制
    一篇比较好的F5配置介绍
    统计随机数出现个数与直方图显示的C实现
    [LinuxC-笔记1]
    [转]达夫设备(Duff's device)
    [转]Ubuntu 18.04设置1920*1080
  • 原文地址:https://www.cnblogs.com/xinaixia/p/5777965.html
Copyright © 2011-2022 走看看