.NET 机能优化的技能 | 申博官网
登录
  • 欢迎进入申博官网!
  • 如果您觉得申博官网对你有帮助,那么赶紧使用Ctrl+D 收藏申博官网并分享出去吧
  • 这里是申博官方网!
  • 申博官网是菲律宾sunbet官网品牌平台!
  • 申博开户专业品牌平台!

.NET 机能优化的技能

申博_安全防护 申博 100次浏览 已收录 0个评论

最大化内联

内联是将要领体(method body)复制到挪用站点的手艺,如许我们就可以防止跳转、参数通报和寄存器保留/恢复等烦琐历程。除了节约这些之外,内联照样完成其他优化的必要前提。 不不过Roslyn(C#的编译器)没有内联代码,它是经由过程JIT完成的,大多数优化也是云云。

运用静态扔掷助手(static throw helper)

近来的变化触及一个重要的重构,在序列化基准的挪用持续时候上增添了约莫20ns,从~130ns增添到了~150ns。

罪魁祸首是这个助手要领中增加的throw语句:

public static Writer<TBufferWriter> CreateWriter<TBufferWriter>(
    this TBufferWriter buffer,
    SerializerSession session) where TBufferWriter : IBufferWriter<byte>
{
    if (session == null) throw new ArgumentNullException(nameof(session));
    return new Writer<TBufferWriter>(buffer, session);
}

当助手要领中包含throw语句时,JIT不会内联它。处置惩罚这个题目的罕见技能是增加一个静态的“throw helper”要领,来完成一些辣手的事情,所以终究效果以下所示:

public static Writer<TBufferWriter> CreateWriter<TBufferWriter>(
    this TBufferWriter buffer,
    SerializerSession session) where TBufferWriter : IBufferWriter<byte>
{
    if (session == null) ThrowSessionNull();
    return new Writer<TBufferWriter>(buffer, session);

    void ThrowSessionNull() => throw new ArgumentNullException(nameof(session));
}

代码库在很多地方运用这个技能,将throw语句放在一个零丁的要领中可以会有其他优点,比方比方改良经常使用代码途径的位置。

最小化假造或接口挪用

假造挪用比直接挪用慢,假如你正在编写一个症结体系,那末极可以会在剖析器中看到假造挪用的历程。起首,假造挪用须要间接挪用。

去假造化是很多JIT编译器的一个特征,RyuJIT也不破例。然则,这是一个庞杂的功用,而且RyuJIT现在可以证实(自身)要领可以被假造化并因而成为内联的候选者的状况并不多。以下是运用假造化的一些通例技能,但我确信另有更多。

1. 默许状况下将类标记为sealed,当一个类/要领被标记为sealed时,RyuJIT可以将其斟酌在内而且可以可以内联一个要领挪用。RyuJIT极可以成为下一代的JIT编译器。64位盘算已是大势所趋,纵然它并不老是比32位更快或更有用力。当前的.NET JIT编译器就是一个使得64位盘算机上偶然致使顺序速率减慢的的例子。然则,这将会被转变:一个新的,下一代x64的JIT编译器编译代码的速率将加速两倍,它将转变你对64位.NET代码的印象。

2. 假如可以,将掩盖(override)要领标记为sealed。override可以翻译为掩盖,从字面就可以晓得,它是掩盖了一个要领而且对其重写,以求到达差别的作用。对我们来讲最熟习的掩盖就是对接口要领的完成,在接口中平常只是对要领举行了声明,而我们在完成时,就须要完成接口声明的一切要领。除了这个典范的用法之外,我们在继承中也可以会在子类掩盖父类中的要领。

3. 运用详细范例而不是接口,详细范例为JIT供应了更多信息,因而它更有可以内联你的挪用。

4. 在统一要领中实例化和运用非sealed对象(而不是运用’create’要领),当范例明白已知时,比方组织以后,RyuJIT可以对非sealed要领挪用举行假造化。

5. 对多态范例运用泛型范例束缚,以便可以运用详细范例对它们举行特地处置惩罚,而且可以对接口挪用举行非假造化。在Hagar中,我们的中心编写器范例定义以下:

public ref struct Writer<TBufferWriter> where TBufferWriter : IBufferWriter<byte>
{
    private TBufferWriter output;
    // --- etc ---

一切对CIL中Roslyn发出的输出要领的挪用之前都邑有一条束缚指令,该指令通知JIT,该挪用可以对TBufferWriter上定义的准确要领举行挪用,而不是举行假造/接口挪用。这有助于去假造化。效果,一切对在输出上定义的要领的挪用都被胜利地去假造化。下面是由JIT团队的Andy Ayers 编写的CoreCLR线程,它详细描述了当前和将来的去假造化事情。

削减分派

.NET的渣滓网络器是一项很巨大的项目, 渣滓网络器是 许可对一些无锁数据构造举行算法优化,而且还可以删除全部类的毛病并减轻开发人员的认知累赘。总之,渣滓网络是一种非常胜利的内存治理手艺。

.NET运用bump分派器,个中每一个线程经由过程找到各自的指针来从每一个线程高低文中分派对象。因而,当在统一线程上分派和运用短时候分派时,可以更好的完成部分缓存(cache locality)机制。

有关.NET 渣滓网络器的更多信息,请点此相识。

对象池(Object Pool) 或缓冲池(Buffer Pool) 

Hagar自身并不治理缓冲区,而是将义务转移给用户。这听起来可以很贫苦,但实际上并不贫苦,由于它与System.IO.Pipelines兼容。因而,我们可以运用默许管道经由过程System.Buffers.ArrayPool <T>供应的高机能缓冲池。

平常来讲,重复运用缓冲区可以减轻渣滓网络器的压力。

防止装箱

只管不要经由过程将值范例转换为援用范例来装箱值范例。这是罕见的发起,但在API设想中须要斟酌一些要素。在Hagar中,可以接收值范例的接口和要领定义是通用的,以便它们可以特地用于准确范例并防止装箱/拆箱本钱。效果,没有热途径拳击。在某些状况下依然存在拳击,比方非常要领的字符串花样。可以经由过程对参数的显式. tostring()挪用来删除那些特定的装箱分派。

削减封闭分派

只分派闭包一次,并存储效果,就可供屡次重复运用。比方,一般将托付通报给ConcurrentDictionary. getoradd。与其将托付编写为内联lambda,还不如将define定义为类中的私有字段。下面是来自Hagar中可选ISerializable支撑包的一个例子:

private readonly Func<Type, Action<object, SerializationInfo, StreamingContext>> createConstructorDelegate;

public ObjectSerializer(SerializationConstructorFactory constructorFactory)
{
    // Other parameters/statements omitted.
    this.createConstructorDelegate = constructorFactory.GetSerializationConstructorDelegate;
}

// Later, on a hot code path:
var constructor = this.constructors.GetOrAdd(info.ObjectType, this.createConstructorDelegate);

只管削减复制

.NET Core 2.0和2.1以及近来的C#版本,在删除数据复制历程的方面取得了相当大的进步。最值得注意的是Span<T>,但在参数修饰符和只读构造中也值得一提。Span<T> 是ref 构造客栈,而不是托管堆上分派。

运用Span<T>来防止数组分派并防止数据复制

一个Span<T>示意恣意内存的相邻地区, 一个Span<T>实例通经常使用来保留数组的元素或数组的一部分。

关于.NET Core来讲,Span<T>关于机能优化非常重要,它们运用优化的示意来减小它们的大小,这须要增加对内部指针的渣滓网络器的支撑。内部指针是指向数组范围内的托管援用,而不是只能指向第一个元素,因而须要一个包含数组偏移量的附加字段。有关Span<T>的更多信息,请点此参考。

Hagar普遍运用Span<T>,由于它许可我们建立可用于较大缓冲区的分段试图。

美国F15战斗机存在严重安全漏洞

在8月8日-11日举行的defcon安全大会上,一个由7名白帽子组成的团队经过授权对美国军用战斗机飞行系统进行了安全测试,测试发现F15战斗机系统中存在多个严重漏洞。 攻击者利用漏洞可以控制视频摄像头和传感器,在飞行时可以关闭Trusted Aircraft Information Download Station (可信飞机信息下载站,TADS)。TADS是在战斗机飞行过程中从视频摄像头和传感器来收集数据的一个价值2万美元的设备。 白帽子还发现了美国空军在去年11月举行的defcon大会上发现的尚未修复的安全漏洞。美国空军发言人称未来会继续让美国的白帽子对军用信息系统开展安全测试,以在敌国之前发现系统中

经由过程ref通报构造以最小化客栈上的副本

Hagar运用两个重要构造,Reader 和Writer<TOutputBuffer>。这些构造包含几个字段,险些每次挪用都邑通报给序列化或反序列化挪用途径。

在没有干涉干与的状况下,运用这些构造举行的每一个要领挪用都邑带来很大的影响,由于每一个挪用都须要将全部构造复制到客栈中。

我们可以经由过程将这些构造作为ref参数通报来防止副本的发作,别的,C#还支撑运用ref this作为扩大要领的目的,这非常轻易。据我所知,没有方法确保特定的构造范例老是由ref通报,假如你不小心在挪用的参数列表中省略了ref,这可以会致使运转毛病。

防止保护性拷贝(defensive copy) 

Roslyn偶然须要做一些事情来保证一些言语不变量,当构造存储在只读字段中时,编译器将插进去一些指令,以防止复制该字段,然后再将其包含就任何能保证不会对其举行修正的操纵中。一般,这意味着挪用在构造范例自身上定义的要领,由于将构造作为参数通报给在另一范例上定义的要领已须要将构造复制到客栈上(除非经由过程ref或in通报)。

假如将<LangVersion> 7.2 </ LangVersion>增加到csproj文件中,则可以将构造定义为只读构造(这是c# 7.2的言语特征),则可以防止保护性拷贝。

偶然,假如你没法将其定义为只读构造,则最好在其他不可变构造字段上省略readonly修饰符。

以Jon Skeet的NodaTime库为例,在这个示例中,Jon使大多数构造变成只读,因而可以将readonly修饰符增加到包含这些构造的字段中,而不会对机能发作负面影响。

削减分支和分支毛病展望

当代cpu依赖于长pipeline的指令,这些指令经由过程并发性举行处置惩罚。这触及到CPU剖析指令,以肯定哪些指令不依赖于前面的指令,还触及猜想将采纳哪些前提跳转语句。为此,CPU运用一个名为分支展望器(branch predictor)的组件,该组件担任猜想将采纳哪一个分支。它一般经由过程读取和写入表中的条目来完成这一点,并依据上次实行前提跳转时发作的状况修正其展望。

当展望准确时,就会加速进程,不然就须要把展望分支的指令排空,从新猎取准确分支的指令进入pipeline继承实行。

所以加速进程的最好方法就是削减分支和分支毛病展望,起首尝试最小化分支数目,假如没法消弭分支,请只管削减毛病展望率,这可以触及运用排序数据或重构代码,可以用查找的方法来替代分支展望。

其他杂项提醒

1. 防止运用LINQ,LINQ在运用顺序代码方面很精彩,但在库/框架代码中很少被用于途径。LINQ很难对JIT举行优化(IEnumerable<T>..),而且倾向于多多分派。

2. 运用详细范例而不是接口或笼统范例,或许最罕见的是,假如你在List <T>上举行迭代,最好不要先将该列表强迫转换为IEnumerable <T>(比方,经由过程运用LINQ或将其作为IEnumerable <T>参数通报给要领)。如许做的原因是运用foreach罗列列表运用非分派List <T> .Enumerator构造,然则当它转换为IEnumerable <T>时,该构造必需被装箱到IEnumerator <T> for foreach。

3. 反射在库代码中迥殊有用,缓存反射效果,斟酌运用IL或Roslyn为接见器天生托付,或许更好的要领是运用现有的库,如Microsoft.Extensions.ObjectMethodExecutor.Sources,Microsoft.Extensions.PropertyHelper.Sources或FastMember。

特定于库的优化

优化天生的代码

Hagar运用Roslyn为要序列化的POCO天生C#代码,这个C#代码在编译时包含在你的项目中。我们可以对天生的代码实行一些优化,以加速速率。

经由过程跳过对已知范例的编解码器查找来防止假造挪用

当庞杂对象包含尽人皆知的字段(如int,Guid,string)时,代码天生器将直接插进去对这些范例的手动编码编解码器的挪用,而不是挪用CodecProvider来检索该范例的IFieldCodec <T>实例。这许可JIT内联那些挪用,并防止了假造/接口间接。

在运转时特地化泛型范例

与上面相似,代码天生器可以天生在运转时运用特地化的代码。

预先盘算常数值以消弭某些分支

在序列化时期,每一个字段都带有一个标头,一般是一个字节。它会通知解串器哪一个字段是编码的。此字段题目包含3条信息:字段的规格(牢固宽度、长度前缀、标记分开、援用等),字段的形式范例(预期、尽人皆知、之前定义的、编码)用于多态,并将末了3位专用于编码字段id(假如它小于7)。在很多状况下,可以确实地晓得在编译时这个标头字节是什么。假如字段具有值范例,那末我们就晓得运转时范例永久不能与字段范例差别,而且一直晓得字段id。

因而,我们一般可以保留盘算标头值所需的一切事情,并可以直接将其作为常量嵌入到代码中。如许可以节约分支而且一般会消弭大批的中心言语代码。

挑选恰当的数据构造

经由过程切换到构造数组,很大程度上消弭了索引和保护鸠合的本钱,而且参考跟踪不再出现在基准测试中。这有一个瑕玷,关于大型对象图,这类新要领可以较慢。

挑选适宜的算法

Hagar消费大批时候对可变长度整数举行编码/解码,这类要领被称为varints,varints是用一个或多个字节序列化整形的一种要领,以减小有用载荷的大小。很多二进制序列化器运用这类手艺,包含协定缓冲区。以至.NET的BinaryWriter也运用这类编码。下面是参考资料的一小段:

protected void Write7BitEncodedInt(int value) {
    // Write out an int 7 bits at a time.  The high bit of the byte,
    // when on, tells reader to continue reading more bytes.
    uint v = (uint) value;   // support negative numbers
    while (v >= 0x80) {
        Write((byte) (v | 0x80));
        v >>= 7;
    }
    Write((byte)v);
}

我想指出ZigZag编码关于包含负值的有标记整数可以更有用,而不是强迫转换为uint。

这些序列化器中的变量运用称为Little Endian Base-128或LEB128的算法,该算法每字节编码多达7位。它运用每一个字节的最高有用位来指导是不是追随另一个字节(1 =是,0 =否)。这是一种简朴的花样,但可以不是最快的。不过PrefixVarint更快,运用PrefixVarint,一切来自LEB128的1都是在有用载荷的开首一次性写入的。这可以让我们运用硬件内涵函数来进步这类编码和解码的速率。经由过程将大小信息往前移,我们也可以从有用载荷中一次读取更多字节,从而削减内部压力并进步机能。

本文翻译自:https://reubenbond.github.io/posts/dotnet-perf-tuning


申博|网络安全巴士站声明:该文看法仅代表作者自己,与本平台无关。版权所有丨如未注明 , 均为原创丨本网站采用BY-NC-SA协议进行授权
转载请注明.NET 机能优化的技能
喜欢 (0)
[]
分享 (0)
发表我的评论
取消评论
表情 贴图 加粗 删除线 居中 斜体 签到

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址