中文字幕在线观看,亚洲а∨天堂久久精品9966,亚洲成a人片在线观看你懂的,亚洲av成人片无码网站,亚洲国产精品无码久久久五月天

.NET程序性能的基本要領(lǐng)

2018-07-20    來源:編程學習網(wǎng)

容器云強勢上線!快速搭建集群,上萬Linux鏡像隨意使用

  Bill Chiles(Roslyn編譯器的程序經(jīng)理)寫了一篇文章《Essential Performance Facts and .NET Framework Tips》,知名博主寒江獨釣對該文進行了摘譯,文中分享了性能優(yōu)化的一些建議和思考,比如不要過早優(yōu)化、好工具很重要、性能的關(guān)鍵,在于內(nèi)存分配等,并指出開發(fā)者不要盲目的沒有根據(jù)的優(yōu)化,首先定位和查找到造成產(chǎn)生性能問題的原因點最重要。

  全文如下:

  本文提供了一些性能優(yōu)化的建議,這些經(jīng)驗來自于使用托管代碼重寫C# 和 VB編譯器,并以編寫C# 編譯器中的一些真實場景作為例子來展示這些優(yōu)化經(jīng)驗。.NET 平臺開發(fā)應(yīng)用程序具有極高的生產(chǎn)力。.NET 平臺上強大安全的編程語言以及豐富的類庫,使得開發(fā)應(yīng)用變得卓有成效。但是能力越大責任越大。我們應(yīng)該使用.NET框架的強大能力,但同時如果我們需要處理大量的數(shù)據(jù)比如文件或者數(shù)據(jù)庫也需要準備對我們的代碼進行調(diào)優(yōu)。

  為什么來自新的編譯器的性能優(yōu)化經(jīng)驗也適用于您的應(yīng)用程序

  微軟使用托管代碼重寫了C#和Visual Basic的編譯器,并提供了一些列新的API來進行代碼建模和分析、開發(fā)編譯工具,使得Visual Studio具有更加豐富的代碼感知的編程體驗。重寫編譯器,并且在新的編譯器上開發(fā)Visual Studio的經(jīng)驗使得我們獲得了非常有用的性能優(yōu)化經(jīng)驗,這些經(jīng)驗也能用于大型的.NET應(yīng)用,或者一些需要處理大量數(shù)據(jù)的APP上。你不需要了解編譯器,也能夠從C#編譯器的例子中得出這些見解。

  Visual Studio使用了編譯器的API來實現(xiàn)了強大的智能感知(Intellisense)功能,如代碼關(guān)鍵字著色,語法填充列表,錯誤波浪線提示,參數(shù)提示,代碼問題及修改建議等,這些功能深受開發(fā)者歡迎。Visual Studio在開發(fā)者輸入或者修改代碼的時候,會動態(tài)的編譯代碼來獲得對代碼的分析和提示。

  當用戶和App進行交互的時候,通常希望軟件具有好的響應(yīng)性。輸入或者執(zhí)行命令的時候,應(yīng)用程序界面不應(yīng)該被阻塞。幫助或者提示能夠迅速顯示出來或者當用戶繼續(xù)輸入的時候停止提示,F(xiàn)在的App應(yīng)該避免在執(zhí)行長時間計算的時候阻塞UI線程從而讓用戶感覺程序不夠流暢。

  想了解更多關(guān)于新的編譯器的信息,可以訪問 .NET Compiler Platform ("Roslyn")

 基本要領(lǐng)

  在對.NET 進行性能調(diào)優(yōu)以及開發(fā)具有良好響應(yīng)性的應(yīng)用程序的時候,請考慮以下這些基本要領(lǐng):

  要領(lǐng)一:不要過早優(yōu)化

  編寫代碼比想象中的要復(fù)雜的多,代碼需要維護,調(diào)試及優(yōu)化性能。 一個有經(jīng)驗的程序員,通常會對自然而然的提出解決問題的方法并編寫高效的代碼。 但是有時候也可能會陷入過早優(yōu)化代碼的問題中。比如,有時候使用一個簡單的數(shù)組就夠了,非要優(yōu)化成使用哈希表,有時候簡單的重新計算一下可以,非要使用復(fù)雜的可能導致內(nèi)存泄漏的緩存。發(fā)現(xiàn)問題時,應(yīng)該首先測試性能問題然后再分析代碼。

  要領(lǐng)二:沒有評測,便是猜測

  剖析和測量不會撒謊。測評可以顯示CPU是否滿負荷運轉(zhuǎn)或者是存在磁盤I/O阻塞。測評會告訴你應(yīng)用程序分配了什么樣的以及多大的內(nèi)存,以及是否CPU花費了很多時間在 垃圾回收上。

  應(yīng)該為關(guān)鍵的用戶體驗或者場景設(shè)置性能目標,并且編寫測試來測量性能。通過使用科學的方法來分析性能不達標的原因的步驟如下:使用測評報告來指導,假設(shè)可能出現(xiàn)的情況,并且編寫實驗代碼或者修改代碼來驗證我們的假設(shè)或者修正。如果我們設(shè)置了基本的性能指標并且經(jīng)常測試,就能夠避免一些改變導致性能的回退(regression),這樣就能夠避免我們浪費時間在一些不必要的改動中。

  要領(lǐng)三:好工具很重要

  好的工具能夠讓我們能夠快速的定位到影響性能的最大因素(CPU,內(nèi)存,磁盤)并且能夠幫助我們定位產(chǎn)生這些瓶頸的代碼。微軟已經(jīng)發(fā)布了很多性能測試工具比如: Visual Studio Profiler, Windows Phone Analysis Tool, 以及 PerfView.

  PerfView是一款免費且性能強大的工具,他主要關(guān)注影響性能的一些深層次的問題(磁盤 I/O,GC 事件,內(nèi)存),后面會展示這方面的例子。我們能夠抓取性能相關(guān)的 Event Tracing for Windows(ETW)事件并能以應(yīng)用程序,進程,堆棧,線程的尺度查看這些信息。PerfView能夠展示應(yīng)用程序分配了多少,以及分配了何種內(nèi)存以及應(yīng)用程序中的函數(shù)以及調(diào)用堆棧對內(nèi)存分配的貢獻。這些方面的細節(jié),您可以查看隨工具下載發(fā)布的關(guān)于PerfView的非常詳細的幫助,Demo以及視頻教程(比如 Channel9上的視頻教程)

  要領(lǐng)四:所有的都與內(nèi)存分配相關(guān)

  你可能會想,編寫響應(yīng)及時的基于.NET的應(yīng)用程序關(guān)鍵在于采用好的算法,比如使用快速排序替代冒泡排序,但是實際情況并不是這樣。編寫一個響應(yīng)良好的app的最大因素在于內(nèi)存分配,特別是當app非常大或者處理大量數(shù)據(jù)的時候。

  在使用新的編譯器API開發(fā)響應(yīng)良好的IDE的實踐中,大部分工作都花在了如何避免開辟內(nèi)存以及管理緩存策略。PerfView追蹤顯示新的C# 和VB編譯器的性能基本上和CPU的性能瓶頸沒有關(guān)系。編譯器在讀入成百上千甚至上萬行代碼,讀入元數(shù)據(jù)活著產(chǎn)生編譯好的代碼,這些操作其實都是I/O bound 密集型。UI線程的延遲幾乎全部都是由于垃圾回收導致的。.NET框架對垃圾回收的性能已經(jīng)進行過高度優(yōu)化,他能夠在應(yīng)用程序代碼執(zhí)行的時候并行的執(zhí)行垃圾回收的大部分操作。但是,單個內(nèi)存分配操作有可能會觸發(fā)一次昂貴的垃圾回收操作,這樣GC會暫時掛起所有線程來進行垃圾回收(比如 Generation 2型的垃圾回收)

 常見的內(nèi)存分配以及例子

  這部分的例子雖然背后關(guān)于內(nèi)存分配的地方很少。但是,如果一個大的應(yīng)用程序執(zhí)行足夠多的這些小的會導致內(nèi)存分配的表達式,那么這些表達式會導致幾百M,甚至幾G的內(nèi)存分配。比如,在性能測試團隊把問題定位到輸入場景之前,一分鐘的測試模擬開發(fā)者在編譯器里面編寫代碼會分配幾G的內(nèi)存。

  裝箱

  裝箱發(fā)生在當通常分配在線程棧上或者數(shù)據(jù)結(jié)構(gòu)中的值類型,或者臨時的值需要被包裝到對象中的時候(比如分配一個對象來存放數(shù)據(jù),活著返回一個指針給一個Object對象)。.NET框架由于方法的簽名或者類型的分配位置,有些時候會自動對值類型進行裝箱。將值類型包裝為引用類型會產(chǎn)生內(nèi)存分配。.NET框架及語言會盡量避免不必要的裝箱,但是有時候在我們沒有注意到的時候會產(chǎn)生裝箱操作。過多的裝箱操作會在應(yīng)用程序中分配成M上G的內(nèi)存,這就意味著垃圾回收的更加頻繁,也會花更長時間。

  在PerfView中查看裝箱操作,只需要開啟一個追蹤(trace),然后查看應(yīng)用程序名字下面的GC Heap Alloc 項(記住,PerfView會報告所有的進程的資源分配情況),如果在分配相中看到了一些諸如System.Int32和System.Char的值類型,那么就發(fā)生了裝箱。選擇一個類型,就會顯示調(diào)用棧以及發(fā)生裝箱的操作的函數(shù)。

  例1 string方法和其值類型參數(shù)

  下面的示例代碼演示了潛在的不必要的裝箱以及在大的系統(tǒng)中的頻繁的裝箱操作。

