2 行代码,将 .NET 执行时间降低 87%!
作者 | STEVE GORDON
译者 | 弯月,责编 | 屠敏
头图 | CSDN 下载自东方 IC
出品 | CSDN(ID:CSDNnews)
以下为译文:
长期以来,我一直在致力于提高性能,并且努力避免在关键代码路径中进行内存分配。例如,使用Span<T>在解析数据时避免内存分配,以及使用ArrayPool避免为临时缓冲区分配数组。这样的修改虽然对性能有好处,但会增加新版本代码的维护难度。
在本文中,我想展示的性能优化并不需要大量复杂的代码修改。有时候,有些简单的修改也能在提升性能上有出色的表现。下面我们就来看一个这样的例子。
找出优化的对象
最近,我在研究Elasticsearch.NET客户端代码库。我对库中某些热路径的性能感到好奇。
给应用程序性能分析方面的新手解释一下,热路径就是在正常的使用过程中被频繁调用的一系列方法。例如,Web应用程序中可能有一个端点,与所有其他端点相比,该端点在生产环境中被调用的频率更高。那么,该端点对应的方法很可能是应用程序中热路径的开始。相应地,它调用的各种方法也可能位于热路径上。再举一个例子,循环内的代码,如果循环执行数百或数千次,则可能会对其他方法产生大量调用。
在优化应用程序性能时,通常首先应该关注热路径,由于被调用的频率很高,因此对它们做出的改进能够给性能带来最显著的影响。改进调用次数仅占10%的代码,产生的收益也要小得多。
.NET有两个相关的Elasticsearch客户端。NEST是支持强类型查询的高级客户端,位于底层客户端Elasticsearch.NET之上。
NEST命名空间内有一个抽象的RequestBase类,该类派生出的子类都是强类型的请求类型。每个可以用的Elasticsearch HTTP API端点都有一个强类型的请求类。请求的主要特征是它包含与其相关的API端点的一个或多个URL。
定义多个URL的原因是,许多ElasticSearch的API都可以使用基本路径或包含特定资源标识符的路径进行调用。例如,Elasticsearch中有一个端点可以查询集群运行状况。该端点可以通过URL“_cluster/health”执行整个集群的一般健康检查;也可以在路径中加入索引名称“_cluster/health/{索引}”来针对特定索引执行健康检查。
在逻辑上,这些URL由库中的同一个请求类处理。在创建请求时,消费者可以提供一个可选的请求值,以指定特定索引。在这种情况下,必须在运行时构建URL,通过用户提供的索引名称替换URL中的{索引}部分。如果请求没有提供索引名称,则使用较短的URL “_cluster/health”。
因此,在请求被发送的时候,最终的URL必须已经确定并且构建好了。首先从可能的URL列表中找出要使用的URL模式。这个过程需要使用强类型请求对象指定的请求值。在URL模式匹配完成后,就可以生成最终的URL了。必要时还可以使用带有标记的URL模式,利用调用者代码提供的路由值替换可选的标记,从而创建最终的URL字符串。
该URL构建的核心主要包含在UrlLookup类中,该类包括一个ToUrl方法,如下所示:
public string ToUrl(ResolvedRouteValuesvalues)
{var sb = new StringBuilder(_length);var i = 0;for (var index = 0; index < _tokenized.Length; index++){var t = _tokenized[index];if (t[0] == '@'){if (values.TryGetValue(_parts[i],out var v)){if (string.IsNullOrEmpty(v))throw newException($"'{_parts[i]}' defined but is empty on url: {_route}");sb.Append(Uri.EscapeDataString(v));}else throw new Exception($"Novalue provided for '{_parts[i]}' on url: {_route}");i++;}else sb.Append(t);}return sb.ToString();
}
上述代码首先创建了StringBuilder实例。然后,遍历带有标记的URL中的每个字符串。URL路径中的标记元素存储在字符串数组字段“_tokenized”中。在每次迭代中,如果字符串值以“@”字符开头,则表明需要用相应的值替换它。然后搜索路由的值,找出与当前标记名称匹配的值,保存在“_parts”数组中。如果找到匹配项,则在对URI进行转义后将其值附加到URL StringBuilder中(第15行)。
对于不需要替换路径中的任何部分,则无需修改即可将它们直接附加到StringBuilder上(第21行)。
当所有带有标记的值都被添加并替换之后,就可以调用StringBuilder的ToString方法,返回最终的字符串。每次客户端发送请求时,这段代码都会被调用,因此是库中的热路径。
下面我们来考虑:如何对其进行优化,以提高执行速度,并减少资源分配?
现在这段代码使用的是StringBuilder,这是良好的实践,在需要将补丁数量的字符串连接到一起时,可以避免字符串分配。有几种使用Span<T>的方法可以减少字符串分配的次数。但是,添加Span<T>或其他技巧(如利用ArrayPools提供零分配缓冲区),会增加代码复杂度。由于这个库被许多调用者使用,因此这种做法也许值得。
在日常的编程工作中,除非你的服务处于极端的使用/负载状态,否则这种优化可能有点过。如果你熟悉Span<T>之类的高性能技巧,那么可能会情不自禁朝着最佳优化(即零分配)努力。这样的想法会让你对应该优先考虑的简单改动视而不见。
当回顾ToUrl方法并通过逻辑流程进行思考时,我有了一个想法。对于某些情况,可以有另外两种方法,实现简单但能有效地提升性能。再看一下上面的代码,你能否找到简单的提升性能的改进?提示:只需在方法开头加上几行。
让我们再次考虑集群健康的示例,它有两个URL模式,“ _cluster/health”和“ _cluster/health/{index}”。
后者要求路径的最后一部分使用用户提供的索引名称替换。但是前者并没有任何替换的要求。对于绝大多数端点来说,只有一小部分情况需要使用路由的值替换路径中的一部分。明白我的意思了吗?
我的想法是,某些情况下ToUrl方法完全不需要构建URL。这样就根本不需要使用(更不需要内存分配)StringBuilder示例,也不需要生成新的URL字符串。既然URL不需要替换,那么其中就只包含完整的原始URL路径字符串。那么,直接返回就可以了。
优化代码
在进行任何优化之前,我需要先做两件事。首先,我需要检查现有代码是否有足够的单元测试。任何重构都有可能破坏当前的行为。如果没有测试,我就会先根据目前的行为编写一些测试。在优化之后,如果测试依然能够通过,就说明没有破坏任何东西。为了简洁起见,本文将省略测试,相信许多开发人员都已经非常熟悉了。
优化之前需要做的第二件事就是,在已有代码上建立评测基准,这样之后就可以确定代码改动是否能够提升性能,并定量地测量性能的提升。对性能做出假设是危险的,最安全的做法就是用科学的方法来确保。首先建立理论,测量已有的行为,然后进行试验(代码优化),最终再次测量,以验证假设。编写性能测试脚本的方法也许你并不熟悉,你可以参考我关于.NET性能测试的文章(https://www.stevejgordon.co.uk/introduction-to-benchmarking-csharp-code-with-benchmark-dot-net)。
在此ToUrl示例中,基准测试非常直观。
namespace BenchmarksDev
{internal class Program =>private static void Main(string[] args) =>BenchmarkRunner.Run<UrlLookupBenchmarks>();[MemoryDiagnoser]public class UrlLookupBenchmarks{private static readonly UrlLookup ClusterHealth = newUrlLookup("_cluster/health");private static readonly UrlLookup ClusterHealthIndex = newUrlLookup("_cluster/health/{index}");private static readonly ResolvedRouteValues EmptyRouteValues = newResolvedRouteValues();private static readonly ResolvedRouteValues IndexRouteValue = newResolvedRouteValues(){{ "index", "a"}};private string _url;[Benchmark]public void Health() => _url = ClusterHealth.ToUrl(EmptyRouteValues);[Benchmark]public void HealthIndex() => _url =ClusterHealthIndex.ToUrl(IndexRouteValue);}
}
其中一些静态字段用于设置性能测试的类型,以及需要的输入。我们不希望测量性能的测试产生额外的开销。接下来是两个性能测试,分别用于两个URL模式。我们希望优化那个不需要替换路由值的模式,但也有必要对另一种情况进行测试。我们不希望在改进一个的同时对另一个产生负面影响。
更改任何代码之前,首次运行的结果如下:
| Method | Mean | Error | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated |
|------------|---------:|---------:|---------:|-------:|------:|------:|----------:|
| Health | 41.60 ns | 0.637 ns | 0.596 ns | 0.0381 | - | - | 160 B |
| HealthIndex | 85.60 ns | 0.851 ns |0.796 ns | 0.0457 | - | - | 192 B |
这为我们提供了一个基准,供我们完成工作后进行比较。
在ToUrl方法中,我们希望在不需要进行替换时,略过根据路径构建URL的过程。只需要添加两行代码即可实现。
if (values.Count == 0 &&_tokenized.Length == 1 && _tokenized[0][0] != '@')return _tokenized[0];
只需要在方法开头添加这两行(如果你喜欢在return语句周围添加大括号,那么就添加4行)。这段代码执行三个逻辑检查。如果它们都返回true,我们就知道不需要任何替换,可以直接返回。第一个检查可以确保用户没有提供路由值。如果用户提供了路由值,就应该假设需要进行某种替换。接下来我们检查标记的数字是否包含一个元素,以及该元素的首字母不是“@”字符。
标准的集群健康检查请求不会提供索引名称,那么这些条件就会满足,可以直接从标记数组的0号位置返回“_cluster/health”字符串。
这些额外的代码并不复杂。大多数开发人员都可以顺利阅读并理解其目的。为了完整起见,我们还可以将所有条件重构成一个小的方法或局部函数,这样就可以给它起一个名字,让代码不言自明。本文省略这些内容。
现在代码修改完了,而且单元测试仍然能够通过,下面我们重新运行基准测试来比较一下结果。
| Method | Mean | Error | StdDev | Gen 0 | Gen 1 | Gen 2 |Allocated |
|------------|----------:|----------:|----------:|-------:|------:|------:|----------:|
| Health | 5.352 ns | 0.0611 ns |0.0510 ns | - | - | - | - |
| HealthIndex | 84.470 ns | 0.5005 ns |0.4437 ns | 0.0457 | - | - | 192 B |
第二个性能测试“HealthIndex”没有发生任何变化,因为部分URL需要替换,所以像以前一样整个方法都会执行。但是,第一个性能测试“Health”中更直接的情况改进了许多。该代码路径上不再有任何分配,因此减少了100%!我们不再分配StringBuilder,也不创建新字符串,而是直接返回原始字符串,在这里,原始字符串的内存已经分配过了。
节省160个字节似乎并没有太让人兴奋,但是考虑到客户端每发送一个请求这段代码都会调用一次,因此节省的量非常可观。10个请求(不需要替换的请求)就可以节省1Kb无用的内存分配。如果客户非常频繁地使用Elasticsearch,这个改进就非常值得。
执行时间也减少了87%,因为在这种情况下唯一需要执行的代码就是条件检查和返回。这些改进在热路径上非常成功,对于所有调用该方法的人都有益。由于这是一个客户端库,所以客户也会看到好处,只需要使用包含此优化的最新版客户端即可。
总结
在本文中,我们介绍了并非所有性能优化都需要复杂的实现。在文中的示例中,我们通过条件检查避免执行需要分配内存的代码,从而优化了NEST库的ToUrl方法。尽管可以使用Span<T>从理论上进行一些更广泛的优化,但我们优先考虑了可以快速获得性能提升的方法,这不会带来复杂性,也不会加重维护代码的负担。为了确保示例中的代码改动确实可以提升性能,我们使用了基准来衡量代码变更前后的效果。尽管例子中没有介绍,但我们应该运行单元测试,以避免在这个方法中引入回归问题。
希望通过这个示例,你可以在自己的代码中找出只需简单的修改就能快速提升性能的地方。在寻求值得优化的代码时,请优先考虑热路径,并从简单的地方开始,尝试解决能快速提升性能的问题,然后再转向更复杂的优化。对于大多数代码库来说,类似于本文的某些修改应该是合理的,而更高级的优化可能会加重维护的负担。就像本文的示例一样,某些优化工作可能非常简单,只需使用条件检查避免某些代码的执行即可。
原文:https://www.stevejgordon.co.uk/dotnet-performance-optimisations-dont-have-to-be-complex
作者:STEVE GORDON,微软MVP。
本文为 CSDN 翻译,转载请注明来源出处。
更多精彩推荐
自拍卡通化,拯救动画师,StyleGAN再次玩出新花样
秋天的第一杯奶茶该买哪家?Python 爬取美团网红奶茶店告诉你
Azure Arc 正式商用、Power Platform+GitHub 世纪牵手,一文看懂 Ignite 2020
起底 ARM:留给中国队的时间不多了
相关文章:

Ansible基础一Playbook(二)
摘自:http://www.ansible.com.cn/docs/playbooks_intro.htmlHandlers: 在发生改变时执行的操作(当发生改动时)’notify’ actions 会在 playbook 的每一个 task 结束时被触发,而且即使有多个不同的 task 通知改动的发生, ‘notify’ actions …

sendmail服务器案例配置
Linux/UNIX下的老牌邮件服务器。Sendmail作为一种免费的邮件服务器软件,已被广泛的应用于各种服务器中,它在稳定性、可移植性、及确保没有bug等方面具有一定的特色,且可以在网络中搜索到大量的使用资料。 实验拓扑图: 注ÿ…

网页制作的中的一些工具代码
1. οncοntextmenu"window.event.returnvaluefalse" 将彻底屏蔽鼠标右键 <table border οncοntextmenureturn(false)><td>no</table> 可用于Table2. <body onselectstart"return false"> 取消选取、防止复制3. οnpaste"r…
神经网络其实和人一样懒惰,喜欢走捷径......
作者 | Jrn-Henrik Jacobsen, Robert Geirhos, Claudio Michaelis,深度学习研究专家译者 | Arvin,责编 | 夕颜出品 | CSDN(ID:CSDNnews)以下为译文:人工智能会很快取代放射科医生吗?最近,研究人…

nodejs获取ASP.Net WebAPI(IIS Windows验证)
处理了很多天,终于使用Nodejs可以发出请求至WebAPI,能够正常处理数据了 首先加入npm包 npm install httpntlm 在app.js中加入代码 var httpntlm require(httpntlm); var fs require(fs);var options {url: http://get001.mygroup.com/InstantNoodle_S…

ubb代码转化html代码
ubb代码转化html代码 public static string UbbDecode(string str){str HtmlEncode(str);str Regex.Replace(str, "/[url](?<url>.?)/[/url]", "<a href${url} target_blank>${url}</a>", RegexOptions.Compiled | RegexOptions.Ig…

如何定位EXC_BAD_ACCESS错误 (info malloc-history)
在 iphone 开发中使用内存时,我们经常会遇到 EXC_BAD_ACCESS 的错误。 出现这个错误的原因是我们访问了一个已经被释放掉的对象,如: implementation FeedbackViewController - (void)viewDidLoad {[super viewDidLoad];_scrollView [[UIScro…
一周内咸鱼疯转2.4W次,最终被所有大厂封杀!
(含答案)所有面试资料及技术好文精选文档都整理到网盘了。Java面试官手册需要加微信免费领取Java面试官手册需要加微信免费领取长按扫码或搜索微信号:gh16670101550,免费领取

java 基础知识三 java变量
1、作用域 {} 包围起来的代码称之为代码块,在块中声明的变量只能在块中使用 2、常量 就是固定不变的量,一旦被定义,它的值就不能再被改变。 3、变量 变量必须在程序中被定义(或声明)后才能使用,而且为每个变…
打通语言理论和统计NLP,Transformers/GNNs架构能做到吗?
作者 | Chaitanya K. Joshi译者 | 苏本如,责编 | 夕颜来源 | CSDN(ID:CSDNnews)我的工程师朋友经常问我:图深度学习听起来很棒,但是有没有实际应用呢?虽然图神经网络被用于Pinterest、阿里巴巴和推特的推荐…

艰辛的面向对象
为什么80%的码农都做不了架构师?>>> 所有的操作系统都不是面向对象的。 所有的操作系统都是基于函数的。ANDROID框架里面的好多类也是基于函数的。很多都是静态的方法。这个框架包括两个部分:一是JAVA部分,一是本地类。本地类不…

计算机网络第一课
1.IPv4与IPv6的区别是什么?在windows 7以上系统中,在设置本地IP地址的时候经常会看到同事含有IPV4协议项与IPV6协议项,并不同于以往windows xp系统中仅有TCP/IP协议项,不少朋友都觉得比较奇怪,询问编辑IPv4与IPv6的区别…

常用函数集农历函数
常用函数集农历函数原来是vb代码,重新整理为VB.NET版的,并在VS2003中编译通过Imports System.MathPublic Class UCnCalendarPrivate Structure SolarHolidayStructDim Month As IntegerDim Day As IntegerDim Recess As IntegerDim HolidayName As Strin…
微软发布代码智能新基准数据集CodeXGLUE,多角度衡量模型优劣
来源 | 微软研究院AI头条编者按:代码智能(code intelligence)目的是让计算机具备理解和生成代码的能力,并利用编程语言知识和上下文进行推理,支持代码检索、补全、翻译、纠错、问答等场景。以深度学习为代表的人工智能…

Spring从菜鸟到高手(四)(上)使用JdbcTemplate类实现用户登陆验证、批量更新
标签:Spring java JdbcTemplate Spring从菜鸟到高手 绝缘材料原创作品,允许转载,转载时请务必以超链接形式标明文章 原始出处 、作者信息和本声明。否则将追究法律责任。http://tonyaction.blog.51cto.com/227462/42042看了我前面几篇文章的朋…

CSS盒模型及边距问题
盒模型是CSS的基石之一,页面的每一个元素都被看作一个矩形框,分别由外边距,边框,内边距,内容组成, 在CSS中,width和height的值指的是内容的宽高,增加外边距,边框…

区分C语言中getch、getche、fgetc、getc、getchar、fgets、gets
首先,这两个函数不是C标准库中的函数, int getch(void) //从标准输入读入一个字符,当你用键盘输入的时候,屏幕不显示你所输入的字符。也就是,不带回显。 int getche(void) //从标准输入读入一个字符&…
无限想象空间,用Python玩转3D人体姿态估计
前言姿态估计,一直是近几年的研究热点。它就是根据画面,捕捉人体的运动姿态,比如 2D 姿态估计:再比如 3D 姿态估计:看着好玩,那这玩应有啥用呢?自动驾驶,大家应该都不陌生࿰…

Mac中将delete键定义为删除键
在Mac中,delete键实际是退格键(Backspace),fndelete才是删除键。这也是从Windows转到Mac时不习惯的地方之一。 通过安装DoubleCommand软件可以解决这个问题。安装后,在System Preferences中找到DoubleCommand找打开在E…

CHIL-SQL-MIN() 函数
MIN() 函数 MIN 函数返回一列中的最小值。NULL 值不包括在计算中。 SQL MIN() 语法 SELECT MIN(column_name) FROM table_name 注释:MIN 和 MAX 也可用于文本列,以获得按字母顺序排列的最高或最低值。 SQL MIN() 实例 我们拥有下面这个 "Orders&quo…
Google排名第一的语言,引数十万人关注:搞定它,技术大牛都甘拜下风
毋庸置疑,Python越来越被认可为程序员新时代的风口语言。无论是刚入门的程序员,还是年薪百万的 BATJ 的大牛都无可否认:Python的应用能力是成为一名码农大神的必要项。 所以,很多程序员把Python当做第一语言来学习。 但对于Python…

CSS滤镜详解
CSS滤镜详解 简介〓 设置文字透明层次,模糊效果,给文字加光晕等这些本来要靠图片才能处理的效果,现在CSS可以既简单又快速的把它实现了……接着往下看就知道了。 〓正文〓 语法:STYLE"filter:filtername(fparameter1, fpa…

php实现单链表
<?php //单链表的存储结构 class Node{ public $data;//数据域 public $next;//指针域 指向下一个结点 function __construct(){ $this->data null; $this->next null; } } //单链表数据类型 class LinkList{ public $data; public $next; function _…

2017-2-23 C#基础 中间变量
用中间变量做这个题 1、“请输入年份:”(1-9999) “请输入月份:”(1-12) “请输入日期:”(要判断大小月,判断闰年) 判断输入的时间日期是否正确 2、计算输入的…

HTA的简单应用
HTA简介:HTA是HTML Application的缩写(HTML应用程序),是软件开发的新概念,直接将HTML保存成HTA的格式,就是一个独立的应用软件,与VB、C等程序语言所设计的软件没什么差别。下面是一个HTA的例子&…
300亿美元,AMD为什么要买Xilinx?
作者 | Just来源 | CSDN(ID:CSDNnews)自2015年5月,Intel(英特尔)以167亿美元收购FPGA生产商Altera后,半导体行业接连传出大整合。上个月,NVIDIA(英伟达)宣布以400亿美元收购芯片设计公司Arm&…

PIM-SSM简介
源特定组播(SSM:Source Specific Multicast)是一种区别于传统组播的新的业务模型,它使用组播组地址和组播源地址同时来标识一个组播会话,而不是向传统的组播服务那样只使用组播组地址来标识一个组播会话。SSM保留了传统PIM-SM模式中的主机显示…

MyBatis开发入门二:一对多连表查询
1. 步骤: (1). 加包(2). 编写db.properties;编写conf.xml,将db.properties加入到conf.xml;引入别名(3). 建立实体类(4). 编写sql操作对应的***Mapper.xml文件(5). 将sql操作对应的***Mapper.xml文件注册到conf.xml文件中(6). 编写…

ASP.NET里的事务处理
出自: http://blog.csdn.net/ycl111/ 事务是一组组合成逻辑工作单元的数据库操作,虽然系统中可能会出错,但事务将控制和维护每个数据库的一致性和完整性。如果在事务过程中没有遇到错误,事务中的所有修改都将永久成为数据库的一部…

JAVA的正则表达式语法
Java 正则表达式表达式意义:1.字符x 字符 x。例如a表示字符a\\ 反斜线字符。在书写时要写为\\\\。(注意:因为java在第一次解析时,把\\\\解析成正则表达式\\,在第二次解析时再解析为\,所以凡是不是1.1列举到的转义…