أفضل الممارسات لتحسين الأداء في C #

تحية للجميع. لقد أعددنا ترجمة لمواد أخرى مفيدة عشية بدء الدورة "C # Developer" . استمتع بالقراءة.




نظرًا لأنني قمت مؤخرًا بوضع قائمة بأفضل الممارسات في C # لـ Criteo ، اعتقدت أنه سيكون من الجيد مشاركتها علنًا. الغرض من هذه المقالة هو توفير قائمة غير مكتملة من قوالب التعليمات البرمجية التي يجب تجنبها ، إما لأنها مشكوك فيها ، أو لأنها تعمل بشكل ضعيف. قد تبدو القائمة عشوائية بعض الشيء لأنها مأخوذة قليلاً من السياق ، ولكن تم العثور على جميع عناصرها في مرحلة ما في الكود الخاص بنا وتسببت في مشاكل في الإنتاج. آمل أن يكون هذا بمثابة منع جيد ومنع أخطائك في المستقبل.

لاحظ أيضًا أن خدمات Criteo للويب تعتمد على كود عالي الأداء ، وبالتالي الحاجة إلى تجنب التعليمات البرمجية غير الفعالة. في معظم التطبيقات ، لن يكون هناك اختلاف ملموس ملحوظ عن استبدال بعض هذه القوالب.

وأخيرًا وليس آخرًا ، ConfigureAwaitتمت بالفعل مناقشة بعض النقاط (على سبيل المثال ) في العديد من المقالات ، لذلك لن أتناولها بالتفصيل. الهدف هو تكوين قائمة مضغوطة من النقاط التي تحتاج إلى الانتباه إليها ، وعدم إعطاء وصف تقني مفصل لكل منها.

انتظار رمز غير متزامن بشكل متزامن


لا تتوقع مهام غير منتهية بشكل متزامن. وينطبق هذا، ولكن ليس على سبيل الحصر: Task.Wait, Task.Result, Task.GetAwaiter().GetResult(), Task.WaitAny, Task.WaitAll.

كتعميم: يمكن أن تتسبب أي علاقة متزامنة بين خيوط تجمع في استنفاد التجمع. يتم وصف أسباب هذه الظاهرة في هذه المقالة .

ConfigureAwait


إذا كان من الممكن استدعاء الرمز الخاص بك من سياق المزامنة ، فاستخدم ConfigureAwait(false)لكل من مكالماتك المنتظرة.

يرجى ملاحظة أن هذا ConfigureAwait مفيد فقط عند استخدام كلمة رئيسية await.

على سبيل المثال ، الكود التالي لا معنى له:

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

فراغ غير متزامن


لا تستخدم أبدًاasync void . الاستثناء الذي تم طرحه في async voidالطريقة ينتشر إلى سياق المزامنة وعادة ما يتسبب في تعطل التطبيق بالكامل.

إذا لم تتمكن من إرجاع المهمة في طريقتك (على سبيل المثال ، لأنك تقوم بتنفيذ الواجهة) ، انقل الرمز غير المتزامن إلى طريقة أخرى واستدعها:

interface IInterface
{
    void DoSomething();
}

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

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

تجنب التزامن كلما أمكن ذلك


بسبب العادة أو بسبب الذاكرة العضلية ، يمكنك كتابة شيء مثل:

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

على الرغم من أن الشفرة صحيحة دلالة ، فإن استخدام كلمة رئيسية asyncغير مطلوب هنا ويمكن أن يؤدي إلى زيادة كبيرة في البيئة المحملة للغاية. حاول تجنبه كلما أمكن:

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

ومع ذلك ، ضع في اعتبارك أنه لا يمكنك اللجوء إلى هذا التحسين عندما يتم تغليف التعليمات البرمجية الخاصة بك في كتل (على سبيل المثال ، try/catchأو 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();
    }
}

في النسخة الخاطئة ( Incorrect()) ، قد يتم حذف العميل قبل إتمام GetAsyncالمكالمة ، لأن المهمة داخل كتلة الاستخدام غير متوقعة في الانتظار.

المقارنات الإقليمية


إذا لم يكن لديك سبب لاستخدام المقارنات الإقليمية ، فاستخدم المقارنات الترتيبية دائمًا . على الرغم من ذلك ، نظرًا للتحسينات الداخلية ، فإن هذا لا يهم كثيرًا بالنسبة لأشكال عرض البيانات في الولايات المتحدة ، فإن المقارنة هي ترتيب أبطأ لأشكال عرض المناطق الأخرى (وما يصل إلى أمرين من الحجم على Linux!). نظرًا لأن مقارنة السلاسل هي عملية متكررة في معظم التطبيقات ، يزداد الحمل بشكل كبير.