public class Logger
{
    public static void WriteLine(string s)
    {
        /*...*/
    }
}
public class BoxingExample
{
    public void Log(int id, int size)
    {
        var s = string.Format("{0}:{1}", id, size);
        Logger.WriteLine(s);
    }
}

  這是一個日志基礎(chǔ)類,因此app會很頻繁的調(diào)用Log函數(shù)來記日志,可能該方法會被調(diào)用millons次。問題在于,調(diào)用string.Format方法會調(diào)用其 重載的接受一個string類型和兩個Object類型的方法:

String.Format Method (String, Object, Object)

  該重載方法要求.NET Framework 把int型裝箱為object類型然后將它傳到方法調(diào)用中去。為了解決這一問題,方法就是調(diào)用id.ToString()size.ToString()方法,然后傳入到string.Format 方法中去,調(diào)用ToString()方法的確會導致一個string的分配,但是在string.Format方法內(nèi)部不論怎樣都會產(chǎn)生string類型的分配。

  你可能會認為這個基本的調(diào)用string.Format 僅僅是字符串的拼接,所以你可能會寫出這樣的代碼:

var s = id.ToString() + ':' + size.ToString();

  實際上,上面這行代碼也會導致裝箱,因為上面的語句在編譯的時候會調(diào)用:

string.Concat(Object, Object, Object);

  這個方法,.NET Framework 必須對字符常量進行裝箱來調(diào)用Concat方法。

  解決方法:

  完全修復(fù)這個問題很簡單,將上面的單引號替換為雙引號即將字符常量換為字符串常量就可以避免裝箱,因為string類型的已經(jīng)是引用類型了。

