Aracılığıyla paylaş


Büyük, Yanıt Veren .NET Framework Uygulamaları Yazma

Bu makale, büyük .NET Framework uygulamalarının veya dosyalar veya veritabanları gibi büyük miktarda veriyi işleyen uygulamaların performansını geliştirmeye yönelik ipuçları sağlar. Bu ipuçları, yönetilen kodda C# ve Visual Basic derleyicilerinin yeniden yazılmasından gelir ve bu makale C# derleyicisinden birkaç gerçek örnek içerir.

.NET Framework, uygulama oluşturmak için son derece üretkendir. Güçlü ve güvenli diller ve zengin kitaplık koleksiyonu, uygulama oluşturmayı son derece verimli hale getirir. Ancak, büyük üretkenlik sorumluluk getirir. .NET Framework'ün tüm gücünü kullanmanız, ancak gerektiğinde kodunuzun performansını ayarlamaya hazır olmanız gerekir.

Yeni derleyici performansı neden uygulamanız için geçerlidir?

.NET Derleyici Platformu ("Roslyn") ekibi, yönetilen koddaki C# ve Visual Basic derleyicilerini yeniden yazarak kodu modellemeye ve çözümlemeye, araçlar oluşturmaya ve Visual Studio'da çok daha zengin, koda duyarlı deneyimlere olanak sağlamaya yönelik yeni API'ler sağlar. Derleyicileri yeniden yazmak ve yeni derleyiciler üzerinde Visual Studio deneyimleri oluşturmak, büyük bir .NET Framework uygulaması veya çok fazla veri işleyen herhangi bir uygulama için geçerli olan yararlı performans içgörülerini ortaya çıkardı. C# derleyicisinden elde ettiğiniz içgörülerden ve örneklerden yararlanmak için derleyiciler hakkında bilgi sahibi olmanız gerekmez.

Visual Studio, tanımlayıcıların ve anahtar sözcüklerin renklendirmesi, söz dizimi tamamlama listeleri, hatalar için dalgalı çizgiler, parametre ipuçları, kod sorunları ve kod eylemleri gibi kullanıcıların sevdiği tüm IntelliSense özelliklerini oluşturmak için derleyici API'lerini kullanır. Visual Studio, geliştiriciler kodlarını yazarken ve değiştirirken bu yardımı sağlar ve derleyici kod geliştiricilerinin düzenlemesini sürekli modellerken Visual Studio'nun yanıt vermeye devam etmesi gerekir.

Son kullanıcılarınız uygulamanızla etkileşime geçtiğinde yanıt vermenizi bekler. Yazma veya komut işleme hiçbir zaman engellenmemelidir. Kullanıcı yazmaya devam ederse yardım hızlı bir şekilde açılır veya vazgeçer. Uygulamanız, uygulamanın yavaş hissetmesini sağlayan uzun hesaplamalarla ui iş parçacığını engellemekten kaçınmalıdır.

Roslyn derleyicileri hakkında daha fazla bilgi için bkz . .NET Derleyici Platformu SDK'sı.

Sadece Gerçekler

Performansı ayarlarken ve duyarlı .NET Framework uygulamaları oluştururken bu olguları göz önünde bulundurun.

Gerçek 1: Erken iyileştirmeler her zaman zahmete değmez

Olması gerekenden daha karmaşık bir kod yazmak bakım, hata ayıklama ve parlatma maliyetlerine neden olur. Deneyimli programcılar kodlama sorunlarını çözme ve daha verimli kod yazma konusunda sezgisel bir kavramaya sahiptir. Ancak, bazen kodlarını erken iyileştirirler. Örneğin, basit bir dizi yeterli olduğunda karma tablo kullanır veya yalnızca değerleri yeniden derlemek yerine belleği sızdırabilecek karmaşık önbelleğe alma kullanırlar. Deneyim programcısı olsanız bile, sorun bulduğunuzda performansı test etmeli ve kodunuzu analiz etmelisiniz.

2. Olgu: Ölçümlemiyorsanız,

Profiller ve ölçümler yalan söylemez. Profiller, CPU'nun tamamen yüklenip yüklenmediğini veya disk G/Ç'sinde engellenip engellenmediğinizi gösterir. Profiller, ne tür ve ne kadar bellek ayırdığınızı ve CPU'nuzun çöp toplamada (GC) çok fazla zaman harcayıp harcamadığını bildirir.

