前言

最近做了一个过滤代码块功能的接口。就是获取一些博客文章做文本处理,然后这些博客文章的代码块太多了,很多重复的代码关键词如果被拿过来处理,那么会对文本的特征表示已经特征选择会有很大的影响。所以需要将这些代码块的部分给过滤掉。过滤起来很简单,就是找代码块的html 标记,然后将html标记之间的内容给删除就可以了。代码块的html标记一般都是<pre></pre>

我使用了String,Regex,StringBuilder,Span<T>这些不同的方法来实现这个功能,利用BenchMarks比较它们之间的性能差距。

BenchMarks

要对比不同代码之间的性能差距,还是不用StopWatch来计算消耗时间,这样简单的方法,而是使用BenchMarksDotNet包:一个专业的.net core下测试程序性能的工具包。

BenchMarksDotNetgithub地址

这里简短介绍下BenchMarksDotNet的使用:

首先新建一个需要测试的类:FilterCodeBlocks ,并在类中写上被测试的方法:FilterCodeBlockByString

 public class FilterCodeBlocks
{ public string FilterCodeBlockByString(string content)
{
return content;
}
}

然后新建一个类: FilterCodeBlocksBenchMark

using System;
using System.IO;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Order; namespace QuickSortBenchMarks
{
[RankColumn]
[Orderer(SummaryOrderPolicy.FastestToSlowest)]
[MemoryDiagnoser]
public class FilterCodeBlocksBenchmarks
{
FilterCodeBlocks FilterCodeBlocks = new FilterCodeBlocks();
[Benchmark]
public void FilterByString()
{ FilterCodeBlocks.FilterCodeBlockByString(s);
}
}
}

最后在入口Progam.cs中 写上

    class Program
{
static void Main(string[] args)
{
var summary = BenchmarkRunner.Run<FilterCodeBlocksBenchmarks>();
}
}

执行dotnet build -c Release 然后 dotnet yourproject.dll 就可以看见BenchMarks测试效果.

铺垫好东西,现在开始进入正题。

使用 string

首先,直接用string 操作。由于测试博文可能会比较长,会有比较多的代码块。所以我的思路是,while(true) 去寻找代码块标记,并使用string 的寻址: indexOf() , 拼接:+= 和 剪切:Substring() 完成代码块的过滤。过程也很简单。 这只是解决问题的一种方法,这篇文章的目的不是寻找最优解决方法,而是比较发现使用不同的 "工具" 之间的巨大性能差距。

        private static string _startTag = "<pre";
private static string _endTag = "</pre>"; private static int _startTagLength => _startTag.Length;
private static int _endTagLength => _endTag.Length;
public FilterCodeBlocks()
{ } public string FilterCodeBlockByString(string content)
{
string result = "";
while (true)
{
var startPos = content.IndexOf(_startTag, StringComparison.CurrentCulture);
if (startPos == -1)
break; var content2 = content.Substring(startPos + _startTagLength, content.Length - startPos - _startTagLength);
var endPos = content2.IndexOf(_endTag, StringComparison.CurrentCulture);
result += content.Substring(0, startPos);
content = content2.Substring(endPos + _endTagLength, content2.Length - endPos - _endTagLength);
}
result += content;
return result;
}

一开始选取了比较短的文本进行测试 ,可以直接写在程序中:

[RankColumn]
[Orderer(SummaryOrderPolicy.FastestToSlowest)]
[MemoryDiagnoser]
public class FilterCodeBlocksBenchmarks
{
FilterCodeBlocks FilterCodeBlocks = new FilterCodeBlocks();
public static string s = "<p>我们通过IndexWriterConfig 可以设置IndexWriter的属性," +
"已达到我们希望构建索引的需求,这里举一些属性,这些属性可以影响到IndexWriter写入索引的速度:" +
"</p>\n<div class=\"cnblogs_code\">\n<pre>IndexWriterConfig.setRAMBufferSizeMB" +
"(<span style=\"color: #0000ff;\">double</span><span style=\"color: #000000;\">);" +
"\nIndexWriterConfig.setMaxBufferedDocs(</span><span style=\"color: #0000ff;\">int</span><span " +
"style=\"color: #000000;\">);\nIndexWriterConfig.setMergePolicy(MergePolicy)</span></pre>\n</div>\n<p>" +
"setRAMBufferSizeMB()&nbsp;是设置";
[Benchmark]
public void FilterByString()
{ FilterCodeBlocks.FilterCodeBlockByString(s);
}
}

按照上述的方法,运行dll 得出 使用string 相关方法的性能。

平均处理时间 48微秒 分配内存 1.41kb,看来效果也是不错的,我感觉上面的代码中方法也是大家都会经常使用的方法。