ConcurrentBag <T>


لا تستخدم أبدًا ConcurrentBag<T>بدون قياس الأداء . تم تصميم هذه المجموعة لحالات استخدام محددة للغاية (عندما يتم استبعاد العنصر في معظم الوقت من قائمة الانتظار بواسطة مؤشر الترابط الذي وضعه في قائمة الانتظار) ويعاني من مشكلات خطيرة في الأداء إذا تم استخدامه لأغراض أخرى. إذا كنت في حاجة الى جمع ذات ألوان أو تفضل ConcurrentQueue <T> .

ReaderWriterLock / ReaderWriterLockSlim <T >
لا تستخدم أبدًا بدون قياس الأداء. ReaderWriterLock<T>/ReaderWriterLockSlim<T>على الرغم من أن استخدام هذا النوع من المزامنة المتخصصة البدائية عند العمل مع القراء والكتاب يمكن أن يكون مغريًا ، إلا أن تكلفته أعلى بكثير من التكلفة البسيطة Monitor(المستخدمة مع الكلمة الرئيسية lock). إذا لم يكن عدد القراء الذين يؤدون القسم الحاسم في نفس الوقت كبيرًا جدًا ، فلن يكون التزامن كافيًا لامتصاص زيادة الحمل ، وسيعمل الرمز بشكل أسوأ.

تفضل وظائف لامدا بدلا من مجموعات الطريقة


خذ بعين الاعتبار الرمز التالي:

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

يقترح Resharper إعادة كتابة الرمز بدون وظيفة لامدا ، والتي قد تبدو أكثر نظافة:

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

لسوء الحظ ، يؤدي هذا إلى تخصيص ذاكرة ديناميكية لكل مكالمة. في الواقع ، يتم تجميع المكالمة على النحو التالي:

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

يمكن أن يكون لهذا تأثير كبير على الأداء إذا تم استدعاء الرمز في قسم تم تحميله بكثرة.

يبدأ استخدام وظائف lambda بتحسين برنامج التحويل البرمجي ، والذي يخزن المفوض في ذاكرة التخزين المؤقت في حقل ثابت ، مع تجنب التخصيص. هذا يعمل فقط إذا كانت Filterثابتة. إذا لم يكن الأمر كذلك ، يمكنك التخزين المؤقت للمفوض بنفسك:

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

تحويل التعدادات إلى سلاسل


داعيا Enum.ToStringفي .netغير مكلفة للغاية، لأنه يتم استخدام انعكاس لتحويل الداخل، واستدعاء الأسلوب الظاهري على يستفز هيكل التعبئة والتغليف. يجب تجنب هذا قدر الإمكان.

غالبًا ما يمكن استبدال التعداد بسلاسل ثابتة:

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

إذا كنت تحتاج حقًا إلى استخدام التعداد ، ففكر في تخزين القيمة المحولة في القاموس في ذاكرة التخزين المؤقت لاستهلاك النفقات العامة.

مقارنة التعداد


ملاحظة: هذا لم يعد مناسبًا في .net core ، نظرًا لأن الإصدار 2.1 ، يتم تنفيذ التحسين بواسطة JIT تلقائيًا.

عند استخدام التعداد كعلامات ، قد يكون من المغري استخدام الطريقة Enum.HasFlag:

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

private Options _option;

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

يثير هذا الرمز حزمتين مع تخصيص: واحدة للتحويل Options.Option2إلى Enum، والأخرى لمكالمة افتراضية HasFlagللبنية. هذا يجعل هذا الرمز باهظ الثمن بشكل غير متناسب. بدلاً من ذلك ، يجب عليك التضحية بإمكانية القراءة واستخدام عوامل التشغيل الثنائية:

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

تنفيذ طرق المقارنة للهياكل


عند استخدام بنية في المقارنات (على سبيل المثال ، عند استخدامها كمفتاح لقاموس) ، تحتاج إلى تجاوز الأساليب Equals/GetHashCode. يستخدم التطبيق الافتراضي الانعكاس وهو بطيء جدًا. عادةً ما يكون التنفيذ الناتج عن Resharper جيدًا جدًا.

يمكنك معرفة المزيد حول هذا الأمر هنا: devblogs.microsoft.com/premier-developer/performance-implications-of-default-struct-equality-in-c

تجنب التغليف غير المناسب عند استخدام الهياكل ذات الواجهات


خذ بعين الاعتبار الرمز التالي:

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)
{
    // ...
}

