سؤال

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

لقد أجريت بعض الاختبارات للتحقق من فارق الأداء ولم أجد شيئًا.أفعل شيئا خاطئا؟هل أفتقد الحالة التي تعطي فيها الفصول المختومة نتائج أفضل؟

هل قام أحد بإجراء الاختبارات ورأى الفرق؟

ساعدني على التعلم :)

هل كانت مفيدة؟

المحلول

سيستخدم JITter أحيانًا مكالمات غير افتراضية للطرق الموجودة في الفئات المختومة نظرًا لعدم وجود طريقة يمكن تمديدها بشكل أكبر.

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

لاحظ أن أي نوع من فوائد الأداء التي قد تحصل عليها من هذا المستوى من التحسين يجب اعتباره الملاذ الأخير، وقم دائمًا بالتحسين على مستوى الخوارزمية قبل التحسين على مستوى التعليمات البرمجية.

إليك رابط واحد يذكر ذلك: التجول على الكلمة الأساسية المختومة

نصائح أخرى

الجواب هو لا، فالفئات المختومة لا تعمل بشكل أفضل من غير المختومة.

المسألة تأتي إلى call ضد callvirt رموز العمليات IL. Call أسرع من callvirt, ، و callvirt يتم استخدامه بشكل أساسي عندما لا تعرف ما إذا كان الكائن قد تم تصنيفه ضمن فئة فرعية.لذلك يفترض الناس أنه إذا قمت بإغلاق فصل دراسي، فستتغير جميع رموز التشغيل منه calvirts ل calls وسوف يكون أسرع.

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

استخدام الهياكل call لأنه لا يمكن تصنيفها ضمن فئات فرعية ولا تكون فارغة أبدًا.

انظر هذا السؤال لمزيد من المعلومات:

اتصل و callvirt

تحديث:اعتبارًا من .NET Core 2.0 و.NET Desktop 4.7.1، يدعم CLR الآن عملية التحول الافتراضي.يمكنه استخدام أساليب في الفصول المغلقة واستبدال المكالمات الافتراضية بالمكالمات المباشرة - ويمكنه أيضًا القيام بذلك للفصول غير المغلقة إذا استطاع معرفة أن القيام بذلك آمن.

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

ومع ذلك، لا أعتقد أن الأمر يستحق القلق بشأنه إلا إذا لقد قمت بالفعل بتكوين ملف تعريف للكود وقررت أنك في مسار ساخن بشكل خاص حيث يتم الاتصال بك ملايين المرات، أو شيء من هذا القبيل:

https://blogs.msdn.microsoft.com/dotnet/2017/06/29/performance-improvements-in-ryujit-in-net-core-and-net-framework/


الجواب الأصلي:

لقد قمت بإنشاء برنامج الاختبار التالي، ثم قمت بفك ترجمته باستخدام Reflector لمعرفة كود MSIL الذي تم إصداره.

public class NormalClass {
    public void WriteIt(string x) {
        Console.WriteLine("NormalClass");
        Console.WriteLine(x);
    }
}

public sealed class SealedClass {
    public void WriteIt(string x) {
        Console.WriteLine("SealedClass");
        Console.WriteLine(x);
    }
}

public static void CallNormal() {
    var n = new NormalClass();
    n.WriteIt("a string");
}

public static void CallSealed() {
    var n = new SealedClass();
    n.WriteIt("a string");
}

في جميع الحالات، يقوم برنامج التحويل البرمجي C# (Visual studio 2010 في تكوين إصدار الإصدار) بإصدار MSIL مماثل، وهو كما يلي:

L_0000: newobj instance void <NormalClass or SealedClass>::.ctor()
L_0005: stloc.0 
L_0006: ldloc.0 
L_0007: ldstr "a string"
L_000c: callvirt instance void <NormalClass or SealedClass>::WriteIt(string)
L_0011: ret 

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

فكرتي التالية كانت أنه على الرغم من تطابق MSIL، ربما يتعامل مترجم JIT مع الفئات المختومة بشكل مختلف؟

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