接下来 .NET Core 2.1的新特性: Span 隆重登场!

Span< T >

What is a Span< T >?

Span< T > : 结构体值类型 。相当于C++ 中的指针,它是一段连续内存的引用,也就是一段连续内存的首地址。有了Span< T >,我们就可以不在unsafe的代码块中写指针了。Span< char > 相对于 string 也就具有很大的性能优势。

举个栗子: string.Substring() 函数,实际上是在堆中额外创建了一个新的 string 对象,把字符 copy 过去,再返回这个对象的引用。而相对应的 Span< T > 的Slice() 函数则是直接在内存中返回子串的首地址引用,此过过程几乎不分配内存,并且十分高效。

后面的优化也是使用Span< T > 的Slice() 代替了 string 的SubString()

简单看下 Span< T > 的源码,就可以窥见 Span< T > 的奥秘:

 public readonly ref partial struct Span<T>
{
/// <summary>A byref or a native ptr.</summary>
internal readonly ByReference<T> _pointer;
/// <summary>The number of elements this Span contains.</summary> private readonly int _length; .... public Span(T[] array)
{
if (array == null)
{
this = default;
return; // returns default
}
if (default(T) == null && array.GetType() != typeof(T[]))
ThrowHelper.ThrowArrayTypeMismatchException(); _pointer = new ByReference<T>(ref Unsafe.As<byte, T>(ref array.GetRawSzArrayData()));
_length = array.Length;
}
}

Span< T > 内部主要就是一个ByReference< T > 类型的对象,实际上就是ref T: 一个类型的引用,它和C 的int* char* 如出一折。 Span < T > 也就是建立 ref 的基础上。

限定长度: _length ,就像 C 中定义指针,在使用前需要 malloc 或者 alloc 分配固定长度的内存。关于Span< T > 更多详细知识:

https://msdn.microsoft.com/en-us/magazine/mt814808.aspx

使用 Span< T > 优化

将上述 string 代码使用 Span< char > 优化一下

public string FilterCodeBlockBySpanAndToString(ReadOnlySpan<char> content)
{
string result = "";
ReadOnlySpan<char> contentSpan2 = new ReadOnlySpan<char>();
int startPos = 0;
int endPos = 0; ReadOnlySpan<char> startTagSpan = _startTag.AsSpan();
ReadOnlySpan<char> endTagSpan = _endTag.AsSpan();
while (true)
{
startPos = content.IndexOf(startTagSpan);
if (startPos == -1)
break; contentSpan2 = content.Slice(startPos + _startTagLength, content.Length - startPos - _startTagLength);
endPos = contentSpan2.IndexOf(endTagSpan);
result += content.Slice(0, startPos).ToString();
content = contentSpan2.Slice(endPos + _endTagLength, contentSpan2.Length - endPos - _endTagLength);
}
result += content.ToString();
return result; }

这里 ReadOnlySpan<char> 是 Span< char > 的只读类型。使用Slice 代替SubString 。上述代码我依然返回的是 string。为了得到 string,我不惜使用Span< T > 的ToString() 函数,在我印象中,这个操作会把Span 的优势给拉回起跑线。

接下来看测试结果:

真是大吃一惊,平均消耗时间,居然少了 48000 纳秒,Span< T > 只是 string 的不到百分之一消耗。内存消耗减少了一半

Span< T >果然名不虚传,正如前面所说的SubStringSlice 之间的性能差距。

Span< T > 的特色

虽然Span< T > 的性能十分出色 ,但是 string 有太多完善的接口,string 是为了简化你的代码让你更加舒服的使用字符串,所以牺牲了性能。因此 在对计算机消耗要求十分的严苛的情况下,尝试使用Span< T > ,大多数情况下,简短的string 已经能满足需求。我的认知下的Span< T >的特色:

  • Span< T >的定义方法多种多样,可以直接 ( i ) 像定义数组那样 : Span<int> a = new int[10]; ( ii ) 在构造函数中直接传入 数组(指针+长度)Span<T> a = new Span<T>(T[]),Span<T> a = new Span<T>(void*,length) ; ( iii )可以直接在栈中分配内存:Span<char> a = stackalloc char[10]; 在C# 8.0中才可以,这样的写法真是高大上。

  • Span< T > 只能存在于栈中,而不能放在堆中。因为 ( i ) GC 在堆中很难跟踪这些指针, ( ii ) 在堆中会出现多线程, 如果两个线程的两个Span< T >指向了同一个地址,那就糟了。

  • 可以使用 Memory< T > 代替 Span< T >在堆中使用。

  • 所有 string 的接口都可以用 Span< char > 来实现,这似乎又回到了原始的C语言时代。

  • Span < T > 有个兄弟叫 ReadOnlySpan< T > 。