Uygulamanızdaki önemli müşteri deneyimleri veya senaryoları için performans hedefleri belirlemeniz ve performansı ölçmek için testler yazmanız gerekir. Bilimsel yöntemi uygulayarak başarısız testleri araştırın: Size yol gösterecek profilleri kullanın, sorunun ne olabileceğini hipotezleyin ve bir deneme veya kod değişikliğiyle hipotezinizi test edin. Düzenli testlerle zaman içinde temel performans ölçümleri oluşturun, böylece performansta regresyonlara neden olan değişiklikleri yalıtabilirsiniz. Performans çalışmalarına sıkı bir şekilde yaklaşarak ihtiyacınız olmayan kod güncelleştirmeleriyle zaman kaybetmekten kaçınmış olursunuz.

Olgu 3: İyi araçlar tüm farkları yaratır

İyi araçlar, en büyük performans sorunlarını (CPU, bellek veya disk) hızla detaylandırmanıza ve bu performans sorunlarına neden olan kodu bulmanıza yardımcı olur. Microsoft, Visual Studio Profiler ve PerfView gibi çeşitli performans araçları sağlar.

PerfView, disk G/Ç, GC olayları ve bellek gibi derin sorunlara odaklanmanıza yardımcı olan güçlü bir araçtır. Windows için Performansla ilgili Olay İzleme (ETW) olaylarını yakalayabilir ve uygulama başına, işlem başına, yığın başına ve iş parçacığı bilgileri başına kolayca görüntüleyebilirsiniz. PerfView, uygulamanızın ne kadar bellek ayırdığı ve hangi işlevlerin veya çağrı yığınlarının bellek ayırmalarına ne kadar katkıda bulunduğu gösterilir. Ayrıntılar için, araçla birlikte sunulan zengin yardım konularına, tanıtımlara ve videolara bakın.

Olgu 4: Her şey ayırmalarla ilgili

Hızlı yanıt veren bir .NET Framework uygulaması oluşturmanın, kabarcık sıralama yerine hızlı sıralama kullanma gibi algoritmalarla ilgili olduğunu düşünebilirsiniz, ancak durum bu değildir. Duyarlı bir uygulama oluşturmanın en büyük faktörü, özellikle uygulamanız çok büyük olduğunda veya büyük miktarda veri işlediğinde bellek ayırmadır.

Yeni derleyici API'leriyle duyarlı IDE deneyimleri oluşturmaya yönelik neredeyse tüm çalışmalar, ayırmalardan kaçınmayı ve önbelleğe alma stratejilerini yönetmeyi içeriyor. PerfView izlemeleri, yeni C# ve Visual Basic derleyicilerinin performansının nadiren CPU'ya bağlı olduğunu gösterir. Derleyiciler yüz binlerce veya milyonlarca kod satırı okurken, meta verileri okurken veya oluşturulan kodu yayarken G/Ç sınırına bağlı olabilir. Ui iş parçacığı gecikmelerinin neredeyse tümü çöp toplamadan kaynaklanmaktadır. .NET Framework GC yüksek performans için ayarlanmıştır ve uygulama kodu yürütülürken çalışmalarının büyük bir kısmını eşzamanlı olarak yapar. Ancak tek bir ayırma, tüm iş parçacıklarını durdurarak pahalı bir 2 . nesil koleksiyonunu tetikleyebilir.

Yaygın ayırmalar ve örnekler

Bu bölümdeki örnek ifadelerin küçük görünen gizli ayırmaları vardır. Ancak, büyük bir uygulama ifadeleri yeterince kez yürütürse, yüzlerce megabayt, hatta gigabayt ayırmalara neden olabilir. Örneğin, bir geliştiricinin düzenleyicide yazdığının benzetimini yapılan bir dakikalık testler gigabayt bellek ayırdı ve performans ekibini yazma senaryolarına odaklanmaya yönlendirdi.

Kutulama

