云博开户:杂谈WebApiClient的性能优化

admin/2020-05-27/ 分类:科技/阅读:

前言

WebApiClient的netcoreapp版本的开发已靠近尾声,最后的进攻偏向是性能的压榨,我把我所做性能优化的历程先容给人人,人人可以依葫芦画瓢,应用到自己的现实项目中,提高程序的性能。

总体功效展示

使用MockResponseHandler消除真实http请求,原生HttpClient、WebApiClientCore和Refit的性能参考:

BenchmarkDotNet=v0.12.1, OS=Windows 10.0.18362.836 (1903/May2019Update/19H1) Intel Core i3-4150 CPU 3.50GHz (Haswell), 1 CPU, 4 logical and 2 physical cores .NET Core SDK=3.1.202 [Host] : .NET Core 3.1.4 (CoreCLR 4.700.20.20201, CoreFX 4.700.20.22101), X64 RyuJIT DefaultJob : .NET Core 3.1.4 (CoreCLR 4.700.20.20201, CoreFX 4.700.20.22101), X64 RyuJIT 
Method Mean Error StdDev
HttpClient_GetAsync 3.945 μs 0.2050 μs 0.5850 μs
WebApiClientCore_GetAsync 13.320 μs 0.2604 μs 0.3199 μs
Refit_GetAsync 43.503 μs 0.8489 μs 1.0426 μs
Method Mean Error StdDev
HttpClient_PostAsync 4.876 μs 0.0972 μs 0.2092 μs
WebApiClientCore_PostAsync 14.018 μs 0.1829 μs 0.2246 μs
Refit_PostAsync 46.512 μs 0.7885 μs 0.7376 μs

优化之后的WebApiClientCore,性能靠近原生HttpClient,并领先于Refit。

Benchmark历程

性能基准测试可以辅助我们对照多个方式的性能,在没有性能基准测试工具的情况下,我们仅凭肉眼若何区分性能的转变。

BenchmarkDotNet是一款强力的.NET性能基准测试库,其为每个被测试的方式提供了伶仃的环境,使用BenchmarkDotnet,我们很容易的编写种种性能测试方式,并可以制止许多常见的坑。

请求总时间对比

拿到BenchmarkDotNet,我就迫在眉睫地写了WebApiClient的老版本、原生HttpClient和WebApiClientCore三个请求对比,看看新的Core版本有没有预期的性能有所提高,以及他们与原生HttpClient有若干性能消耗。

Method Mean Error StdDev
WebApiClient_GetAsync 279.479 us 22.5466 us 64.3268 us
WebApiClientCore_GetAsync 25.298 us 0.4953 us 0.7999 us
HttpClient_GetAsync 2.849 us 0.0568 us 0.1393 us
WebApiClient_PostAsync 25.942 us 0.3817 us 0.3188 us
WebApiClientCore_PostAsync 13.462 us 0.2551 us 0.6258 us
HttpClient_PostAsync 4.515 us 0.0866 us 0.0926 us

粗略地看了一下效果,我开怀一笑,Core版本比原版本性能好一倍,且靠近原生。
细看让我大吃一惊,老版本的Get请求怎么这么慢,想想可能是老版本使用Json.net,之前吃过Json.net频仍建立ContractResolver性能急剧下降的亏,就算是单例ContractResolver第一次建立也很占用时间。以是改善为在对比之前,做一次请求预热,这样对照靠近现实使用场景,预热之后的老版本WebApiClient,Get请求从279us降低到39us

WebApiClientCore的Get与Post对比

从上面的数据来看,WebApiClientCore在Get请求时显著落后于其Post请求,我的接口是如下界说的:

public interface IWebApiClientCoreApi { [HttpGet("/benchmarks/{id}")] Task<Model> GetAsyc([PathQuery]string id); [HttpPost("/benchmarks")] Task<Model> PostAsync([JsonContent] Model model); } 

Get只需要处置参数id,做为请求uri,而Post需要json序列化model为json,证实代码内里的处置参数的[PathQuery]特征性能低下,[PathQuery]依赖于UriEditor工具类,执行流程为先实验Replace(),不乐成则挪用AddQUery(),UriEditor的原型如下:

class UriEditor { public bool Replace(string name, string? value); public void AddQuery(string name, string? value); } 

考虑到请求uri为[HttpGet("/benchmarks/{id}")],这里流程上是不会挪用到AddQuery()方式的,以是锁定性能低的方式就是Replace()方式,接下来就是想办法革新Replace方式了,下面为革新前的Replace()实现:

