作者:Shendu.CC
連結:http://www.cnblogs.com/dacc123/p/10644816.html
前言
最近做了一個過濾程式碼塊功能的介面。就是獲取一些部落格文章做文字處理,然後這些部落格文章的程式碼塊太多了,很多重覆的程式碼關鍵詞如果被拿過來處理,那麼會對文字的特徵表示已經特徵選擇會有很大的影響。
所以需要將這些程式碼塊的部分給過濾掉。過濾起來很簡單,就是找程式碼塊的html 標記,然後將html標記之間的內容給刪除就可以了。
程式碼塊的html標記一般都是
我使用了String,Regex,StringBuilder,Span這些不同的方法來實現這個功能,利用BenchMarks比較它們之間的效能差距。
BenchMarks
要對比不同程式碼之間的效能差距,還是不用StopWatch來計算消耗時間,這樣簡單的方法,而是使用BenchMarksDotNet包:一個專業的.net core下測試程式效能的工具包。
BenchMarksDotNet的github地址:https://github.com/stevejgordon/BenchmarkAndSpanExample/tree/Benchmarks
這裡簡短介紹下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();
}
}
執行dotnet build -c Release 然後 dotnet yourproject.dll 就可以看見BenchMarks測試效果.
鋪墊好東西,現在開始進入正題。
使用 string
首先,直接用string 操作。由於測試博文可能會比較長,會有比較多的程式碼塊。所以我的思路是,while(true) 去尋找程式碼塊標記,並使用string 的定址: indexOf() , 拼接:+= 和 剪下:Substring() 完成程式碼塊的過濾。過程也很簡單。
這隻是解決問題的一種方法,這篇文章的目的不是尋找最優解決方法,而是比較發現使用不同的 “工具” 之間的巨大效能差距。
private static string _startTag = "
;
private static string _endTag = "
“;
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>
<div class=”cnblogs_code“>
<pre>IndexWriterConfig.setRAMBufferSizeMB” +
“(<span style=”color: #0000ff;”>doublespan><span style=”color: #000000;”>);” +
”
IndexWriterConfig.setMaxBufferedDocs(span><span style=”color: #0000ff;”>intspan><span ” +
“style=”color: #000000;”>);
IndexWriterConfig.setMergePolicy(MergePolicy)span>pre>
div>
<p>” +
“setRAMBufferSizeMB() 是設定”;
[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
{
/// A byref or a native ptr.
internal readonly ByReference _pointer;
/// The number of elements this Span contains.
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(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 是 Span< char > 的只讀型別。
使用Slice 代替SubString 。上述程式碼我依然傳回的是 string。為了得到 string,我不惜使用Span< T > 的ToString() 函式,在我印象中,這個操作會把Span 的優勢給拉回起跑線。
接下來看測試結果:
真是大吃一驚,平均消耗時間,居然少了 48000 納秒,Span< T > 只是 string 的不到百分之一消耗。記憶體消耗減少了一半
Span< T >果然名不虛傳,正如前面所說的SubString 和Slice 之間的效能差距。
Span< T > 的特色
雖然Span< T > 的效能十分出色 ,但是 string 有太多完善的介面,string 是為了簡化你的程式碼讓你更加舒服的使用字串,所以犧牲了效能。
因此 在對計算機消耗要求十分的嚴苛的情況下,嘗試使用Span< T > ,大多數情況下,簡短的string 已經能滿足需求。我的認知下的Span< T >的特色:
-
Span< T >的定義方法多種多樣,可以直接 ( i ) 像定義陣列那樣 : Span a = new int[10]; ( ii ) 在建構式中直接傳入 陣列(指標+長度)Span a = new Span(T[]),Span a = new Span(void*,length) ; ( iii )可以直接在棧中分配記憶體:Span 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("(
)(.| )*?(
)”, 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很優秀,但是為瞭解決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的引數。接下來看看武裝到牙齒的Span效能如何。
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的驚人表現。同時 .NET Core 在Span加入之後,各個地方都有效能的提升,比如說Regex。 真是讓開發者何其幸哉。
在Regex中的原始碼,我看到了一個ValueStringBuilder一個內部的結構體,只能在System/Text 的內部中使用。
它是一個結構體!它的建構式可以直接傳入Span,我將它copy出來,代替StringBuilder , 時間消耗不分伯仲,但是記憶體消耗又減少了一半!。
這應該是極致的效能表現。鑒於篇幅原因就不展開了。
可以在看到ValueStringBuilder
(https://github.com/SilentCC/MyTestBenchMarks)以及完整的程式碼。
朋友會在“發現-看一看”看到你“在看”的內容