Normalde yığında veya veri yapılarında yaşayan değer türleri bir nesneye sarmalandığında kutulama gerçekleşir. Başka bir ifadeyle, verileri tutmak için bir nesne ayırır ve sonra nesneye bir işaretçi döndürürsiniz. .NET Framework bazen bir yöntemin imzası veya depolama konumu türü nedeniyle değerleri kutular. Nesnedeki bir değer türünü sarmalama, bellek ayırmaya neden olur. Birçok kutulama işlemi, uygulamanıza megabaytlar veya gigabaytlar kadar ayırmaya katkıda bulunabilir ve bu da uygulamanızın daha fazla GC'ye neden olacağı anlamına gelir. .NET Framework ve dil derleyicileri mümkün olduğunda kutulama yapmaktan kaçınabilir, ancak bazen en az beklediğiniz durumlarda bu durum ortaya çıkar.

PerfView'da kutulama işlemini görmek için bir izleme açın ve uygulamanızın işlem adının altındaki GC Yığın Yığın Yığınları'na bakın (unutmayın, PerfView tüm işlemlerle ilgili raporlar). ve System.Char gibi System.Int32 türleri ayırmaların altında görüyorsanız, değer türlerini kutulu olarak ayarlarsınız. Bu türlerden birini seçmek, kutulandıkları yığınları ve işlevleri gösterir.

Örnek 1: dize yöntemleri ve değer türü bağımsız değişkenleri

Bu örnek kod, potansiyel olarak gereksiz ve aşırı kutulama durumunu gösterir:

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);
    }
}

Bu kod günlük işlevi sağlar, bu nedenle bir uygulama işlevi sık, belki de milyonlarca kez çağırabilir Log . Sorun, çağrısının string.Format aşırı yüklemeye çözümlenmesidir Format(String, Object, Object) .

Bu aşırı yükleme, .NET Framework'ün bu yöntem çağrısına int geçirmek için değerleri nesnelere kutular. Kısmi bir düzeltme, çağrıya tüm dizeleri (nesne olan) çağırmak id.ToString() ve size.ToString() geçirmektir string.Format . Çağrısı ToString() bir dize ayırır, ancak bu ayırma yine de içinde string.Formatgerçekleşir.

bu temel çağrısının string.Format yalnızca dize birleştirme olduğunu düşünebilirsiniz, bu nedenle bunun yerine bu kodu yazabilirsiniz:

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

Ancak, bu kod satırı için derlendiğinden bir kutulama ayırması ekler Concat(Object, Object, Object). .NET Framework'ün çağrılmak için karakter değişmez değeri kutusuna yazması gerekir Concat

Örneğin 1 düzeltmesi

Tam düzeltme basittir. Karakter değişmez değerini, dizeler zaten nesne olduğundan kutulama gerektirmeyen bir dize değişmez değeriyle değiştirmeniz gerekir:

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

Örnek 2: numaralandırma kutulama

Bu örnek, özellikle sözlük arama işlemlerinde numaralandırma türlerinin sık kullanılması nedeniyle yeni C# ve Visual Basic derleyicilerinde çok büyük miktarda ayırmadan sorumluydu.

public enum Color
{
    Red, Green, Blue
}

public class BoxingExample
{
    private string name;
    private Color color;
    public override int GetHashCode()
    {
        return name.GetHashCode() ^ color.GetHashCode();
    }
}

Bu sorun çok ince. Yöntem, uygulama nedenleriyle numaralandırma türünün temel gösterimini kutuladığı için PerfView bunu kutulama olarak GetHashCode() bildirir. PerfView'a yakından bakarsanız, her çağrısı GetHashCode()için iki kutulama ayırması görebilirsiniz. Derleyici birini ekler ve .NET Framework diğerini ekler.

Örneğin 2 düzeltmesi

çağrısından GetHashCode()önce temel gösterime dönüştürerek her iki ayırmayı da kolayca önleyebilirsiniz:

((int)color).GetHashCode()

Numaralandırma türlerinde kutulamanın bir diğer yaygın kaynağı yöntemidir Enum.HasFlag(Enum) . geçirilen HasFlag(Enum) bağımsız değişkenin kutulanması gerekir. Çoğu durumda, çağrısının Enum.HasFlag(Enum) bit tabanlı bir testle değiştirilmesi daha basit ve ayırmasızdır.

İlk performans olgusunu aklınızda bulundurun (yani, erken iyileştirmeyin) ve tüm kodunuzu bu şekilde yeniden yazmaya başlamayın. Bu kutulama maliyetlerine dikkat edin, ancak kodunuzu yalnızca uygulamanızın profilini çıkardıktan ve sık erişim noktalarını bulduktan sonra değiştirin.