var s = id.ToString() + ":" + size.ToString();

  例2 枚舉類型的裝箱

  下面的這個例子是導致新的C# 和VB編譯器由于頻繁的使用枚舉類型,特別是在Dictionary中做查找操作時分配了大量內(nèi)存的原因。

public enum Color { Red, Green, Blue }
public class BoxingExample
{
    private string name;
    private Color color;
    public override int GetHashCode()
    {
        return name.GetHashCode() ^ color.GetHashCode();
    }
}

  問題非常隱蔽,PerfView會告訴你enmu.GetHashCode()由于內(nèi)部實現(xiàn)的原因產(chǎn)生了裝箱操作,該方法會在底層枚舉類型的表現(xiàn)形式上進行裝箱,如果仔細看PerfView,會看到每次調(diào)用GetHashCode會產(chǎn)生兩次裝箱操作。編譯器插入一次,.NET Framework插入另外一次。

  解決方法:

  通過在調(diào)用GetHashCode的時候?qū)⒚杜e的底層表現(xiàn)形式進行強制類型轉(zhuǎn)換就可以避免這一裝箱操作。

((int)color).GetHashCode()

  另一個使用枚舉類型經(jīng)常產(chǎn)生裝箱的操作時enum.HasFlag。傳給HasFlag的參數(shù)必須進行裝箱,在大多數(shù)情況下,反復(fù)調(diào)用HasFlag通過位運算測試非常簡單和不需要分配內(nèi)存。

  要牢記基本要領(lǐng)第一條,不要過早優(yōu)化。并且不要過早的開始重寫所有代碼。 需要注意到這些裝箱的耗費,只有在通過工具找到并且定位到最主要問題所在再開始修改代碼。

  字符串

  字符串操作是引起內(nèi)存分配的最大元兇之一,通常在PerfView中占到前五導致內(nèi)存分配的原因。應(yīng)用程序使用字符串來進行序列化,表示JSON和REST。在不支持枚舉類型的情況下,字符串可以用來與其他系統(tǒng)進行交互。當我們定位到是由于string操作導致對性能產(chǎn)生嚴重影響的時候,需要留意string類的Format(),Concat(),Split(),Join(),Substring()等這些方法。使用StringBuilder能夠避免在拼接多個字符串時創(chuàng)建多個新字符串的開銷,但是StringBuilder的創(chuàng)建也需要進行良好的控制以避免可能會產(chǎn)生的性能瓶頸。

  例3 字符串操作

  在C#編譯器中有如下方法來輸出方法前面的xml格式的注釋。