/// <summary> /// 替换带有花括号的参数的值 /// </summary> /// <param name="name">参数名称,不带花括号</param> /// <param name="value">参数的值</param> /// <returns>替换乐成则返回true</returns> public bool Replace(string name, string? value) { if (this.Uri.OriginalString.Contains('{') == false) { return false; } var replaced = false; var regex = new Regex($"{{{name}}}", RegexOptions.IgnoreCase); var url = regex.Replace(this.Uri.OriginalString, m => { replaced = true; return HttpUtility.UrlEncode(value, this.Encoding); }); if (replaced == true) { this.Uri = new Uri(url); } return replaced; } 

Repace的改善方案性能对比

在上面代码中,有点履历一眼就知道是Regex拖的后腿,由于营业需要不区分大小写的字符串替换,而现成中能用的,有且仅有Regex能用了,Regex有两种使用方式,一种是建立Regex实例,一种是使用Regex的静态方式。

Regex实例与静态方式
Method Mean Error StdDev
ReplaceByRegexStatic 480.9 ns 5.50 ns 5.15 ns
ReplaceByRegexNew 2,615.8 ns 41.33 ns 36.63 ns

这一跑就知道缘故原由了,把new Regex替换为静态的Regex挪用,性能马上提高5倍!

Regex静态方式与自实现Replace函数

感受Regex静态方式的性能还不是很高,自己实现一个Replace函数对比试试,万一比Regex静态方式还更快呢。于是我花一个晚上的时间写了这个Replace函数,对,就是整整一个晚上,来为它做性能测试,为它做单元测试,为它做内存分配优化。

/// <summary> /// 不区分大小写替换字符串 /// </summary> /// <param name="str"></param> /// <param name="oldValue">原始值</param> /// <param name="newValue">新值</param> /// <param name="replacedString">替换后的字符中</param> /// <exception cref="ArgumentNullException"></exception> /// <returns></returns> public static bool RepaceIgnoreCase(this string str, string oldValue, string? newValue, out string replacedString) { if (string.IsNullOrEmpty(str) == true) { replacedString = str; return false; } if (string.IsNullOrEmpty(oldValue) == true) { throw new ArgumentNullException(nameof(oldValue)); } var strSpan = str.AsSpan(); using var owner = ArrayPool.Rent<char>(strSpan.Length); var strLowerSpan = owner.Array.AsSpan(); var length = strSpan.ToLowerInvariant(strLowerSpan); strLowerSpan = strLowerSpan.Slice(0, length); var oldValueLowerSpan = oldValue.ToLowerInvariant().AsSpan(); var newValueSpan = newValue.AsSpan(); var replaced = false; using var writer = new BufferWriter<char>(strSpan.Length); while (strLowerSpan.Length > 0) { var index = strLowerSpan.IndexOf(oldValueLowerSpan); if (index > -1) { // 左边未替换的 var left = strSpan.Slice(0, index); writer.Write(left); // 替换的值 writer.Write(newValueSpan); // 切割长度 var sliceLength = index oldValueLowerSpan.Length; // 原始值与小写值同步切割 strSpan = strSpan.Slice(sliceLength); strLowerSpan = strLowerSpan.Slice(sliceLength); replaced = true; } else { // 替换过剩下的原始值 if (replaced == true) { writer.Write(strSpan); } // 再也无匹配替换值,退出 break; } } replacedString = replaced ? writer.GetWrittenSpan().ToString() : str; return replaced; } 

这代码不算长,但为它写了好多个Buffers相关类型,以是总体工作量很大。不外总算写好了,来个长一点文本的Benchmark:

public class Benchmark : IBenchmark { private readonly string str = "WebApiClientCore.Benchmarks.StringReplaces.WebApiClientCore"; private readonly string pattern = "core"; private readonly string replacement = "CORE"; [Benchmark] public void ReplaceByRegexNew() { new Regex(pattern, RegexOptions.IgnoreCase).Replace(str, replacement); } [Benchmark] public void ReplaceByRegexStatic() { Regex.Replace(str, pattern, replacement, RegexOptions.IgnoreCase); } [Benchmark] public void ReplaceByCutomSpan() { str.RepaceIgnoreCase(pattern, replacement, out var _); } } 
Method Mean Error StdDev Median
ReplaceByRegexNew 3,323.7 ns 115.82 ns 326.66 ns 3,223.4 ns
ReplaceByRegexStatic 881.9 ns 16.79 ns 43.94 ns 868.3 ns
ReplaceByCutomSpan 524.0 ns 4.78 ns 4.47 ns 524.9 ns