Dizeler

Dize işlemeleri, ayırmalar için en büyük suçlulardan bazılarıdır ve genellikle ilk beş ayırmada PerfView'da gösterilir. Programlar serileştirme, JSON ve REST API'leri için dizeler kullanır. Sabit listesi türlerini kullanamıyorsanız sistemlerle birlikte çalışabilmek için dizeleri programlı sabitler olarak kullanabilirsiniz. Profil oluşturma işleminiz dizelerin performansı yüksek oranda etkilediğini gösterdiğinde , , Concat, Split, Join, Substringve gibi Formatyöntemlere String yönelik çağrılar arayın. StringBuilder Birçok parçadan bir dize oluşturmanın maliyetini önlemek için kullanmak yardımcı olur, ancak nesneyi ayırmanız StringBuilder bile yönetmeniz gereken bir performans sorununa neden olabilir.

Örnek 3: dize işlemleri

C# derleyicisinde biçimlendirilmiş XML belge açıklamasının metnini yazan şu kod vardı:

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++)
            WriteLine(lines[i].TrimStart().Substring(substringStart));
    }
    else { /* ... */ }

Bu kodun çok fazla dize işlemesi yaptığını görebilirsiniz. Kod, satırları ayrı dizelere bölmek, boşluğu kırpmak, bağımsız değişkenin text BIR XML belge açıklaması olup olmadığını denetlemek ve satırlardan alt dizeleri ayıklamak için kitaplık yöntemlerini kullanır.

içindeki WriteFormattedDocCommentilk satırda text.Split , çağrısı her çağrıldığında bağımsız değişken olarak yeni bir üç öğeli dizi ayırır. Derleyicinin bu diziyi her seferinde ayırmak için kod yaymak zorunda. Bunun nedeni, derleyicinin diziyi dizinin başka bir kod tarafından değiştirilebileceği bir yerde depolup Split depolamayacağını bilmemesi ve daha sonra 'a yapılan çağrıları etkilemesidir WriteFormattedDocComment. çağrısı Split ayrıca içindeki text her satır için bir dize ayırır ve işlemi gerçekleştirmek için başka bir bellek ayırır.

WriteFormattedDocComment yöntemine üç çağrıya TrimStart sahiptir. İkisi, işi ve ayırmaları çoğaltan iç döngülerdedir. Daha da kötüsü, bağımsız değişken olmadan yöntemini çağırmak TrimStart , dize sonucuna ek olarak boş bir dizi (parametre için params ) ayırır.

Son olarak, yöntemine Substring genellikle yeni bir dize ayıran bir çağrı vardır.

Örnek 3 için düzeltme

Önceki örneklerden farklı olarak, küçük düzenlemeler bu ayırmaları düzeltemez. Geri adım atıp soruna bakmanız ve farklı yaklaşmanız gerekir. Örneğin, için bağımsız değişkeninin WriteFormattedDocComment() yöntemin ihtiyaç duyduğu tüm bilgileri içeren bir dize olduğunu fark edeceksiniz, bu nedenle kod birçok kısmi dize ayırma yerine daha fazla dizin oluşturma gerçekleştirebilir.

Derleyicinin performans ekibi tüm bu ayırmaları aşağıdaki gibi bir kodla ele aldı:

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;
}

// etc...

İlk sürümü WriteFormattedDocComment() ayrılmış bir dizi, birkaç alt dize ve boş params bir dizi ile birlikte kırpılmış alt dize. "///" için de denetlendi. Düzeltilen kod yalnızca dizin oluşturmayı kullanır ve hiçbir şey ayırmaz. Boşluk olmayan ilk karakteri bulur ve ardından dizenin "///" ile başlayıp başlamadığını görmek için karakteri karaktere göre denetler. Yeni kod, boşluk olmayan bir karakterin oluştuğu ilk dizini (belirtilen bir başlangıç dizininden sonra) döndürmek için yerine TrimStart kullanırIndexOfFirstNonWhiteSpaceChar. Düzeltme tamamlanmadı, ancak eksiksiz bir çözüm için benzer düzeltmelerin nasıl uygulanacağını görebilirsiniz. Bu yaklaşımı kod genelinde uygulayarak içindeki tüm ayırmaları WriteFormattedDocComment()kaldırabilirsiniz.

