授权自AI科技大本营(ID:rgznai100)

本文约3200字,建议阅读6分钟

本文介绍了性能优化通过简单的修改也能在提升性能上有出色的表现。

以下为译文:

长期以来,我一直在致力于提高性能,并且努力避免在关键代码路径中进行内存分配。例如,使用Span<T>在解析数据时避免内存分配,以及使用ArrayPool避免为临时缓冲区分配数组。这样的修改虽然对性能有好处,但会增加新版本代码的维护难度。

在本文中,我想展示的性能优化并不需要大量复杂的代码修改。有时候,有些简单的修改也能在提升性能上有出色的表现。下面我们就来看一个这样的例子。

1、找出优化的对象

最近,我在研究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路径字符串,直接返回就可以了。

2、优化代码

在进行任何优化之前,我需要先做两件事。首先,我需要检查现有代码是否有足够的单元测试。任何重构都有可能破坏当前的行为。如果没有测试,我就会先根据目前的行为编写一些测试。在优化之后,如果测试依然能够通过,就说明没有破坏任何东西。为了简洁起见,本文将省略测试,相信许多开发人员都已经非常熟悉了。

优化之前需要做的第二件事就是,在已有代码上建立评测基准,这样之后就可以确定代码改动是否能够提升性能,并定量地测量性能的提升。对性能做出假设是危险的,最安全的做法就是用科学的方法来确保。首先建立理论,测量已有的行为,然后进行试验(代码优化),最终再次测量,以验证假设。编写性能测试脚本的方法也许你并不熟悉,你可以参考我关于.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%,因为在这种情况下唯一需要执行的代码就是条件检查和返回。这些改进在热路径上非常成功,对于所有调用该方法的人都有益。由于这是一个客户端库,所以客户也会看到好处,只需要使用包含此优化的最新版客户端即可。

3、总结

在本文中,我们介绍了并非所有性能优化都需要复杂的实现,在文中的示例中,我们通过条件检查避免执行需要分配内存的代码,从而优化了NEST库的ToUrl方法。尽管可以使用Span<T>从理论上进行一些更广泛的优化,但我们优先考虑了可以快速获得性能提升的方法,这不会带来复杂性,也不会加重维护代码的负担。为了确保示例中的代码改动确实可以提升性能,我们使用了基准来衡量代码变更前后的效果。尽管例子中没有介绍,但我们应该运行单元测试,以避免在这个方法中引入回归问题。

希望通过这个示例,你可以在自己的代码中找出只需简单的修改就能快速提升性能的地方。在寻求值得优化的代码时,请优先考虑热路径,并从简单的地方开始,尝试解决能快速提升性能的问题,然后再转向更复杂的优化。对于大多数代码库来说,类似于本文的某些修改应该是合理的,而更高级的优化可能会加重维护的负担。就像本文的示例一样,某些优化工作可能非常简单,只需使用条件检查避免某些代码的执行即可。

原文:

https://www.stevejgordon.co.uk/dotnet-performance-optimisations-dont-have-to-be-complex

作者:STEVE GORDON,微软MVP。

本文为 CSDN 翻译,转载请注明来源出处。

编辑:于腾凯