大动干戈一个晚上,没若干提高,收支不成正比啊。

与Refit对比

在自家里和老哥哥比没意思,以是想跳出来和功效异常相似的Refit做对照看看,在对照之前,我是很有信心的。为了公正,两者都使用默认设置,都举行预热,使用相同的接口界说:

设置与预热

public abstract class BenChmark : IBenchmark { protected IServiceProvider ServiceProvider { get; } public BenChmark() { var services = new ServiceCollection(); services .AddHttpClient(typeof(HttpClient).FullName) .AddHttpMessageHandler(() => new MockResponseHandler()); services .AddHttpApi<IWebApiClientCoreApi>() .AddHttpMessageHandler(() => new MockResponseHandler()) .ConfigureHttpClient(c => c.BaseAddress = new Uri("http://webapiclient.com/")); services .AddRefitClient<IRefitApi>() .AddHttpMessageHandler(() => new MockResponseHandler()) .ConfigureHttpClient(c => c.BaseAddress = new Uri("http://webapiclient.com/")); this.ServiceProvider = services.BuildServiceProvider(); this.PreheatAsync().Wait(); } private async Task PreheatAsync() { using var scope = this.ServiceProvider.CreateScope(); var core = scope.ServiceProvider.GetService<IWebApiClientCoreApi>(); var refit = scope.ServiceProvider.GetService<IRefitApi>(); await core.GetAsyc("id"); await core.PostAsync(new Model { }); await refit.GetAsyc("id"); await refit.PostAsync(new Model { }); } } 

等同的接口界说

public interface IRefitApi { [Get("/benchmarks/{id}")] Task<Model> GetAsyc(string id); [Post("/benchmarks")] Task<Model> PostAsync(Model model); } public interface IWebApiClientCoreApi { [HttpGet("/benchmarks/{id}")] Task<Model> GetAsyc(string id); [HttpPost("/benchmarks")] Task<Model> PostAsync([JsonContent] Model model); } 

测试函数

/// <summary> /// 跳过真实的http请求环节的模拟Get请求 /// </summary> public class GetBenchmark : BenChmark { /// <summary> /// 使用原生HttpClient请求 /// </summary> /// <returns></returns> [Benchmark] public async Task<Model> HttpClient_GetAsync() { using var scope = this.ServiceProvider.CreateScope(); var httpClient = scope.ServiceProvider.GetRequiredService<IHttpClientFactory>().CreateClient(typeof(HttpClient).FullName); var id = "id"; var request = new HttpRequestMessage(HttpMethod.Get, $"http://webapiclient.com/{id}"); var response = await httpClient.SendAsync(request); var json = await response.Content.ReadAsByteArrayAsync(); return JsonSerializer.Deserialize<Model>(json); } /// <summary> /// 使用WebApiClientCore请求 /// </summary> /// <returns></returns> [Benchmark] public async Task<Model> WebApiClientCore_GetAsync() { using var scope = this.ServiceProvider.CreateScope(); var banchmarkApi = scope.ServiceProvider.GetRequiredService<IWebApiClientCoreApi>(); return await banchmarkApi.GetAsyc(id: "id"); } /// <summary> /// Refit的Get请求 /// </summary> /// <returns></returns> [Benchmark] public async Task<Model> Refit_GetAsync() { using var scope = this.ServiceProvider.CreateScope(); var banchmarkApi = scope.ServiceProvider.GetRequiredService<IRefitApi>(); return await banchmarkApi.GetAsyc(id: "id"); } } 

测试效果

去掉物理网络请求时间段,WebApiClient的性能是Refit的3倍,我终于可以放心的睡个好觉了!

总结

这文章写得对照乱,是真实的纪录我在做性能调优的历程,现实上的历程中,走过的大大小小弯路还更乱,要是写下来文章就没法看了,有需要性能调优的同伙,不防跑一跑banchmark,你会有收获的。

,

Sunbet

www.ipvps.cn信誉来自于每一位客户的口碑,Sunbet携手江苏安腾科技有限公将致力服务好每位Sunbet会员。!

TAG:
阅读:
广告 330*360
广告 330*360
Sunbet_进入申博sunbet官网
微信二维码扫一扫
关注微信公众号
新闻自媒体 Copyright © 2002-2019 Sunbet 版权所有
二维码
意见反馈 二维码