Örnek 4: StringBuilder

Bu örnekte bir StringBuilder nesne kullanılır. Aşağıdaki işlev genel türler için tam tür adı oluşturur:

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();
    }
}

Odak, yeni StringBuilder bir örnek oluşturan satırdadır. Kod, uygulama içinde ve iç ayırmalar için sb.ToString() ayırmaya StringBuilder neden olur, ancak dize sonucunu istiyorsanız bu ayırmaları denetleyemezsiniz.

Örnek 4 için düzeltme

Nesne ayırmayı StringBuilder düzeltmek için nesneyi önbelleğe alın. Atılan tek bir örneği önbelleğe almak bile performansı önemli ölçüde geliştirebilir. Bu, işlevin yeni uygulamasıdır ve yeni ilk ve son satırlar dışındaki tüm kodu atlar:

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

Temel parçalar yeni AcquireBuilder() ve GetStringAndReleaseBuilder() işlevlerdir:

[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;
}

Yeni derleyiciler iş parçacığı kullandığından, bu uygulamalar önbelleğe StringBuilderalmak için bir iş parçacığı statik alanı (ThreadStaticAttribute öznitelik) kullanır ve büyük olasılıkla bildiriminden ThreadStatic yararlanabilirsiniz. İş parçacığı statik alanı, bu kodu yürüten her iş parçacığı için benzersiz bir değer tutar.

AcquireBuilder() varsa, alanı veya önbelleği StringBuilder null olarak ayarladıktan sonra önbelleğe alınmış örneği döndürür. Aksi takdirde, AcquireBuilder() yeni bir örnek oluşturur ve alanı veya önbelleği null olarak bırakarak bu örneği döndürür.

ile StringBuilder işiniz bittiğinde dize sonucunu almak, örneği alana veya önbelleğe kaydetmek StringBuilder ve ardından sonucu döndürmek için öğesini çağırırsınızGetStringAndReleaseBuilder(). Yürütmenin bu kodu yeniden girmesi ve birden çok StringBuilder nesne oluşturması mümkündür (nadiren gerçekleşse de). Kod, daha sonra kullanmak üzere yalnızca son yayımlanan StringBuilder örneği kaydeder. Bu basit önbelleğe alma stratejisi, yeni derleyicilerdeki ayırmaları önemli ölçüde azaltmıştır. .NET Framework ve MSBuild ("MSBuild") bölümleri, performansı geliştirmek için benzer bir teknik kullanır.

Bu basit önbelleğe alma stratejisi, boyut üst sınırına sahip olduğundan iyi bir önbellek tasarımına bağlıdır. Ancak, şu anda özgün koddan daha fazla kod var ve bu da daha fazla bakım maliyeti anlamına geliyor. Önbelleğe alma stratejisini yalnızca bir performans sorunu bulduğunuzda benimsemeniz gerekir ve PerfView ayırmaların önemli bir katkıda bulunan olduğunu StringBuilder göstermiştir.

LINQ ve lambdalar

DilLe Tümleşik Sorgu (LINQ), lambda ifadeleriyle birlikte üretkenlik özelliğine bir örnektir. Ancak, kullanımı zaman içindeki performansı önemli ölçüde etkileyebilir ve kodunuzu yeniden yazmanız gerektiğini fark edebilirsiniz.

Örnek 5: Lambdas, Liste<T> ve IEnumerable<T>

Bu örnekte, derleyicinin modelinde ad dizesi verilen bir simgeyi bulmak için LINQ ve işlevsel stil kodu kullanılmaktadır:

class Symbol {
    public string Name { get; private set; }
    /*...*/
}

class Compiler {
    private List<Symbol> symbols;
    public Symbol FindMatchingSymbol(string name)
    {
        return symbols.FirstOrDefault(s => s.Name == name);
    }
}

Yeni derleyici ve üzerinde oluşturulan IDE deneyimleri çok sık çağrır FindMatchingSymbol() ve bu işlevin tek kod satırında birkaç gizli ayırma vardır. Bu ayırmaları incelemek için önce işlevin tek kod satırını iki satıra bölün:

Func<Symbol, bool> predicate = s => s.Name == name;
     return symbols.FirstOrDefault(predicate);