校对:王欣

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

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

    作者 | STEVE GORDON 译者 | 弯月,责编 | 屠敏 头图 | CSDN 下载自东方 IC 出品 | CSDN(ID:CSDNnews) 以下为译文: 长期以来,我一直在致力于提高性能, ...

  2. 独家 | 教你用不到30行的Keras代码编写第一个神经网络(附代码教程)

    翻译:陈丹 校对:和中华 本文长度为3000字,建议阅读5分钟 本文为大家介绍了如何使用Keras来快速实现一个神经网络. 回忆起我第一次接触人工智能的时候,我清楚地记得有些概念看起来是多么令人畏惧. ...

  3. delphi 实现屏幕旋转代码_Cocos Creator模拟射箭效果 | 附代码

    1 获取代码 关注微信公众号,发送[射箭]获取代码 2 效果预览 3 操作方法 点击屏幕,屏幕出现起始位置标志的圆点,不松开手指,滑动屏幕,控制力度和方向,移动距离越大,弓箭拉伸效果越大,松开以后,箭 ...

  4. java结束全部操作代码_Java基本的线程操作(附代码)

    啦啦啦啦,从头整理一遍java并发的内容.开始是基本的线程操作 线程状态切换: 新建线程: @Testpublic voidnewTread(){ Thread t1= new Thread(newR ...

  5. 超强计算机病毒代码,木马编程 之超强服务... 附代码 原创.

    该楼层疑似违规已被系统折叠 隐藏此楼查看此楼 } BOOL AddSvchostGroup(VOID) { HKEY hkey;//其实是一个句柄. if( RegOpenKey(HKEY_LOCAL ...

  6. c语言选择冒泡排序,c语言选择冒泡排序讲解(附代码)

    c语言选择冒泡排序讲解(附代码) c语言选择冒泡排序讲解(附代码) 冒泡排序原理举例: 给定一组数 15 20 1 16 进行从大到小冒泡排序.第一次起泡的第一次比较:用15和20比较,若15比20小 ...

  7. 【译】最大限度地降低多线程 C# 代码的复杂性

    [译]最大限度地降低多线程 C# 代码的复杂性 原文:[译]最大限度地降低多线程 C# 代码的复杂性 分支或多线程编程是编程时最难最对的事情之一.这是由于它们的并行性质所致,即要求采用与使用单线程的线 ...

  8. 100个必会的python脚本-100行Python代码实现自动抢火车票(附源码)

    前言 又要过年了,今年你不妨自己写一段代码来抢回家的火车票,是不是很Cool.下面话不多说了,来一起看看详细的介绍吧. 先准备好: 12306网站用户名和密码 chrome浏览器及下载chromedr ...

  9. Python命令行解析:IDE内点击Run运行代码直接得出结果、基于TF flags(或argparse、sys.argv)在Dos内命令行(一条命令)调用代码文件得出结果

    Python命令行解析:IDE内点击Run运行代码直接得出结果.基于TF flags(或argparse.sys.argv)在Dos内命令行(一条命令)调用代码文件得出结果 目录 命令行解析 T1.采 ...

最新文章

  1. flash绘图API:恋上你的CD
  2. sscanf用法(转)
  3. oracle rac em cluster name,ORACLE 11G RAC重建EM问题
  4. cuisineroyale服务器所在位置,cuisine royale进不去怎么办?原因及解决办法分享
  5. mac 下 ssh被拒绝
  6. 2018 A Sparse Topic Model for Extracting Aspect-Specific Summaries from Online Reviews 稀疏主题模型学习笔记
  7. 阿里巴巴开源离线同步工具 DataX3.0 介绍
  8. 中国开放教育资源协会
  9. SpringMVC控制类的Controller方法返回值
  10. 金山词霸怎样才能在PDF阅读器(ADOBE 7.0/9.0)上取词翻译
  11. 我的世界启动时要Java_我的世界启动时Java出现日志怎么办
  12. 微信公众号迁移公证书好办吗,真实经历!公众平台迁移见证需要什么材料,流程及费用详解
  13. Wps ppt中无法打开超链接外部文件的解决办法。
  14. vscode生成的.BROWSE.VC.DB文件超大问题
  15. cont在c语言用法,在S7-1500中指令TSEND_C and TRCV_C如何使用?
  16. 树莓派-linux内核编译
  17. 用微信怎么定位别人手机位置
  18. R+ECharts2Shiny实现web动态交互式可视化数据(中)
  19. 【测试】linux tc命令|Linux模拟网络延迟、丢包等|traffic control(流量控制)
  20. NumPy库回顾与分享(一)

热门文章

  1. 臻好黄金百香果苗做一个有脑子的程序员
  2. [学习笔记]CDQ分治
  3. webpack + loader 使用笔记
  4. Python-4 两个变量相互交换
  5. zabbix trapper方式监控
  6. 十分钟用Windows服务器简单搭建DHCP中继代理!!
  7. vs2010给c语言文件添加头注释
  8. 关于工作流引擎的设计讨论
  9. 常见面试题:为什么HashMap不是线程安全的呢?(JDK1.7和JDK1.8角度)(看完你就能和面试官笑谈人生了)
  10. tokenizer.encode() 与 tokenizer.tokenize()对比,言简意赅 转 高人讲学