Praktik terbaik untuk meningkatkan kinerja dalam C #

Halo semuanya. Kami telah menyiapkan terjemahan materi lain yang bermanfaat pada awal kursus "Pengembang C #" . Selamat membaca.




Karena saya baru-baru ini membuat daftar praktik terbaik dalam C # untuk Criteo, saya pikir akan baik untuk membaginya secara publik. Tujuan artikel ini adalah untuk menyediakan daftar templat kode yang tidak lengkap yang harus dihindari, baik karena mereka dipertanyakan, atau karena mereka hanya bekerja dengan buruk. Daftar ini mungkin tampak agak acak karena sedikit dikeluarkan dari konteks, tetapi semua elemennya ada di beberapa titik yang ditemukan dalam kode kami dan menyebabkan masalah dalam produksi. Saya harap ini akan berfungsi sebagai pencegahan yang baik dan mencegah kesalahan Anda di masa depan.

Perhatikan juga bahwa layanan web Criteo bergantung pada kode berkinerja tinggi, maka kebutuhan untuk menghindari kode yang tidak efisien. Dalam sebagian besar aplikasi, tidak akan ada perbedaan nyata yang nyata dengan mengganti beberapa templat ini.

Dan yang terakhir, namun tidak kalah pentingnya, beberapa poin (misalnya, ConfigureAwait) telah dibahas dalam banyak artikel, jadi saya tidak akan membahasnya secara rinci. Tujuannya adalah untuk membentuk daftar poin yang ringkas yang perlu Anda perhatikan, dan tidak memberikan deskripsi teknis terperinci tentang masing-masing poin.

Sinkron menunggu kode asinkron


Jangan pernah mengharapkan tugas yang belum selesai secara sinkron. Hal ini berlaku untuk, tetapi tidak terbatas pada: Task.Wait, Task.Result, Task.GetAwaiter().GetResult(), Task.WaitAny, Task.WaitAll.

Sebagai generalisasi: hubungan yang sinkron antara dua utas pool dapat menyebabkan penipisan pool. Penyebab fenomena ini dijelaskan dalam artikel ini .

Konfigurasikan, Tunggu


Jika kode Anda dapat dipanggil dari konteks sinkronisasi, gunakan ConfigureAwait(false)untuk masing-masing panggilan tunggu Anda.

Harap perhatikan bahwa ini ConfigureAwait hanya berguna saat menggunakan kata kunci await.

Misalnya, kode berikut tidak ada artinya:

//  ConfigureAwait        
var result = ProcessAsync().ConfigureAwait(false).GetAwaiter().GetResult();

batal async


Jangan pernah gunakanasync void . Pengecualian yang dilemparkan dalam async voidmetode menyebar ke konteks sinkronisasi dan biasanya menyebabkan seluruh aplikasi macet.

Jika Anda tidak dapat mengembalikan tugas dalam metode Anda (misalnya, karena Anda mengimplementasikan antarmuka), pindahkan kode asinkron ke metode lain dan panggil:

interface IInterface
{
    void DoSomething();
}

class Implementation : IInterface
{
    public void DoSomething()
    {
        //      ,
        //      
        _ = DoSomethingAsync();
    }

    private async Task DoSomethingAsync()
    {
        await Task.Delay(100);
    }
}

Hindari async bila memungkinkan


Karena kebiasaan atau karena ingatan otot, Anda dapat menulis sesuatu seperti:

public async Task CallAsync()
{
    var client = new Client();
    return await client.GetAsync();
}

Meskipun kode secara semantik benar, menggunakan kata kunci asynctidak diperlukan di sini dan dapat menghasilkan overhead yang signifikan di lingkungan yang sangat dimuat. Cobalah untuk menghindarinya sedapat mungkin:

public Task CallAsync()
{
    var client = new Client();
    return _client.GetAsync();
}