İlk satırda lambda ifadesi s => s.Name == name yerel değişkeni nameüzerinden kapanır. Bu, bir nesneyi tutan temsilci predicate için ayırmaya ek olarak, kodun değerini nameyakalayan ortamı tutmak için statik bir sınıf ayıracağı anlamına gelir. Derleyici aşağıdakine benzer bir kod oluşturur:

// Compiler-generated class to hold environment state for lambda
private class Lambda1Environment
{
    public string capturedName;
    public bool Evaluate(Symbol s)
    {
        return s.Name == this.capturedName;
    }
}

// Expanded Func<Symbol, bool> predicate = s => s.Name == name;
Lambda1Environment l = new Lambda1Environment() { capturedName = name };
var predicate = new Func<Symbol, bool>(l.Evaluate);

İki new ayırma (biri ortam sınıfı, biri temsilci için) şimdi açık.

Şimdi çağrısına FirstOrDefaultbakın. Türündeki System.Collections.Generic.IEnumerable<T> bu uzantı yöntemi de bir ayırmaya neden olur. Bir FirstOrDefault IEnumerable<T> nesneyi ilk bağımsız değişkeni olarak aldığından, çağrıyı şu koda genişletebilirsiniz (tartışma için biraz basitleştirilmiş):

// Expanded return symbols.FirstOrDefault(predicate) ...
     IEnumerable<Symbol> enumerable = symbols;
     IEnumerator<Symbol> enumerator = enumerable.GetEnumerator();
     while(enumerator.MoveNext())
     {
         if (predicate(enumerator.Current))
             return enumerator.Current;
     }
     return default(Symbol);

symbols değişkeninin türü List<T>vardır. List<T> Koleksiyon türü, ile structuygulayan List<T> bir numaralandırıcıyı (IEnumerator<T>arabirim) uygular IEnumerable<T> ve akıllıca tanımlar. Sınıf yerine bir yapı kullanmak, genellikle atık toplama performansını etkileyebilecek yığın ayırmalarından kaçınmanız anlamına gelir. Numaralandırıcılar genellikle çağrı yığınında döndürülürken numaralandırıcı yapısını kullanan dilin foreach döngüsüyle birlikte kullanılır. Bir nesneye yer açmak için çağrı yığını işaretçisinin artırılması, yığın ayırmanın yaptığı gibi GC'yi etkilemez.

Genişletilmiş FirstOrDefault çağrı söz konusu olduğunda kodun üzerinde IEnumerable<T>çağrısı GetEnumerator() yapması gerekir. türündeki IEnumerable<Symbol> enumerable değişkene atandığınızdasymbols, gerçek nesnenin bir List<T>olduğu bilgisi kaybolur. Bu, kod ile enumerable.GetEnumerator()numaralandırıcıyı getirdiğinde .NET Framework'ün değişkene atamak için döndürülen yapıyı kutulaması enumerator gerekir.

Örneğin 5 düzeltmesi

Düzeltme, tek kod satırını hala kısa, kolay okunabilen ve anlaşılması kolay ve bakımı kolay altı kod satırıyla değiştirerek aşağıdaki gibi yeniden yazmaktır FindMatchingSymbol :

public Symbol FindMatchingSymbol(string name)
    {
        foreach (Symbol s in symbols)
        {
            if (s.Name == name)
                return s;
        }
        return null;
    }

Bu kod LINQ uzantısı yöntemlerini, lambda'ları veya numaralandırıcıları kullanmaz ve ayırma yapmaz. Derleyici koleksiyonun symbols a List<T> olduğunu görebildiği ve kutulamadan kaçınmak için elde edilen numaralandırıcıyı (bir yapı) doğru türe sahip bir yerel değişkene bağlayabildiği için ayırma yoktur. Bu işlevin özgün sürümü, C# dilinin etkileyici gücünün ve .NET Framework'ün üretkenliğinin harika bir örneğiydi. Bu yeni ve daha verimli sürüm, bakım için herhangi bir karmaşık kod eklemeden bu özellikleri korur.

Zaman uyumsuz yöntem önbelleğe alma

Sonraki örnekte, zaman uyumsuz bir yöntemde önbelleğe alınmış sonuçları kullanmaya çalıştığınızda sık karşılaşılan bir sorun gösterilmektedir.

Örnek 6: Zaman uyumsuz yöntemlerde önbelleğe alma

