前言

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,你会有收获的。

杂谈WebApiClient的性能优化的更多相关文章

  1. Oracle SQL性能优化技巧大总结

    http://wenku.baidu.com/link?url=liS0_3fAyX2uXF5MAEQxMOj3YIY4UCcQM4gPfPzHfFcHBXuJTE8rANrwu6GXwdzbmvdV ...

  2. 01.SQLServer性能优化之----强大的文件组----分盘存储

    汇总篇:http://www.cnblogs.com/dunitian/p/4822808.html#tsql 文章内容皆自己的理解,如有不足之处欢迎指正~谢谢 前天有学弟问逆天:“逆天,有没有一种方 ...

  3. 03.SQLServer性能优化之---存储优化系列

    汇总篇:http://www.cnblogs.com/dunitian/p/4822808.html#tsql 概  述:http://www.cnblogs.com/dunitian/p/60413 ...

  4. Web性能优化:What? Why? How?

    为什么要提升web性能? Web性能黄金准则:只有10%~20%的最终用户响应时间花在了下载html文档上,其余的80%~90%时间花在了下载页面组件上. web性能对于用户体验有及其重要的影响,根据 ...

  5. Web性能优化:图片优化

    程序员都是懒孩子,想直接看自动优化的点:传送门 我自己的Blog:http://cabbit.me/web-image-optimization/ HTTP Archieve有个统计,图片内容已经占到 ...

  6. C#中那些[举手之劳]的性能优化

    隔了很久没写东西了,主要是最近比较忙,更主要的是最近比较懒...... 其实这篇很早就想写了 工作和生活中经常可以看到一些程序猿,写代码的时候只关注代码的逻辑性,而不考虑运行效率 其实这对大多数程序猿 ...

  7. JavaScript性能优化

    如今主流浏览器都在比拼JavaScript引擎的执行速度,但最终都会达到一个理论极限,即无限接近编译后程序执行速度. 这种情况下决定程序速度的另一个重要因素就是代码本身. 在这里我们会分门别类的介绍J ...

  8. 02.SQLServer性能优化之---牛逼的OSQL----大数据导入

    汇总篇:http://www.cnblogs.com/dunitian/p/4822808.html#tsql 上一篇:01.SQLServer性能优化之----强大的文件组----分盘存储 http ...

  9. C++ 应用程序性能优化

    C++ 应用程序性能优化 eryar@163.com 1. Introduction 对于几何造型内核OpenCASCADE,由于会涉及到大量的数值算法,如矩阵相关计算,微积分,Newton迭代法解方 ...

随机推荐

  1. document.documentElement.scrollTop指定位置失效解决办法

    近期在vue的H5项目中,做指定位置定位的时候发现使用document.documentElement.scrollTop一直不生效. 解决办法是document.documentElement.sc ...

  2. 在Jetson TX2上安装OpenCV(3.4.0)

    参考文章:How to Install OpenCV (3.4.0) on Jetson TX2 与参考文章大部分都是相似的,如果不习惯看英文,可以看看我下面的描述 在我们使用python3进行编程时 ...

  3. RobotFrameWork 自动化环境搭建(基于 python2.7)

    1.自动化工具安装顺序 robot Framework(两个RF框架) WXpython(不要更改安装路径,自动安装在python文件中) 安装依赖库 RF3.0 和 RF1.5.2.1 打开 rid ...

  4. python的unittest框架中的assert断言

    unittest框架自带断言,如果想用assert断言,一定要引入unittest.TestCase框架才行,不然不会自动识别assert断言

  5. A. Powered Addition(二进制性质-思维)

    \(拿样例来看1 7 6 5\) \(6成长到7是最合理的,因为1s就可以实现而且对于后面来说最优\) \(5成长到7是最合理的,因为2s就可以实现而且对于后面最优\) \(发现了什么?二进制是可以组 ...

  6. 李婷华 201771010113 《面向对象程序设计(java)》 第三周学习总结

    第一部分:理论知识回顾 第一章 再次了解了java“白皮书”的关键术语,java的常见术语,对于“白皮书”的关键术语依然环游不太理解的. 第二章 在使用命令行工具运行java程序的过程中,依然会有环境 ...

  7. QML文字灰飞烟灭效果

    QML文字灰飞烟灭效果 1,目的 实现文字化作一缕青烟随风而逝的效果. 2,设计分析 在前面的章节中讲述了如何化作光斑碎片逐渐消失的效果,我们可以借鉴它将光斑换成烟雾,再加入端流产生微风浮动,加上字幕 ...

  8. CSS躬行记(10)——CSS方法论

    方法论是一个哲学术语,会对一系列具体的方法进行分析研究.系统总结并最终提出较为一般性的原则.CSS方法论是一种面向CSS.由个人和组织设计.已被诸多项目检验且公认有效的最佳实践.这些方法论都会涉及结构 ...

  9. 为什么PUSH推送要经常背锅?

    前言 只有光头才能变强. 文本已收录至我的GitHub精选文章,欢迎Star:https://github.com/ZhongFuCheng3y/3y 自从做了推送以后,每隔一段时间就发现有各大的公司 ...

  10. 关于redis,你需要了解的几点!

    一.关于 redis key: 1.是二进制安全的,也就是说,你可以使用任何形式的二进制序列来作为key,比如一个string,或者一个jpg图片的数据,需要说明的是,空字符串也是一个有效的key. ...