最大化内联内联是一种将方法体复制到调用点的技术,这样我们就可以避免跳转、参数传递和寄存器保存/恢复的繁琐过程。除了这些节省之外,内联是实现其他优化所必需的。Roslyn(C#的编译器)不仅不内联代码,它还像大多数优化一样通过JIT进行内联。使用静态抛出助手最近的一项更改涉及重大重构,将序列化基准的调用持续时间增加了大约20ns,从~130ns到~150ns。罪魁祸首是在这个辅助方法中添加的throw语句:>(buffer,session);}当辅助方法包含throw语句时,JIT不会内联它。解决这个问题的一个常见技巧是添加一个静态的“throwhelper”方法来完成一些棘手的工作,因此最终结果如下所示:{if(session==null)ThrowSessionNull();returnnewWriter(buffer,session);voidThrowSessionNull()=>thrownewArgumentNullException(nameof(session));}代码库很多地方都用到了这个trick,把throw语句放在单独的方法中可能还有其他好处,比如改进常用代码路径的放置。最小化虚拟或接口调用虚拟调用比直接调用慢,如果您正在编写一个关键系统,您很可能会在分析器中看到虚拟调用的过程。首先,虚拟调用需要间接调用。去虚拟化是很多JIT编译器的特性,RyuJIT也不例外。然而,这是一个复杂的特性,目前RyuJIT可以证明(自己)一个方法可以被虚拟化从而成为内联候选者的案例并不多。这里有一些利用虚拟化的一般技巧,但我相信还有更多。1.类默认被标记为密封,当一个类/方法被标记为密封时,RyuJIT可以考虑到这一点并且可以内联一个方法调用。RyuJIT很可能是下一代的JIT编译器。64位计算将继续存在,即使它并不总是比32位计算更快或更高效。当前的.NETJIT编译器是有时会降低64位机器上程序速度的一个例子。但是,这种情况即将发生变化:一种新的下一代x64JIT编译器,其编译代码的速度是原来的两倍,这将改变您对64位.NET代码的看法。2.如果可能,将覆盖方法标记为密封。Override可以翻译为覆盖。从字面上可以知道,它覆盖了一个方法,并对其进行重写,以实现不同的功能。我们最熟悉的覆盖范围是接口方法的实现。一般在接口中只声明了方法,我们在实现的时候,需要实现接口声明的所有方法。除了这种典型的用法,我们还可能在继承中在子类中重写父类中的方法。3.使用具体类型而不是接口,具体类型为JIT提供了更多信息,因此更有可能内联您的调用。4.在同一个方法中实例化和使用非密封对象(而不是使用'create'方法),当类型明确知道时,比如在构建之后,RyuJIT可以虚拟化非密封方法调用。5.对多态类型使用通用类型约束,以便它们可以专门化为具体类型,并且接口调用可以去虚拟化。在Hagar中,我们的核心writer类型定义如下:publicrefstructWriterwhereTBufferWriter:IBufferWriter{privateTBufferWriteroutput;//---etc---CIL中Roslyn发出的所有输出方法的调用之前都会有一个Constraint指令告诉JIT可以对TBufferWriter上定义的确切方法进行调用,而不是进行虚拟/接口调用。这有助于去虚拟化。因此,对输出中定义的方法的所有调用都已成功去虚拟化。下面是JIT团队的AndyAyers编写的CoreCLR线程,详细介绍了当前和未来的去虚拟化工作。减少分配。.NET的垃圾收集器是一个伟大的项目,垃圾收集器允许对一些无锁数据结构进行算法优化,它还消除了整类错误并减轻了开发人员的认知负担。总之,垃圾回收是一种非常成功的内存管理技术。.NET使用bump分配器,其中每个线程通过查找自己的指针从每个线程上下文分配对象。因此,在同一线程上分配和使用短期分配时,可以更好地实现缓存局部性机制。在此处了解有关.NET垃圾收集器的更多信息。对象池(ObjectPool)或缓冲池(BufferPool)Hagar本身并不管理缓冲区,而是将责任转嫁给用户。这听起来可能很麻烦,但实际上并非如此,因为它与System.IO.Pipelines兼容。因此,我们可以通过System.Buffers.ArrayPool来利用默认管道提供的高性能缓冲池。通常,重用缓冲区可以减轻垃圾收集器的压力。避免装箱只要有可能,不要通过将值类型转换为引用类型来装箱它们。这是常见的建议,但在API设计中需要考虑一些因素。在Hagar中,可以接受值类型的接口和方法定义是通用的,因此它们可以专门用于精确类型并避免装箱/拆箱成本。结果,没有热路径装箱。装箱在某些情况下仍然存在,例如异常方法的字符串格式化。可以通过对参数显式调用.tostring()来删除那些特定的装箱分配。减少闭包分配分配一次闭包并存储结果,以便可以多次重复使用。例如,通常将委托传递给ConcurrentDictionary.getoradd。与其将委托写成内联lambda,不如将其定义为类中的私有字段。下面是来自Hagar中可选ISerializable支持包的一个示例:privatereadonlyFunc>createConstructorDelegate;publicObjectSerializer(SerializationConstructorFactoryconstructorFactory){//Otherparameters/statementsomitted.this.createConstructorDelegate=constructorFactory.GetSerializationConstructorDelegate;}//稍后,onahotcodepath:varconstructor=this.constructors.GetOrAdd(info.ObjectType,this.createConstructorDelegate);最小化重复.NETCore2.0和2.1以及最近的C#版本在消除数据重复过程方面取得了相当大的进步。最著名的是Spans,但在参数修饰符和只读结构中也值得一提。Span是一堆ref结构,没有分配在托管堆上。使用Span可以避免数组分配并避免数据复制。一个Span代表任意内存的一个相邻区域。Span实例通常用于保存数组元素或数组的一部分。对于.NETCore,Spans对于性能优化非常重要,它们使用优化的表示来减小它们的大小,这需要为内部指针添加垃圾收集器支持。内部指针是对数组范围的托管引用,而不仅仅是对第一个元素的引用,因此需要一个包含数组偏移量的附加字段。有关Span的更多信息,请单击此处以供参考。Hagar广泛使用Span,因为它允许我们创建可用于更大缓冲区的分段视图。通过ref传递结构以最小化堆栈上的副本Hagar使用两个主要结构,Reader和Writer。这些结构包含几个字段,几乎在每次调用时都会传递到序列化或反序列化调用路径。在没有干预的情况下,使用这些结构进行的每个方法调用都会产生很大的影响,因为每次调用都需要将整个结构复制到堆栈上。我们可以通过将这些结构作为ref参数传递来避免复制。另外,C#还支持使用refthis作为扩展方法的目标,非常方便。据我所知,没有办法确保特定结构类型始终由ref传递,如果您不小心从调用的参数列表中省略了ref,这可能会导致运行时错误。避免防御性复制(defensivecopy)Roslyn有时需要做一些工作来保证一些语言不变性,当结构存储在只读字段中时,编译器会插入一些指令来避免复制该字段,然后再将其包含在任何操作中那可以保证它不会被修改。通常这意味着调用在结构类型本身上定义的方法,因为将结构作为参数传递给在另一种类型上定义的方法已经需要将结构复制到堆栈上(除非通过ref或in传递)。如果将7.2添加到csproj文件,则可以将结构定义为只读(这是C#7.2的语言功能),并且可以避免保护性副本。如果您不能将其定义为只读结构,有时最好在其他不可变的结构字段上省略readonly修饰符。以JonSkeet的NodaTime库为例。在此示例中,Jon将大多数结构设置为只读,以便可以将readonly修饰符添加到包含这些结构的字段中,而不会对性能产生负面影响。减少分支和分支预测错误现代CPU依赖于通过并发处理的长指令流水线。这涉及CPU分析指令以确定哪些指令不依赖于先前的指令,并且涉及猜测将采用哪些条件跳转语句。为此,CPU使用称为分支预测器的组件,它负责猜测将采用哪个分支。它通常通过读取和写入表中的条目来执行此操作,并根据上次执行条件跳转时发生的情况修改其预测。当预测正确时,这个过程将会加速。否则,需要将预测分支的指令清空,重新获取正确分支的指令进入流水线继续执行。所以加快这个过程最好的方法就是减少分支和分支误预测,首先尽量减少分支的数量,如果不能消除分支,就尽量降低误预测率,这可能涉及到使用排序数据或重构代码,可以使用查找的方法代替分支预测。其他杂项提示1.避免使用LINQ,LINQ在应用程序代码中非常有用,但很少用于库/框架代码中的路径。LINQ很难进行JIT优化(IEnumerable..)并且倾向于分配很多。2.使用具体类型而不是接口或抽象类型,也许最常见的是,如果您正在遍历列表,最好不要首先将列表转换为IEnumerable(例如,通过使用LINQ或将其作为IEnumerable参数传递给方法).这样做的原因是使用foreach枚举列表使用的是非分配的List.Enumerator结构,但是在转换为IEnumerable时,foreach必须将该结构装箱为IEnumerator。3.反射在库代码中特别有用,要缓存反射结果,可以考虑使用IL或Roslyn为访问器生成委托,或者最好使用现有的库,例如Microsoft.Extensions.ObjectMethodExecutor.Sources、Microsoft.Extensions。属性助手。来源或FastMember。特定于库的优化优化生成的代码Hagar使用Roslyn为要序列化的POCO生成C#代码,此C#代码在编译时包含在您的项目中。我们可以对生成的代码进行一些优化以加快速度。通过跳过已知类型的编解码器查找来避免虚拟调用当复杂对象包含众所周知的字段(如int、Guid、string)时,代码生成器将直接插入对这些类型的手动编码编解码器的调用,而不是调用CodecProvider来检索一个该类型的IFieldCodec实例。这允许JIT内联这些调用,并避免虚拟/接口间接。在运行时特化泛型与上面类似,代码生成器可以生成在运行时使用特化的代码。预先计算常量值以消除一些分支在序列化过程中,每个字段都带有一个标头,通常是一个字节。它会告诉解串器哪个字段被编码。该字段头包含3条信息:字段的规范(固定宽度、长度前缀、标记分隔、引用等),用于多态性的字段模式类型(预期的、众所周知的、先前定义的、编码的),最后3位专门用于编码字段ID(如果它小于7)。在许多情况下,可以在编译时确切地知道这个头字节是什么。如果该字段具有值类型,那么我们知道运行时类型永远不会与字段类型不同,并且字段ID始终是已知的。因此,我们通常可以省去计算头部值所需的所有工作,可以直接将其作为常量嵌入到代码中。这样就节省了分支,而且常常省去了很多中间语言代码。选择合适的数据结构通过切换到结构数组,索引和维护集合的成本在很大程度上被消除,参考跟踪不再出现在基准测试中。这有一个缺点,即对于大型对象图,这种新方法可能很慢。选择正确的算法Hagar花费大量时间对可变长度整数进行编码/解码。这种方法称为varints。varint是一种序列化具有一个或多个字节的整数以减少有效负载大小的方法。.许多二进制序列化器使用这种技术,包括ProtocolBuffers。甚至.NET的BinaryWriter也使用这种编码。这是引用中的一个片段:protectedvoidWrite7BitEncodedInt(intvalue){//Writeoutanint7bitsatatime.Thehighbitofthebyte,//whenon,tellsreadertocontinuereadingmorebytes.uintv=(uint)value;//supportnegativenumberswhile(v>=0x80){Write((byte)(v|0x80));v>>=7;}Write((byte)v);}我想指出,ZigZag编码对于包含负值的有符号整数可能更有效,而不是转换为uint。这些序列化程序中的变量使用一种称为LittleEndianBase-128或LEB128的算法,该算法每字节最多编码7位。它使用每个字节的最高有效位来指示是否有另一个字节跟随(1=是,0=否)。这是一种简单的格式,但可能不是最快的。但是PrefixVarint更快,使用PrefixVarint时,LEB128中的所有1都一次性写入有效负载的开头。这可能允许我们使用硬件内在函数来提高这种编码和解码的速度。通过向前移动大小信息,我们还可以一次从有效负载中读取更多字节,从而减少内部压力并提高性能。