public void WriteFormattedDocComment(string text)
{
    string[] lines = text.Split(new[] {"\r\n", "\r", "\n"},
        StringSplitOptions.None);
    int numLines = lines.Length;
    bool skipSpace = true;
    if (lines[0].TrimStart().StartsWith("///"))
    {
        for (int i = 0; i < numLines; i++)
        {
            string trimmed = lines[i].TrimStart();
            if (trimmed.Length < 4 || !char.IsWhiteSpace(trimmed[3]))
            {
                skipSpace = false;
                break;
            }
        }
        int substringStart = skipSpace ? 4 : 3;
        for (int i = 0; i < numLines; i++)
            Console.WriteLine(lines[i].TrimStart().Substring(substringStart));
    }
    else
    {
        /* ... */
    }
}

  可以看到,在這片代碼中包含有很多字符串操作。代碼中使用類庫方法來將行分割為字符串,來去除空格,來檢查參數(shù)text是否是XML文檔格式的注釋,然后從行中取出字符串處理。

  在WriteFormattedDocComment方法每次被調(diào)用時,第一行代碼調(diào)用Split()就會分配三個元素的字符串數(shù)組。編譯器也需要產(chǎn)生代碼來分配這個數(shù)組。因為編譯器并不知道,如果Splite()存儲了這一數(shù)組,那么其他部分的代碼有可能會改變這個數(shù)組,這樣就會影響到后面對WriteFormattedDocComment方法的調(diào)用。每次調(diào)用Splite()方法也會為參數(shù)text分配一個string,然后在分配其他內(nèi)存來執(zhí)行splite操作。

  WriteFormattedDocComment方法中調(diào)用了三次TrimStart()方法,在內(nèi)存環(huán)中調(diào)用了兩次,這些都是重復(fù)的工作和內(nèi)存分配。更糟糕的是,TrimStart()的無參重載方法的簽名如下:

namespace System
{ 
    public class String 
    { 
        public string TrimStart(params char[] trimChars);
    }
}

  該方法簽名意味著,每次對TrimStart()的調(diào)用都回分配一個空的數(shù)組以及返回一個string類型的結(jié)果。

  最后,調(diào)用了一次Substring()方法,這個方法通常會導致在內(nèi)存中分配新的字符串。

  解決方法:

  和前面的只需要小小的修改即可解決內(nèi)存分配的問題不同。在這個例子中,我們需要從頭看,查看問題然后采用不同的方法解決。比如,可以意識到WriteFormattedDocComment()方法的參數(shù)是一個字符串,它包含了方法中需要的所有信息,因此,代碼只需要做更多的index操作,而不是分配那么多小的string片段。

  下面的方法并沒有完全解,但是可以看到如何使用類似的技巧來解決本例中存在的問題。C#編譯器使用如下的方式來消除所有的額外內(nèi)存分配。

private int IndexOfFirstNonWhiteSpaceChar(string text, int start)
{
    while (start < text.Length && char.IsWhiteSpace(text[start])) 
        start++;
    return start;
}

private bool TrimmedStringStartsWith(string text, int start, string prefix)
{
    start = IndexOfFirstNonWhiteSpaceChar(text, start); 
    int len = text.Length - start; 
    if (len < prefix.Length) return false;
    for (int i = 0; i < len; i++)
    {
        if (prefix[i] != text[start + i]) 
            return false;
    }
    return true;
}

  WriteFormattedDocComment() 方法的第一個版本分配了一個數(shù)組,幾個子字符串,一個trim后的子字符串,以及一個空的params數(shù)組。也檢查了”///”。修改后的代碼僅使用了index操作,沒有任何額外的內(nèi)存分配。它查找第一個非空格的字符串,然后逐個字符串比較來查看是否以”///”開頭。和使用TrimStart()不同,修改后的代碼使用IndexOfFirstNonWhiteSpaceChar方法來返回第一個非空格的開始位置,通過使用這種方法,可以移除WriteFormattedDocComment()方法中的所有額外內(nèi)存分配。

  例4 StringBuilder

  本例中使用StringBuilder。下面的函數(shù)用來產(chǎn)生泛型類型的全名:

