هل تقدم الفصول المختومة بالفعل فوائد الأداء؟
-
08-06-2019 - |
سؤال
لقد صادفت الكثير من نصائح التحسين التي تنص على أنه يجب عليك وضع علامة على فصولك الدراسية على أنها مختومة للحصول على مزايا أداء إضافية.
لقد أجريت بعض الاختبارات للتحقق من فارق الأداء ولم أجد شيئًا.أفعل شيئا خاطئا؟هل أفتقد الحالة التي تعطي فيها الفصول المختومة نتائج أفضل؟
هل قام أحد بإجراء الاختبارات ورأى الفرق؟
ساعدني على التعلم :)
المحلول
سيستخدم JITter أحيانًا مكالمات غير افتراضية للطرق الموجودة في الفئات المختومة نظرًا لعدم وجود طريقة يمكن تمديدها بشكل أكبر.
هناك قواعد معقدة فيما يتعلق بنوع الاتصال، افتراضي/غير افتراضي، وأنا لا أعرفها جميعًا لذا لا يمكنني تحديدها لك حقًا، ولكن إذا كنت تبحث في Google عن الفصول المختومة والأساليب الافتراضية، فقد تجد بعض المقالات حول هذا الموضوع.
لاحظ أن أي نوع من فوائد الأداء التي قد تحصل عليها من هذا المستوى من التحسين يجب اعتباره الملاذ الأخير، وقم دائمًا بالتحسين على مستوى الخوارزمية قبل التحسين على مستوى التعليمات البرمجية.
إليك رابط واحد يذكر ذلك: التجول على الكلمة الأساسية المختومة
نصائح أخرى
الجواب هو لا، فالفئات المختومة لا تعمل بشكل أفضل من غير المختومة.
المسألة تأتي إلى call
ضد callvirt
رموز العمليات IL. Call
أسرع من callvirt
, ، و callvirt
يتم استخدامه بشكل أساسي عندما لا تعرف ما إذا كان الكائن قد تم تصنيفه ضمن فئة فرعية.لذلك يفترض الناس أنه إذا قمت بإغلاق فصل دراسي، فستتغير جميع رموز التشغيل منه calvirts
ل calls
وسوف يكون أسرع.
للأسف callvirt
يقوم بأشياء أخرى تجعله مفيدًا أيضًا، مثل التحقق من المراجع الخالية.وهذا يعني أنه حتى لو تم إغلاق الفصل، فقد يظل المرجع فارغًا وبالتالي callvirt
وهناك حاجة.يمكنك الالتفاف حول هذا (دون الحاجة إلى إغلاق الفصل)، لكنه يصبح بلا معنى بعض الشيء.
استخدام الهياكل call
لأنه لا يمكن تصنيفها ضمن فئات فرعية ولا تكون فارغة أبدًا.
انظر هذا السؤال لمزيد من المعلومات:
تحديث:اعتبارًا من .NET Core 2.0 و.NET Desktop 4.7.1، يدعم CLR الآن عملية التحول الافتراضي.يمكنه استخدام أساليب في الفصول المغلقة واستبدال المكالمات الافتراضية بالمكالمات المباشرة - ويمكنه أيضًا القيام بذلك للفصول غير المغلقة إذا استطاع معرفة أن القيام بذلك آمن.
في مثل هذه الحالة (فئة مختومة لا يمكن لـ CLR اكتشافها على أنها آمنة للتحول الافتراضي)، يجب أن تقدم الفئة المختومة نوعًا من فوائد الأداء.
ومع ذلك، لا أعتقد أن الأمر يستحق القلق بشأنه إلا إذا لقد قمت بالفعل بتكوين ملف تعريف للكود وقررت أنك في مسار ساخن بشكل خاص حيث يتم الاتصال بك ملايين المرات، أو شيء من هذا القبيل:
الجواب الأصلي:
لقد قمت بإنشاء برنامج الاختبار التالي، ثم قمت بفك ترجمته باستخدام 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