//            var n = new NormalClass();
00000000  push        ebp 
00000001  mov         ebp,esp 
00000003  sub         esp,8 
00000006  cmp         dword ptr ds:[00585314h],0 
0000000d  je          00000014 
0000000f  call        70032C33 
00000014  xor         edx,edx 
00000016  mov         dword ptr [ebp-4],edx 
00000019  mov         ecx,588230h 
0000001e  call        FFEEEBC0 
00000023  mov         dword ptr [ebp-8],eax 
00000026  mov         ecx,dword ptr [ebp-8] 
00000029  call        dword ptr ds:[00588260h] 
0000002f  mov         eax,dword ptr [ebp-8] 
00000032  mov         dword ptr [ebp-4],eax 
//            n.WriteIt("a string");
00000035  mov         edx,dword ptr ds:[033220DCh] 
0000003b  mov         ecx,dword ptr [ebp-4] 
0000003e  cmp         dword ptr [ecx],ecx 
00000040  call        dword ptr ds:[0058827Ch] 
//        }
00000046  nop 
00000047  mov         esp,ebp 
00000049  pop         ebp 
0000004a  ret 

اعتقدت بعد ذلك أن التشغيل تحت مصحح الأخطاء قد يؤدي إلى إجراء تحسين أقل قوة؟

قمت بعد ذلك بتشغيل إصدار مستقل قابل للتنفيذ خارج أي بيئات تصحيح الأخطاء، واستخدمت WinDBG + SOS للاختراق بعد اكتمال البرنامج، وعرض تفكيك كود x86 المترجم من JIT.

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

هنا عند استدعاء الفصل العادي:

Normal JIT generated code
Begin 003c00b0, size 39
003c00b0 55              push    ebp
003c00b1 8bec            mov     ebp,esp
003c00b3 b994391800      mov     ecx,183994h (MT: ScratchConsoleApplicationFX4.NormalClass)
003c00b8 e8631fdbff      call    00172020 (JitHelp: CORINFO_HELP_NEWSFAST)
003c00bd e80e70106f      call    mscorlib_ni+0x2570d0 (6f4c70d0) (System.Console.get_Out(), mdToken: 060008fd)
003c00c2 8bc8            mov     ecx,eax
003c00c4 8b1530203003    mov     edx,dword ptr ds:[3302030h] ("NormalClass")
003c00ca 8b01            mov     eax,dword ptr [ecx]
003c00cc 8b403c          mov     eax,dword ptr [eax+3Ch]
003c00cf ff5010          call    dword ptr [eax+10h]
003c00d2 e8f96f106f      call    mscorlib_ni+0x2570d0 (6f4c70d0) (System.Console.get_Out(), mdToken: 060008fd)
003c00d7 8bc8            mov     ecx,eax
003c00d9 8b1534203003    mov     edx,dword ptr ds:[3302034h] ("a string")
003c00df 8b01            mov     eax,dword ptr [ecx]
003c00e1 8b403c          mov     eax,dword ptr [eax+3Ch]
003c00e4 ff5010          call    dword ptr [eax+10h]
003c00e7 5d              pop     ebp
003c00e8 c3              ret

مقابل فئة مختومة:

Normal JIT generated code
Begin 003c0100, size 39
003c0100 55              push    ebp
003c0101 8bec            mov     ebp,esp
003c0103 b90c3a1800      mov     ecx,183A0Ch (MT: ScratchConsoleApplicationFX4.SealedClass)
003c0108 e8131fdbff      call    00172020 (JitHelp: CORINFO_HELP_NEWSFAST)
003c010d e8be6f106f      call    mscorlib_ni+0x2570d0 (6f4c70d0) (System.Console.get_Out(), mdToken: 060008fd)
003c0112 8bc8            mov     ecx,eax
003c0114 8b1538203003    mov     edx,dword ptr ds:[3302038h] ("SealedClass")
003c011a 8b01            mov     eax,dword ptr [ecx]
003c011c 8b403c          mov     eax,dword ptr [eax+3Ch]
003c011f ff5010          call    dword ptr [eax+10h]
003c0122 e8a96f106f      call    mscorlib_ni+0x2570d0 (6f4c70d0) (System.Console.get_Out(), mdToken: 060008fd)
003c0127 8bc8            mov     ecx,eax
003c0129 8b1534203003    mov     edx,dword ptr ds:[3302034h] ("a string")
003c012f 8b01            mov     eax,dword ptr [ecx]
003c0131 8b403c          mov     eax,dword ptr [eax+3Ch]
003c0134 ff5010          call    dword ptr [eax+10h]
003c0137 5d              pop     ebp
003c0138 c3              ret

