作者 | 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:留给中国队的时间不多了

2 行代码,将 .NET 执行时间降低 87%!相关推荐

  1. 2 行代码,将 .NET 执行时间降低 87%!(附代码)

    授权自AI科技大本营(ID:rgznai100) 本文约3200字,建议阅读6分钟 本文介绍了性能优化通过简单的修改也能在提升性能上有出色的表现. 以下为译文: 长期以来,我一直在致力于提高性能,并且 ...

  2. 万万想不到 10行代码搞定一个决策树

    01决策树模拟实验 文章目录 01决策树模拟实验 要求 决策树简单介绍 搭建环境 产生数据集 划分训练集和测试集 生成决策树 Cross-Validation法 可视化决策树 10行代码搞定决策树 要 ...

  3. 并行算法设计与性能优化 刘文志 第4章 串行代码性能优化

    一方面,串行代码优化有时能获得成千上万倍的加速:另一方面因为单个并行控制流的内部依旧是串行的. 一般而言,不同算法上的优化是最有效的.假设你已经有了一个能得到正确结果的程序,需要在此基础上进行优化,本 ...

  4. 对信用卡欺诈 Say No|百行代码实现简化版实时欺诈检测

    进入互联网时代,你的绝大部分操作都可以在网上进行,极大的方便了我们的生活.但是信用卡盗刷者也可以利用网络来诈骗,典型的做法是:诈骗者首先入侵安全级别较低系统来盗窃信用卡卡号,用盗得的信用卡进行很小额度 ...

  5. 把三千行代码重构为15行

    2019独角兽企业重金招聘Python工程师标准>>> 如果你认为这是一个标题党,那么我真诚的恳请你耐心的把文章的第一部分读完,然后再下结论.如果你认为能够戳中您的G点,那么请随手点 ...

  6. 10行代码实现目标检测,请收下这份教程

     翻译 | 林椿眄 编辑 | 阿司匹林 出品 | AI科技大本营(公众号ID:rgznai100) 作为人工智能的一个重要领域,计算机视觉是一门可以识别并理解图像和场景的计算机及软件系统科学.该领 ...

  7. 鱼佬:百行代码入手数据挖掘赛!

    ↑↑↑关注后"星标"Datawhale 每日干货 & 每月组队学习,不错过 Datawhale干货 作者:鱼佬,武汉大学,Datawhale成员 本实践以科大讯飞xData ...

  8. 【精简教程版】100行代码入手天池CV赛事

    Datawhale 作者:阿水.陈信达  Datawhale成员 本文针对阿里天池<零基础入门CV赛事-街景字符编码识别>,给出了百行代码Baseline,帮助cv学习者更好地结合赛事实践 ...

  9. 精简教程版 | 100行代码入手天池CV赛事

    来源:Datawhale 本文长度为7200字,建议阅读10分钟 本文从数据分析和解题思路分析两方面对阿里赛题进行了详细解读. 本文针对阿里天池<零基础入门CV赛事-街景字符编码识别>,给 ...

最新文章

  1. 三.Hystrix资源隔离
  2. HBase HFile与Prefix Compression内部实现全解–KeyValue格式
  3. shiro+php,一套基于SpringBoot+Vue+Shiro 前后端分离 开发的代码生成器
  4. 杂志订阅管理系统c++_电池管理系统BMS功能安全开发流程详解
  5. 区间数多属性决策matlab,区间数多属性决策的改进理想解法
  6. python星号*在函数中、传参时的含义
  7. php获取最后一条记录的id,获取MySQL的表中每个userid最后一条记录的方法_MySQL
  8. 泰牛php第10期百度云,泰牛程序员 韩顺平 2017年 MyBatis
  9. ubuntu固定内网ip_Ubuntu14设置局域网固定IP
  10. react报错:Uncaught Error: Element type is invalid: expected a string (for built-in components) or a ..
  11. 什么是云桌面?云桌面的三大基本架构组成部分
  12. WINDOWS10 win+L 锁屏快捷键失效
  13. 《图像处理、分析与机器视觉 第四版》 摄像机 相机概述——学习笔记
  14. 消防工程师 8.2 防排烟系统-防烟
  15. Aseprite动画技巧
  16. cmd命令netstat -ano不是内部命令解决方案
  17. android 7.1 白屏,苹果iphone7手机白屏怎么回事 iphone7白屏不能关不了机的快速解决办法...
  18. nginx源码的安装与磁盘分区
  19. 解析:未来物联网发展的十大趋势
  20. 通过小宝的卡牌游戏,看开源SCUT服务器运行使用

热门文章

  1. 跨域资源共享 CORS
  2. 以太坊代币空投合约的实现
  3. Apache Spark 2.2.0 中文文档 翻译活动
  4. 配置Activiti Explorer使用MYSQL
  5. js变量以及其作用域详解
  6. 爱上MVC3系列~开发一个站点地图(俗称面包屑)
  7. javah生成JNI头文件
  8. osi 模型 tcpip网络模型
  9. android id 重名_Android App 自定义权限重名不能安装解决办法
  10. ubuntu安装qwt出现错误时"mkdir: 无法创建目录“/usr/local/qwt-6.1.3“: 权限不够"