到这里还不能结束Span< T >的性能评测。因为在大量字符串处理中还有个隐藏的实力派:正则表达式 Regex

正则表达式

如果我们使用正则表达式呢,它的性能会是如何呢?

正则表达式的实现:

  private static Regex _codeTag = new Regex("(<pre(.*?)>)(.|\n)*?(</pre>)", RegexOptions.Compiled);
public string FilterCodeBlocByRegex(string content)
{
return _codeTag.Replace(content, string.Empty);
}

真是简短的让人看着就舒服。正则表达式的长处是在大文本处理,所以我决定直接将字符串变成100篇博客的内容加在一起。下面就是测试结果:

Incredible! 正则表达式 真的是一匹黑马,直逼Span< T >,时间消耗仅为10.68ms,内存消耗只有7.69MB。难得的是它的内存消耗也比Span< T >低。

为什么Regex会有这么好的表现呢?翻阅一下源码,原来如此!

private static string Replace(MatchEvaluator evaluator, Regex regex, string input, int count, int startat)
{
....
Span<char> charInitSpan = stackalloc char[ReplaceBufferSize];
var vsb = new ValueStringBuilder(charInitSpan);
}

.net core 2.2 中,Regex的 Replace 内部用了 Span< char > 重新实现。看来,正则表达式的高性能表现 和 Span 不无关系。

根据园友的评论,Regex 以前的版本,也是通过指针来进行操作,我也实验了 .net standard的Regex , 二者效率差不多。

Span < T > 很优秀,但是为了解决 string 的性能问题,C# 早早就有了 StringBuilder 。于是我让了字符串处理界的大师:StringBuilder, 来助 Span< T > 一臂之力。

StringBuilder + Span< T >

 public string FilterCodeBlockBySpanAndStringBuilder(ReadOnlySpan<char> content)
{
var result = new StringBuilder(content.Length); var contentSpan2 = new ReadOnlySpan<char>();
var startPos = 0;
var endPos = 0; var startTagSpan = _startTag.AsSpan();
var endTagSpan = _endTag.AsSpan();
while (true)
{
startPos = content.IndexOf(startTagSpan);
if (startPos == -1)
break; contentSpan2 = content.Slice(startPos + _startTagLength, content.Length - startPos - _startTagLength);
endPos = contentSpan2.IndexOf(endTagSpan);
result.Append(content.Slice(0, startPos));
content = contentSpan2.Slice(endPos + _endTagLength, contentSpan2.Length - endPos - _endTagLength);
}
result.Append(content);
return result.ToString(); }

将原先的 字符串拼接变成了 StringBuilder 的 append函数,而且减少了我心心念念的ToString()次数。在 .net core 2.2 中StringBuilder的内部也有 Span< T >的身影。

Append 函数可以直接接受Span< T >的参数。接下来看看武装到牙齿的Span< T >性能如何。

unbelievable ! 使用 StringBuilder 的Span< T >时间消耗居然只有 867.1微妙,内存消耗只有1.7MB ,在各个方面都技压群雄。又是百分之一的消耗。

实际上 StringBuilder的内部操作字符串的 是一个 char 数组,它的 Apend 的性能如此之高,还是因为内部使用了指针。

 			unsafe
{
fixed (char* valuePtr = value)
fixed (char* destPtr = &chunkChars[chunkLength])
{
string.wstrcpy(destPtr, valuePtr, valueLen);
}
}

StringBuilder 只能支持字符串,但是Span< T >可是泛型的哦。不过,程序中最消耗CPU的大都是一些字符串的处理。

结语

在实际中体验了Span< T >的惊人表现。同时 .NET Core 在Span< T >加入之后,各个地方都有性能的提升,比如说Regex。 真是让开发者何其幸哉。

在Regex 中的源代码,我看到了一个 ValueStringBuilder 一个内部的结构体,只能在System/Text 的内部中使用。它是一个结构体!它的构造函数可以直接传入 Span< char >,我将它 copy 出来,代替StringBuilder , 时间消耗不分伯仲,但是内存消耗又减少了一半!。这应该是极致的性能表现。鉴于篇幅原因就不展开了。

可以在 这里 看到ValueStringBuilder,以及完整的代码。