بالنسبة لي، هذا يوفر دليلا قويا على أن هناك لا تستطيع هل هناك أي تحسين في الأداء بين طرق الاتصال على الفئات المختومة وغير المختومة ...أعتقد أنني سعيد الآن :-)

وكما أعلم، ليس هناك ضمان لفائدة الأداء.ولكن هناك فرصة لتقليل عقوبة الأداء في ظل ظروف معينة مع طريقة مختومة.(الطبقة المختومة تجعل جميع الطرق مغلقة.)

لكن الأمر متروك لتطبيق المترجم وبيئة التنفيذ.


تفاصيل

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

هناك عقبة كبيرة تسمى إرسال ديناميكي مما يعطل هذا التحسين "الجلب المسبق".يمكنك فهم هذا على أنه مجرد تفرع مشروط.

// Value of `v` is unknown,
// and can be resolved only at runtime.
// CPU cannot know which code to prefetch.
// Therefore, just prefetch any one of a() or b().
// This is *speculative execution*.
int v = random();
if (v==1) a();
else b();

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

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

تستخدم بعض وحدات المعالجة المركزية (بما في ذلك شرائح Intel x86 الحديثة) تقنية تسمى تنفيذ المضاربة للاستفادة من خط الأنابيب حتى على الوضع.فقط قم بإحضار أحد مسارات التنفيذ مسبقًا.ولكن معدل ضرب هذه التقنية ليس مرتفعا جدا.ويتسبب فشل المضاربة في توقف خط الأنابيب مما يؤدي أيضًا إلى فرض عقوبات كبيرة على الأداء.(وهذا بالكامل عن طريق تنفيذ وحدة المعالجة المركزية.تُعرف بعض وحدات المعالجة المركزية المحمولة بأنها لا تحتوي على هذا النوع من التحسين لتوفير الطاقة)

في الأساس، C# هي لغة مجمعة بشكل ثابت.لكن ليس دائما.لا أعرف الحالة الدقيقة وهذا الأمر متروك تمامًا لتنفيذ المترجم.يمكن لبعض المترجمين استبعاد إمكانية الإرسال الديناميكي عن طريق منع تجاوز الطريقة إذا تم وضع علامة على الطريقة على أنها sealed.المترجمون الأغبياء قد لا يفعلون ذلك.هذه هي فائدة الأداء sealed.


هذه الإجابة (لماذا تكون معالجة المصفوفة المصنفة أسرع من معالجة المصفوفة غير المصنفة؟) يصف التنبؤ بالفرع بشكل أفضل كثيرًا.

وضع علامة على فئة sealed يجب ألا يكون لها أي تأثير على الأداء.

هناك حالات حيث csc قد تضطر إلى إصدار أ callvirt رمز التشغيل بدلاً من A call كود التشغيل.ومع ذلك، يبدو أن هذه الحالات نادرة.

ويبدو لي أن JIT يجب أن يكون قادرًا على إصدار نفس استدعاء الوظيفة غير الافتراضية callvirt أنه من أجل call, ، إذا كان يعلم أن الفصل لا يحتوي على أي فئات فرعية (حتى الآن).في حالة وجود تطبيق واحد فقط للأسلوب، فلا فائدة من تحميل عنوانه من جدول vtable، فقط قم باستدعاء التطبيق الواحد مباشرة.في هذا الصدد، يمكن لـ JIT أيضًا تضمين الوظيفة.

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