Yeni C# ve Visual Basic derleyicileri üzerinde oluşturulan Visual Studio IDE özellikleri sık sık söz dizimi ağaçlarını getirir ve derleyiciler bunu yaparken zaman uyumsuz kullanarak Visual Studio'yu yanıt vermeye devam eder. Söz dizimi ağacını almak için yazabileceğiniz kodun ilk sürümü aşağıdadır:

class SyntaxTree { /*...*/ }

class Parser { /*...*/
    public SyntaxTree Syntax { get; }
    public Task ParseSourceCode() { /*...*/ }
}

class Compilation { /*...*/
    public async Task<SyntaxTree> GetSyntaxTreeAsync()
    {
        var parser = new Parser(); // allocation
        await parser.ParseSourceCode(); // expensive
        return parser.Syntax;
    }
}

Çağrısının GetSyntaxTreeAsync() bir Parserörneğini oluşturduğunu, kodu ayrıştırdığını ve ardından nesnesini Task Task<SyntaxTree>döndürdüğünü görebilirsiniz. Pahalı kısmı örneği ayırma Parser ve kodu ayrıştırmadır. İşlev, çağıranların ayrıştırma işini bekleyebilmesi ve kullanıcı girişine yanıt vermek için ui iş parçacığını serbest durumdan kurtarabilmesi için bir Task döndürür.

Çeşitli Visual Studio özellikleri aynı söz dizimi ağacını almaya çalışabilir, bu nedenle zaman ve ayırmalardan tasarruf etmek için ayrıştırma sonucunu önbelleğe almak için aşağıdaki kodu yazabilirsiniz. Ancak, bu kod bir ayırmaya neden olabilir:

class Compilation { /*...*/

    private SyntaxTree cachedResult;

    public async Task<SyntaxTree> GetSyntaxTreeAsync()
    {
        if (this.cachedResult == null)
        {
            var parser = new Parser(); // allocation
            await parser.ParseSourceCode(); // expensive
            this.cachedResult = parser.Syntax;
        }
        return this.cachedResult;
    }
}

Önbelleğe alma içeren yeni kodun adlı cachedResultbir SyntaxTree alanı olduğunu görürsünüz. Bu alan null olduğunda, GetSyntaxTreeAsync() çalışmayı yapar ve sonucu önbelleğe kaydeder. GetSyntaxTreeAsync()SyntaxTree nesnesini döndürür. Sorun, türünde bir async işleviniz olduğunda ve türünde Task<SyntaxTree>SyntaxTreebir değer döndürdüğünüzde, derleyicinin sonucu tutmak için bir Görev ayırmak için kod yaymasıdır (kullanarakTask<SyntaxTree>.FromResult()). Görev tamamlandı olarak işaretlenir ve sonuç hemen kullanılabilir. Yeni derleyicilerin kodunda, Task zaten tamamlanmış nesneler o kadar sık oluştu ki bu ayırmaların düzeltilmesi dikkat çekici bir şekilde yanıt verme hızını geliştirdi.

Örneğin 6 düzeltmesi

Tamamlanan Task ayırmayı kaldırmak için Task nesnesini tamamlanmış sonuçla önbelleğe alabilirsiniz:

class Compilation { /*...*/

    private Task<SyntaxTree> cachedResult;

    public Task<SyntaxTree> GetSyntaxTreeAsync()
    {
        return this.cachedResult ??
               (this.cachedResult = GetSyntaxTreeUncachedAsync());
    }

    private async Task<SyntaxTree> GetSyntaxTreeUncachedAsync()
    {
        var parser = new Parser(); // allocation
        await parser.ParseSourceCode(); // expensive
        return parser.Syntax;
    }
}

Bu kod, türünü cachedResult olarak Task<SyntaxTree> değiştirir ve dosyasındaki özgün kodu GetSyntaxTreeAsync()tutan bir async yardımcı işlev çalıştırır. GetSyntaxTreeAsync()şimdi null değilse döndürmek cachedResult için null birleşim işlecini kullanır. Null ise cachedResult , GetSyntaxTreeAsync() sonucu çağırır GetSyntaxTreeUncachedAsync() ve önbelleğe alır. GetSyntaxTreeAsync() Kod normalde olduğu gibi çağrısının GetSyntaxTreeUncachedAsync() beklenmeyeceğini fark edin. await kullanmaması, GetSyntaxTreeAsync() nesnesini döndürdüğünde Task GetSyntaxTreeUncachedAsync() hemen döndürdüğü Taskanlamına gelir. Önbelleğe alınan sonuç bir Taskolduğundan, önbelleğe alınan sonucu döndürmek için ayırma yoktur.