Namun, ingatlah bahwa Anda tidak dapat menggunakan optimasi ini ketika kode Anda dibungkus dengan blok (misalnya, try/catchatau using):

public async Task Correct()
{
    using (var client = new Client())
    {
        return await client.GetAsync();
    }
}

public Task Incorrect()
{
    using (var client = new Client())
    {
        return client.GetAsync();
    }
}

Dalam versi yang salah ( Incorrect()), klien dapat dihapus sebelum GetAsyncpanggilan selesai , karena tugas di dalam blok menggunakan tidak diharapkan dengan menunggu.

Perbandingan regional


Jika Anda tidak memiliki alasan untuk menggunakan perbandingan regional, selalu gunakan perbandingan ordinal . Meskipun, karena optimasi internal, ini tidak masalah banyak untuk bentuk presentasi data en-AS, perbandingannya adalah urutan besarnya lebih lambat untuk bentuk presentasi daerah lain (dan hingga dua urutan besarnya di Linux!). Karena perbandingan string adalah operasi yang sering di sebagian besar aplikasi, overhead meningkat secara signifikan.

ConcurrentBag <T>


Jangan pernah gunakan ConcurrentBag<T>tanpa pembandingan . Koleksi ini dirancang untuk kasus penggunaan yang sangat spesifik (saat sebagian besar item dikecualikan dari antrian oleh utas yang mengantri) dan menderita masalah kinerja serius jika digunakan untuk tujuan lain. Jika Anda membutuhkan koleksi benang-aman, lebih memilih ConcurrentQueue <T> .

ReaderWriterLock / ReaderWriterLockSlim <T >
Jangan pernah gunakan tanpa pembandingan. ReaderWriterLock<T>/ReaderWriterLockSlim<T>Meskipun menggunakan jenis sinkronisasi khusus yang primitif saat bekerja dengan pembaca dan penulis bisa menggoda, biayanya jauh lebih tinggi daripada yang sederhana Monitor(digunakan dengan kata kunci lock). Jika jumlah pembaca yang melakukan bagian kritis pada saat yang sama tidak terlalu besar, konkurensi tidak akan cukup untuk menyerap peningkatan overhead, dan kode akan bekerja lebih buruk.

Lebih suka fungsi lambda daripada kelompok metode


Pertimbangkan kode berikut:

public IEnumerable<int> GetItems()
{
    return _list.Where(i => Filter(i));
}
private static bool Filter(int element)
{
    return i % 2 == 0;
}

Resharper menyarankan penulisan ulang kode tanpa fungsi lambda, yang mungkin terlihat sedikit lebih bersih:

public IEnumerable<int> GetItems()
{
    return _list.Where(Filter);
}
private static bool Filter(int element)
{
    return i % 2 == 0;
}

Sayangnya, ini mengarah pada alokasi memori dinamis untuk setiap panggilan. Bahkan, panggilan dikompilasi sebagai:

public IEnumerable<int> GetItems()
{
    return _list.Where(new Predicate<int>(Filter));
}
private static bool Filter(int element)
{
    return i % 2 == 0;
}

Ini dapat memiliki dampak yang signifikan terhadap kinerja jika kode ini dipanggil di bagian yang sarat muatan.

Menggunakan fungsi lambda memulai optimisasi kompiler, yang menyimpan delegasi di bidang statis, menghindari alokasi. Ini hanya berfungsi jika Filterstatis. Jika tidak, Anda dapat menyimpan sendiri delegasi:

private Predicate<int> _filter;

public Constructor()
{
    _filter = new Predicate<int>(Filter);
}

public IEnumerable<int> GetItems()
{
    return _list.Where(_filter);
}

private bool Filter(int element)
{
    return i % 2 == 0;
}

Konversi Enumerasi ke Strings


Memanggil Enum.ToStringdi .netcukup mahal, karena refleksi digunakan untuk mengubah dalam, dan memanggil metode virtual pada memprovokasi struktur kemasan. Ini harus dihindari sebisa mungkin.