(ونعم، مصممو الأجهزة الافتراضية يتابعون بقوة هذه المكاسب الصغيرة في الأداء.)

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

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

<خارج الموضوع>

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

مثال: موضوع آمن اضطررت إلى تغليف فئة Thread لأن Thread مغلق ولا توجد واجهة IThread؛يقوم SafeThread تلقائيًا بملاءمة الاستثناءات غير المعالجة على سلاسل الرسائل، وهو شيء مفقود تمامًا من فئة Thread.[ولا، أحداث الاستثناء غير المعالجة تفعل ذلك لا التقاط الاستثناءات غير المعالجة في المواضيع الثانوية].

</خارج الموضوع-رانت>

أنا أعتبر الفئات "المختومة" هي الحالة العادية ولدي دائمًا سبب لحذف الكلمة الأساسية "المختومة".

أهم الأسباب بالنسبة لي هي:

أ) فحوصات أفضل لوقت الترجمة (سيتم اكتشاف الإرسال إلى الواجهات التي لم يتم تنفيذها في وقت الترجمة، وليس فقط في وقت التشغيل)

والسبب الرئيسي:

ب) إن إساءة استخدام فصولي أمر غير ممكن بهذه الطريقة

أتمنى أن تجعل Microsoft "مختومًا" هو المعيار وليس "مكشوفًا".

@Vaibhav، ما نوع الاختبارات التي قمت بتنفيذها لقياس الأداء؟

أعتقد أنه يجب على المرء أن يستخدم الدوار وللتعمق في واجهة سطر الأوامر (CLI) وفهم كيف يمكن للفصل المختوم تحسين الأداء.

SSCLI (الدوار)
سكلي:البنية التحتية للغة المشتركة المصدر

البنية التحتية اللغوية الشائعة (CLI) هي معيار ECMA الذي يصف جوهر إطار .NET.المصدر المشترك CLI (SSCLI) ، والمعروف أيضًا باسم الدوار ، هو أرشيف مضغوط من الكود المصدر لتنفيذ العمل لـ ECMA CLI ومواصفات لغة ECMA C# ، والتقنيات في قلب الهندسة المعمارية .NET من Microsoft.

ستكون الفصول المغلقة أسرع قليلاً على الأقل، ولكن في بعض الأحيان يمكن أن تكون أسرع بكثير...إذا كان بإمكان JIT Optimizer تضمين المكالمات التي كانت ستكون مكالمات افتراضية.لذلك، عندما تكون هناك طرق تسمى في كثير من الأحيان صغيرة بما يكفي لتضمينها، فكر بالتأكيد في إغلاق الفصل.

ومع ذلك، فإن أفضل سبب لإغلاق الفصل الدراسي هو القول "لم أصمم هذا ليكون موروثًا منه، لذلك لن أسمح لك بالحرق بافتراض أنه تم تصميمه ليكون كذلك، ولن أفعل ذلك". لأحرق نفسي من خلال الانغلاق على التنفيذ لأنني سمحت لك بالاشتقاق منه."

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

قم بتشغيل هذا الكود وسترى أن الفئات المختومة أسرع مرتين:

class Program
{
    static void Main(string[] args)
    {
        Console.ReadLine();

        var watch = new Stopwatch();
        watch.Start();
        for (int i = 0; i < 10000000; i++)
        {
            new SealedClass().GetName();
        }
        watch.Stop();
        Console.WriteLine("Sealed class : {0}", watch.Elapsed.ToString());

        watch.Start();
        for (int i = 0; i < 10000000; i++)
        {
            new NonSealedClass().GetName();
        }
        watch.Stop();
        Console.WriteLine("NonSealed class : {0}", watch.Elapsed.ToString());

        Console.ReadKey();
    }
}

sealed class SealedClass
{
    public string GetName()
    {
        return "SealedClass";
    }
}

class NonSealedClass
{
    public string GetName()
    {
        return "NonSealedClass";
    }
}

انتاج:فئة مختومة:00: 00: 00.1897568 فئة غير محققة:00:00:00.3826678

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top