public class Example 
{ 
    // Constructs a name like "SomeType<T1, T2, T3>" 
    public string GenerateFullTypeName(string name, int arity) 
    { 
        StringBuilder sb = new StringBuilder();
        sb.Append(name);
        if (arity != 0)
        { 
            sb.Append("<");
            for (int i = 1; i < arity; i++)
            {
                sb.Append("T"); sb.Append(i.ToString()); sb.Append(", ");
            } 
            sb.Append("T"); sb.Append(i.ToString()); sb.Append(">");
        }
        return sb.ToString(); 
    }
}

  注意力集中到StringBuilder實例的創(chuàng)建上來。代碼中調(diào)用sb.ToString()會導致一次內(nèi)存分配。在StringBuilder中的內(nèi)部實現(xiàn)也會導致內(nèi)部內(nèi)存分配,但是我們?nèi)绻胍@取到string類型的結(jié)果化,這些分配無法避免。

  解決方法:

  要解決StringBuilder對象的分配就使用緩存。即使緩存一個可能被隨時丟棄的單個實例對象也能夠顯著的提高程序性能。下面是該函數(shù)的新的實現(xiàn)。除了下面兩行代碼,其他代碼均相同

// Constructs a name like "Foo<T1, T2, T3>" 
public string GenerateFullTypeName(string name, int arity)
{
    StringBuilder sb = AcquireBuilder(); /* Use sb as before */ 
    return GetStringAndReleaseBuilder(sb);
}

  關(guān)鍵部分在于新的 AcquireBuilder()GetStringAndReleaseBuilder()方法:

[ThreadStatic]
private static StringBuilder cachedStringBuilder;

private static StringBuilder AcquireBuilder()
{
    StringBuilder result = cachedStringBuilder;
    if (result == null)
    {
        return new StringBuilder();
    } 
    result.Clear(); 
    cachedStringBuilder = null; 
    return result;
}

private static string GetStringAndReleaseBuilder(StringBuilder sb)
{
    string result = sb.ToString(); 
    cachedStringBuilder = sb; 
    return result;
}

  上面方法實現(xiàn)中使用了 thread-static字段來緩存StringBuilder對象,這是由于新的編譯器使用了多線程的原因。很可能會忘掉這個ThreadStatic聲明。Thread-static字符為每個執(zhí)行這部分的代碼的線程保留一個唯一的實例。

  如果已經(jīng)有了一個實例,那么AcquireBuilder()方法直接返回該緩存的實例,在清空后,將該字段或者緩存設(shè)置為null。否則AcquireBuilder()創(chuàng)建一個新的實例并返回,然后將字段和cache設(shè)置為null 。

  當我們對StringBuilder處理完成之后,調(diào)用GetStringAndReleaseBuilder()方法即可獲取string結(jié)果。然后將StringBuilder保存到字段中或者緩存起來,然后返回結(jié)果。這段代碼很可能重復(fù)執(zhí)行,從而創(chuàng)建多個StringBuilder對象,雖然很少會發(fā)生。代碼中僅保存最后被釋放的那個StringBuilder對象來留作后用。新的編譯器中,這種簡單的的緩存策略極大地減少了不必要的內(nèi)存分配。.NET Framework 和 MSBuild中的部分模塊也使用了類似的技術(shù)來提升性能。

  簡單的緩存策略必須遵循良好的緩存設(shè)計,因為他有大小的限制cap。使用緩存可能比之前有更多的代碼,也需要更多的維護工作。我們只有在發(fā)現(xiàn)這是個問題之后才應(yīng)該采緩存策略。PerfView已經(jīng)顯示出StringBuilder對內(nèi)存的分配貢獻相當大。

標簽: 安全 代碼 開發(fā)者 評測 數(shù)據(jù)庫

版權(quán)申明:本站文章部分自網(wǎng)絡(luò),如有侵權(quán),請聯(lián)系:west999com@outlook.com
特別注意:本站所有轉(zhuǎn)載文章言論不代表本站觀點!
本站所提供的圖片等素材,版權(quán)歸原作者所有,如需使用,請與原作者聯(lián)系。

上一篇:一步步教你如何在 Visual Studio 2013 上使用 Github

下一篇:C#實現(xiàn)一個最簡單的HTTP服務(wù)器