Dikkat edilecek diğer noktalar

Burada, çok fazla veri işleyen büyük uygulamalarda veya uygulamalarda olası sorunlar hakkında birkaç nokta daha bulabilirsiniz.

Sözlük

Sözlükler birçok programda yaygın olarak kullanılır ve sözlükler çok kullanışlı ve doğal olarak verimli olsa da. Ancak bunlar genellikle uygunsuz bir şekilde kullanılır. Visual Studio'da ve yeni derleyicilerde çözümleme, sözlüklerin çoğunun tek bir öğe içerdiğini veya boş olduğunu gösterir. Boş Dictionary<TKey,TValue> bir alanın on alanı vardır ve x86 makinesindeki yığında 48 bayt yer alır. Sabit zamanlı arama ile eşleme veya ilişkilendirilebilir veri yapısına ihtiyaç duyduğunuzda sözlükler harikadır. Ancak, yalnızca birkaç öğeniz olduğunda, bir sözlük kullanarak çok fazla alan harcarsınız. Bunun yerine, örneğin, yinelemeli olarak bir List<KeyValuePair\<K,V>>içinde de aynı hızla bakabilirsiniz. Bir sözlüğü yalnızca verilerle yüklemek ve ondan okumak için kullanıyorsanız (çok yaygın bir desen), N(log(N)) aramasıyla sıralanmış bir dizi kullanmak, kullandığınız öğe sayısına bağlı olarak neredeyse o kadar hızlı olabilir.

Sınıflar ve yapılar

Bir şekilde, sınıflar ve yapılar uygulamalarınızı ayarlamak için klasik bir alan/zaman dengeleri sağlar. Hiçbir alanı olmasa bile sınıflar x86 makinesinde 12 bayt ek yük oluşturur, ancak yalnızca bir sınıf örneğine başvurmak için bir işaretçi aldığından geçişleri ucuzdur. Yapılar kutulanmamışsa yığın ayırması gerektirmez, ancak büyük yapıları işlev bağımsız değişkenleri veya dönüş değerleri olarak geçirdiğinizde, yapıların tüm veri üyelerini atomik olarak kopyalamak CPU zaman alır. Yapıları döndüren özelliklere yapılan yinelenen çağrılara dikkat edin ve aşırı veri kopyalamayı önlemek için özelliğin değerini yerel değişkende önbelleğe alın.

Caches

Yaygın bir performans püf noktası, sonuçları önbelleğe almaktır. Ancak boyut sınırı veya atma ilkesi olmayan bir önbellek bellek sızıntısı olabilir. Büyük miktarda veriyi işlerken, önbelleklerde çok fazla bellek tutarsanız, çöp toplamanın önbelleğe alınmış aramalarınızın avantajlarını geçersiz kılabilir.

Bu makalede, özellikle büyük miktarda veri işleyen büyük sistemler veya sistemler için uygulamanızın yanıt hızını etkileyebilecek performans sorunu belirtileri hakkında nasıl bilgi edinmeniz gerektiğini ele aldık. Yaygın suçlular arasında kutulama, dize işlemeleri, LINQ ve lambda, zaman uyumsuz yöntemlerde önbelleğe alma, boyut sınırı veya atma ilkesi olmadan önbelleğe alma, sözlüklerin uygunsuz kullanımı ve yapıların geçirilmesi yer alır. Uygulamalarınızı ayarlamaya yönelik dört olguyu aklınızda bulundurun:

  • Erken iyileştirmeyin; üretken olun ve sorunları tespit ettiğinizde uygulamanızı ayarlayın.

  • Profiller yalan söylemez; ölçmediğiniz için tahminde bulunursunuz.

  • İyi araçlar tüm farkı yaratır – PerfView'ı indirin ve deneyin.

  • Her şey ayırmalarla ilgilidir; derleyici platformu ekibi, zamanının çoğunu yeni derleyicilerin performansını geliştirmek için harcamış olduğu yerdir.

Ayrıca bkz.