实际体验Span<T> 的惊人表现的更多相关文章

  1. shell学习四十天----awk的惊人表现

    awk的惊人表现 awk能够胜任差点儿全部的文本处理工作.     awk 调用 1.调用awk: 方式一:命令行方式 awk [-F field-separator ] 'commands' inp ...

  2. html基础知识笔记

    HTML基础 1.1HTML文件的基本结构和W3C标准 1.1.1HTML简介 HTML是一种描述网页的语言,一种超文本标记的语言! 1.1.2HTML文件的基本结构 头部(head) 头部是网页的标 ...

  3. 2050年这些职业将逐渐被AI(人工智能)取代

    耳熟能详的人工智能   深蓝Deep Blue是美国IBM公司生产的一台超级国际象棋电脑,重1270公斤,有32个大脑(微处理器),每秒钟可以计算2亿步."深蓝”输入了一百多年来优秀棋手的对 ...

  4. 手机打车APP的机遇与挑战

    所谓打车APP,就是个能安装在手机上的打车软件.原理是通过GPS进行定位,能够搜索附近的空车信息然后反馈给用户.同样的,空车信息也会反馈给用户.一般这种啊APP都是跟地图类软件一起的.比如百度地图,谷 ...

  5. 020_CSS3

    目录 如何学习CSS 什么是CSS 发展史 快速入门 css的优势 三种CSS导入方式 拓展:外部样式两种写法 选择器 基本选择器 层次选择器 结构伪类选择器 属性选择器 美化网页元素 为什么要美化网 ...

  6. CSS快速入门基础篇,让你快速上手(附带代码案例)

    1.什么是CSS 学习思路 CSS是什么 怎么去用CSS(快速上手) CSS选择器(难点也是重点) 网页美化(文字,阴影,超链接,列表,渐变等) 盒子模型 浮动 定位 网页动画(特效效果) 项目格式: ...

  7. CSS学习Day01

    1.什么是CSS 如何学习 CSS是什么 CSS怎么用 CSS选择器(重点+难点) 美化网页(文字.阴影.超链接.列表.渐变) 盒子模型 浮动 定位 网页动画(特效效果) 1.1什么是CSS Casc ...

  8. CSS样式快速入门

    CSS样式快速入门 前言 前端基础的博客主要分为HTML.CSS和JavaScript,本类博客主要用于记录博主的学习过程和分享学习经验,由于博主学识浅薄,经验不足,难免会出现错误,欢迎大家提出问题. ...

  9. 2_CSS

    1. 什么是CSS 1.1 什么是CSS Cascading Style Sheet 层叠样式表 是一种用来表现HTML(标准通用标记语言的一个应用)或XML(标准通用标记语言的一个子集)等文件样式的 ...

随机推荐

  1. python读取excel(xlrd)

     一.安装xlrd模块: 1.mac下打开终端输入命令: pip install xlrd 2.验证安装是否成功: 在mac终端输入 python  进入python环境 然后输入 import xl ...

  2. linux 安装nvm,通过nvm安装node

    1,nvm git地址点击打开链接,安装命令 curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.11/install.sh ...

  3. ubuntu intel网卡驱动安装(华硕B250F GAMING主板 )

    jikexianfeng@jikexianfeng:~$ sudo sudo lspci -knn :]: Intel Corporation Device [:591f] (rev ) Subsys ...

  4. puppeteer:官方出品的chrome浏览器自动化测试工具

    puppeteer发布应该有一段时间了,这两天正好基于该工具写了一些自动化解决方案,在这里抛砖引给大家介绍一下. 官方描述: Puppeteer is a Node library which pro ...

  5. amqp笔记

    1.exchange message的生产者可以将消息发送给exchange,然后由exchange路由到不同的queue中. exchange有4种类型: direct exchange:msg只会 ...

  6. ffmpeg中AVOption的实现分析

    [时间:2017-10] [状态:Open] [关键词:ffmpeg,avutil,AVOption] 0 引言 AVOptions提供了一种通用的options机制,可以用于任意特定结构的对象. 本 ...

  7. Pandas基础(十一)时间序列

    1. pandas时间序列:时间索引 2. pandas时间序列数据结构 2.1 定期序列 3. 频率和偏移 4. 重采样,转移,加窗口 4.1 重采样及频率转换 4.2 时间移动 4.3 滚动窗口 ...

  8. Safari 3D transform变换z-index层级渲染异常的研究

    by zhangxinxu from http://www.zhangxinxu.com/wordpress/?p=5569 一.Safari是新时代的IE6 在2年前介绍currentColor变量 ...

  9. 2.基础(Foundations)

    chain rule: Bayes' rule: 其他内容就是一些基本的概率论的概念(联合分布,边际分布等)和图的一些概念(节点,边,路径,向上闭包等)

  10. linux系统搜索文件中关键字的位置

    grep -Irn “ubuntu”-------即查出文件的目录路径