Pencacahan seringkali dapat diganti dengan string konstan:

//       Numbers.One, Numbers.Two, ...
public enum Numbers
{
    One,
    Two,
    Three
}

public static class Numbers
{
    public const string One = "One";
    public const string Two = "Two";
    public const string Three = "Three";
}

Jika Anda benar-benar perlu menggunakan enumerasi, coba caching nilai yang dikonversi dalam kamus untuk mengamortisasi overhead.

Perbandingan Pencacahan


Catatan: ini tidak lagi relevan di .net core, sejak versi 2.1, optimasi dilakukan oleh JIT secara otomatis.

Saat menggunakan enumerasi sebagai flag, mungkin tergoda untuk menggunakan metode ini Enum.HasFlag:

[Flags]
public enum Options
{
    Option1 = 1,
    Option2 = 2,
    Option3 = 4
}

private Options _option;

public bool IsOption2Enabled()
{
    return _option.HasFlag(Options.Option2);
}

Kode ini menimbulkan dua paket dengan alokasi: satu untuk konversi Options.Option2ke Enum, dan yang lainnya untuk panggilan virtual HasFlaguntuk struktur. Ini membuat kode ini mahal secara tidak proporsional. Sebagai gantinya, Anda harus mengorbankan keterbacaan dan menggunakan operator biner:

public bool IsOption2Enabled()
{
    return (_option & Options.Option2) == Options.Option2;
}

Implementasi metode perbandingan untuk struktur


Saat menggunakan struktur dalam perbandingan (misalnya, ketika digunakan sebagai kunci untuk kamus), Anda perlu mengganti metode Equals/GetHashCode. Implementasi standar menggunakan refleksi dan sangat lambat. Implementasi yang dihasilkan oleh Resharper biasanya cukup bagus.

Anda dapat mempelajari lebih lanjut tentang ini di sini: devblogs.microsoft.com/premier-developer/performance-implication-of-default-struct-equality-in-c

Hindari pengemasan yang tidak tepat saat menggunakan struktur dengan antarmuka


Pertimbangkan kode berikut:

public class IntValue : IValue
{
}

public void DoStuff()
{
    var value = new IntValue();

    LogValue(value);
    SendValue(value);
}

public void SendValue(IValue value)
{
    // ...
}

public void LogValue(IValue value)
{
    // ...
}

Membuat IntValueterstruktur bisa menggoda untuk menghindari mengalokasikan memori dinamis. Tetapi karena AddValuedan SendValuediharapkan antarmuka, dan antarmuka memiliki semantik referensi, nilai akan dikemas dengan setiap panggilan, meniadakan manfaat dari "optimasi" ini. Bahkan, akan ada alokasi memori lebih banyak daripada jika itu IntValueadalah kelas, karena nilai akan dikemas secara independen untuk setiap panggilan.

Jika Anda menulis API dan mengharapkan beberapa nilai menjadi struktur, coba gunakan metode generik:

public struct IntValue : IValue
{
}

public void DoStuff()
{
    var value = new IntValue();

    LogValue(value);
    SendValue(value);
}

public void SendValue<T>(T value) where T : IValue
{
    // ...
}

public void LogValue<T>(T value) where T : IValue
{
    // ...
}

Meskipun konversi metode ini ke universal tampaknya tidak berguna pada pandangan pertama, sebenarnya memungkinkan Anda untuk menghindari pengemasan dengan alokasi dalam kasus ketika itu IntValueadalah struktur.

Pembatalan Langganan Berlalu Selalu Sebaris


Saat Anda membatalkan CancellationTokenSource, semua langganan akan dieksekusi di dalam utas saat ini. Hal ini dapat menyebabkan jeda yang tidak direncanakan atau bahkan kebuntuan tersirat.

var cts = new CancellationTokenSource();
cts.Token.Register(() => Thread.Sleep(5000));
cts.Cancel(); //     5 

