WebApiClient的netcoreapp版本的開發已接近尾聲,最後的進攻方向是性能的壓榨,我把我所作性能優化的過程介紹給你們,你們能夠依葫蘆畫瓢,應用到本身的實際項目中,提升程序的性能。react
使用MockResponseHandler消除真實http請求,原生HttpClient、WebApiClientCore和Refit的性能參考:git
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。github
性能基準測試能夠幫助咱們比較多個方法的性能,在沒有性能基準測試工具的狀況下,咱們僅憑肉眼如何區分性能的變化。web
BenchmarkDotNet是一款強力的.NET性能基準測試庫,其爲每一個被測試的方法提供了孤立的環境,使用BenchmarkDotnet,咱們很容易的編寫各類性能測試方法,並能夠避免許多常見的坑。json
拿到BenchmarkDotNet,我就火燒眉毛地寫了WebApiClient的老版本、原生HttpClient和WebApiClientCore三個請求對比,看看新的Core版本有沒有預期的性能有所提升,以及他們與原生HttpClient有多少性能損耗。api
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請求,個人接口是以下定義的:網絡
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的原型以下:app
class UriEditor { public bool Replace(string name, string? value); public void AddQuery(string name, string? value); }
考慮到請求uri爲[HttpGet("/benchmarks/{id}")]
,這裏流程上是不會調用到AddQuery()方法的,因此鎖定性能低的方法就是Replace()方法,接下來就是想辦法改造Replace方法了,下面爲改造前的Replace()實現:async
/// <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; }
在上面代碼中,有點經驗一眼就知道是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函數,對,就是整整一個晚上,來爲它作性能測試,爲它作單元測試,爲它作內存分配優化。
/// <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作比較看看,在比較以前,我是頗有信心的。爲了公平,二者都使用默認配置,都進行預熱,使用相同的接口定義:
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,你會有收穫的。