IntValueيمكن أن يكون إنشاء الهيكل مغريًا لتجنب تخصيص ذاكرة ديناميكية. ولكن منذ AddValueو SendValueيتوقع واجهة، واجهات لها دلالات مرجعية، وسوف تكون معبأة قيمة مع كل مكالمة، يلغي فوائد هذا "التحسين". في الواقع ، سيكون هناك تخصيصات ذاكرة أكبر مما لو IntValueكانت فئة ، حيث سيتم تجميع القيمة بشكل مستقل لكل مكالمة.

إذا كنت تكتب واجهة برمجة تطبيقات وتتوقع أن تكون بعض القيم هياكل ، فجرّب استخدام طرق عامة:

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
{
    // ...
}

على الرغم من أن تحويل هذه الأساليب إلى عالمي يبدو عديم الفائدة للوهلة الأولى ، إلا أنه يسمح لك في الواقع بتجنب التعبئة مع التخصيص في الحالة عندما IntValueيكون هيكلًا.

إلغاء الاشتراكات المنطوقة مضمنة دائمًا


عند الإلغاء CancellationTokenSource، سيتم تنفيذ جميع الاشتراكات داخل سلسلة المحادثات الحالية. يمكن أن يؤدي هذا إلى فترات توقف غير مخطط لها أو حتى جمود ضمني.

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

لا يمكنك الهروب من هذا السلوك. لذلك ، عند الإلغاء CancellationTokenSource، اسأل نفسك عما إذا كان يمكنك السماح بالتقاط خيطك الحالي بأمان. إذا كانت الإجابة لا ، فلف المكالمة Cancelبالداخل Task.Runلتنفيذها في تجمع سلاسل المحادثات.

غالبًا ما تكون عمليات متابعة TaskCompletionSource مضمنة


مثل الاشتراكات CancellationToken، TaskCompletionSourceغالبًا ما تكون عمليات المتابعة مضمنة. يعد هذا تحسينًا جيدًا ، ولكن يمكن أن يسبب أخطاء ضمنية. على سبيل المثال ، خذ بعين الاعتبار البرنامج التالي:

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

tcs.SetResultتؤدي المكالمة إلى استمرار await ProcessAsync()التنفيذ في مؤشر الترابط الحالي. لذلك ، _mutex.Wait()يتم تنفيذ العبارة بواسطة نفس مؤشر الترابط الذي يجب أن يستدعيه _mutex.Set()، مما يؤدي إلى طريق مسدود. يمكن تجنب ذلك عن طريق تمرير المعلمة TaskCreationsOptions.RunContinuationsAsynchronouslyc TaskCompletionSource.

إذا لم يكن لديك سبب وجيه لإهماله ، فاستخدم الخيار دائمًا TaskCreationsOptions.RunContinuationsAsynchronouslyعند الإنشاء TaskCompletionSource.

كن حذرًا: سيتم أيضًا تجميع التعليمات البرمجية إذا كنت تستخدم TaskContinuationOptions.RunContinuationsAsynchronouslyبدلاً من ذلك TaskCreationOptions.RunContinuationsAsynchronously، ولكن سيتم تجاهل المعلمات ، وستظل عمليات الاستمرارية مضمنة. هذا خطأ شائع بشكل مدهش لأنه TaskContinuationOptionsيسبق TaskCreationOptionsالإكمال التلقائي.

Task.Run / Task.Factory.StartNew


إذا لم يكن لديك سبب لاستخدامه Task.Factory.StartNew، فاختر دائمًا Task.Runتشغيل مهمة في الخلفية. Task.Runيستخدم قيمًا افتراضية أكثر أمانًا ، والأهم من ذلك أنه يقوم تلقائيًا بتفريغ المهمة التي تم إرجاعها ، والتي يمكن أن تمنع الأخطاء غير الواضحة بالطرق غير المتزامنة. خذ بعين الاعتبار البرنامج التالي:

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

على الرغم من مظهره ، سيتم عرض نهاية البرنامج قبل الانتهاء من المعالجة. هذا لأنه Task.Factory.StartNewسيعود Task<Task>، ويتوقع الرمز مهمة خارجية فقط. قد يكون الرمز الصحيح إما await Task.Factory.StartNew(ProcessAsync).Unwrap()، أو await Task.Run(ProcessAsync).

هناك ثلاث حالات استخدام صالحة فقط Task.Factory.StartNew:

  • تشغيل مهمة في برنامج جدولة آخر.
  • تنفيذ مهمة في خيط مخصص (باستخدام TaskCreationOptions.LongRunning).
  • ( TaskCreationOptions.PreferFairness).



.



All Articles