Anda tidak bisa lepas dari perilaku ini. Karena itu, ketika membatalkan CancellationTokenSource, tanyakan pada diri Anda apakah Anda dapat dengan aman membiarkan utas Anda saat ini ditangkap. Jika jawabannya tidak, bungkus panggilan Canceldi dalam Task.Rununtuk menjalankannya di utas kolam.

Kelanjutan TaskCompletionSource sering kali sejalan


Seperti langganan CancellationToken, kelanjutan TaskCompletionSourcesering kali sejalan. Ini adalah optimasi yang baik, tetapi dapat menyebabkan kesalahan implisit. Misalnya, pertimbangkan program berikut:

class Program
{
    private static ManualResetEventSlim _mutex = new ManualResetEventSlim();
    
    public static async Task Deadlock()
    {
        await ProcessAsync();
        _mutex.Wait();
    }
    
    private static Task ProcessAsync()
    {
        var tcs = new TaskCompletionSource<bool>();
        
        Task.Run(() =>
        {
            Thread.Sleep(2000); //  - 
            tcs.SetResult(true);
            _mutex.Set();
        });
        
        return tcs.Task;
    }
    
    static void Main(string[] args)
    {
        Deadlock().Wait();
        Console.WriteLine("Will never get there");
    }
}

Panggilan tcs.SetResultmenyebabkan kelanjutan await ProcessAsync()untuk dijalankan di utas saat ini. Oleh karena itu, pernyataan _mutex.Wait()dieksekusi oleh utas yang sama dengan yang seharusnya dipanggil _mutex.Set(), yang mengarah ke jalan buntu. Ini dapat dihindari dengan melewati parameter TaskCreationsOptions.RunContinuationsAsynchronouslyc TaskCompletionSource.

Jika Anda tidak punya alasan untuk mengabaikannya, selalu gunakan opsi TaskCreationsOptions.RunContinuationsAsynchronouslysaat membuat TaskCompletionSource.

Hati-hati: kode juga akan mengkompilasi jika Anda menggunakan TaskContinuationOptions.RunContinuationsAsynchronouslygantinya TaskCreationOptions.RunContinuationsAsynchronously, tetapi parameter akan diabaikan, dan lanjutan masih akan inline. Ini adalah kesalahan yang sangat umum karena TaskContinuationOptionsmendahului TaskCreationOptionspelengkapan otomatis.

Task.Run / Task.Factory.StartNew


Jika Anda tidak memiliki alasan untuk menggunakannya Task.Factory.StartNew, selalu memilih Task.Rununtuk menjalankan tugas latar belakang. Task.Runmenggunakan nilai default yang lebih aman, dan yang lebih penting, itu secara otomatis membongkar tugas yang dikembalikan, yang dapat mencegah kesalahan yang tidak terlihat dengan metode asinkron. Pertimbangkan program berikut:

class Program
{
    public static async Task ProcessAsync()
    {
        await Task.Delay(2000);
        Console.WriteLine("Processing done");
    }
    
    static async Task Main(string[] args)
    {
        await Task.Factory.StartNew(ProcessAsync);
        Console.WriteLine("End of program");
        Console.ReadLine();
    }
}

Terlepas dari kemunculannya, Akhir program akan ditampilkan lebih awal dari Pemrosesan yang dilakukan. Ini karena Task.Factory.StartNewakan kembali Task<Task>, dan kode hanya mengharapkan tugas eksternal. Kode yang benar bisa berupa await Task.Factory.StartNew(ProcessAsync).Unwrap(), atau await Task.Run(ProcessAsync).

Hanya ada tiga kasus penggunaan yang valid Task.Factory.StartNew:

  • Menjalankan tugas di penjadwal lain.
  • Melakukan tugas dalam utas khusus (menggunakan TaskCreationOptions.LongRunning).
  • ( TaskCreationOptions.PreferFairness).



.



All Articles