قبل بضعة أيام، تم نشر مقالات N تقول أن c#/.net بطيء جدًا وتطالب بحذف بعض ميزات c#/.net.
بغض النظر عن هذه المقالات، يبدو أن هناك قاعدة صارمة معترف بها في الصناعة مفادها أن c#/.net بطيء. وبغض النظر عن الطريقة التي أثبت بها الجميع أن c#/.net ليس أبطأ بكثير من c++، فإن الأداء على مستوى التطبيق لا يزال بطيئًا للغاية. .
فأين هو بطيء c#/.net؟
لسوء الحظ، يتم إبطاء معظم برامج C# من قبل معظم المبرمجين. قد لا يكون من السهل قبول هذا الاستنتاج، ولكنه منتشر على نطاق واسع.
عمليات السلسلة
تحتوي جميع البرامج تقريبًا على عمليات سلسلة، ويحتاج 90% منها على الأقل إلى تجاهل مقارنات الحالات، والتحقق من الكود الذي يحتوي على نصفها على الأقل على رمز مشابه لما يلي:
إذا (str1.ToUpper() == str2.ToUpper())
أو إصدار ToLower، حتى أنني رأيت أن هناك Web HttpModule يقول:
لـ (int i = 0; i < strs.Count; i++)
إذا (value.ToUpper() == strs[i].ToUpper())
//...
فكر في الأمر، في كل مرة يتم فيها طلب صفحة، يجب تنفيذ هذا الجزء من التعليمات البرمجية لإنشاء مثيلات سلسلة في مساحات كبيرة. والأمر الأكثر مبالغة هو أن بعض الأشخاص يقولون إن هذا يستبدل المساحة بالوقت. . .
اختبار الأداء
إذا قيل أن هذه الطريقة بطيئة، فقد لا يعترف البعض بذلك ويعتقدون أن هذه هي الطريقة الأفضل، لذلك نحتاج هنا إلى استخدام اختبارات محددة لإثبات الحقيقة.
أولاً قم بإعداد طريقة لاختبار الأداء:
TResult MeasurePerformance ثابت خاص<TArg, TResult>(Func<TArg, TResult> func, TArg arg, int حلقة)
{
GC.Collect();
int gc0 = GC.CollectionCount(0);
int gc1 = GC.CollectionCount(1);
int gc2 = GC.CollectionCount(2);
نتيجة TResult = default(TResult);
ساعة الإيقاف sw = Stopwatch.StartNew();
ل(int i = 0; i < حلقة; i++)
{
النتيجة = func(arg);
}
Console.WriteLine(sw.ElapsedMillithans.ToString() + "ms");
Console.WriteLine("GC 0:" + (GC.CollectionCount(0) - gc0).ToString());
Console.WriteLine("GC 1:" + (GC.CollectionCount(1) - gc1).ToString());
Console.WriteLine("GC 2:" + (GC.CollectionCount(2) - gc2).ToString());
نتيجة الإرجاع؛
}
ثم قم بإعداد سلسلة كومة:
قائمة ثابتة خاصة<string> CreateStrings()
{
List<string> strs = new List<string>(10000);
char[] chs = new char[3];
لـ (int i = 0; i < 10000; i++)
{
كثافة العمليات ي = أنا؛
لـ (int k = 0; k < chs.Length; k++)
{
chs[k] = (char)('a' + j % 26);
ي = ي / 26؛
}
strs.Add(new string(chs));
}
سلسلة العودة؛
}
ثم دعونا نلقي نظرة على تنفيذ ToUpper:
منطقي ثابت خاص ImplementByToUpper (قائمة <سلسلة> سلسلة، قيمة السلسلة)
{
لـ (int i = 0; i < strs.Count; i++)
إذا (value.ToUpper() == strs[i].ToUpper())
عودة صحيحة؛
عودة كاذبة.
}
أخيرًا قم بإعداد الطريقة الرئيسية:
List<string> strs = CreateStrings();
نتيجة منطقية؛
Console.WriteLine("استخدم ImplementByToUpper");
النتيجة = MeasurePerformance(s => ImplementByToUpper(strs, s), "yZh", 1000);
Console.WriteLine("النتيجة هي" + result.ToString());
Console.ReadLine();
دعونا نلقي نظرة على نتائج التنفيذ:
استخدم ImplementByToUpper
2192 مللي ثانية
ج.ك 0:247
جي سي 1:0
جي سي 2:0
النتيجة صحيحة
لنجري اختبارًا مقارنًا ونستخدم string.Equals للاختبار:
منطقي ثابت خاص ImplementByStringEquals (قائمة <سلسلة> سلسلة، قيمة السلسلة)
{
لـ (int i = 0; i < strs.Count; i++)
إذا (string.Equals(value, strs[i], StringComparison.CurrentCultureIgnoreCase))
عودة صحيحة؛
عودة كاذبة.
}
دعونا نلقي نظرة على نتائج التنفيذ:
استخدم ImplementByStringEquals
1117 مللي ثانية
جي سي 0:0
جي سي 1:0
جي سي 2:0
النتيجة صحيحة
بالمقارنة، فإن استخدام ToUpper يكون بطيئًا مرتين ويحتوي على الكثير من الكائنات المهملة من الجيل 0. ويمكن لمن يدعون مقايضة المكان بالزمن أن يتفكروا في هذا، فماذا حصلوا مقابل المكان؟ الوقت السلبي؟
استخدام فئة القاموس
بالاستمرار في سيناريو السلسلة، قد يفكر بعض الأشخاص في استخدام جداول التجزئة والهياكل المماثلة الأخرى للتسريع. نعم، هذه فكرة جيدة، لكن جداول التجزئة ليست دائمًا الحل الأفضل. لماذا لا تصدق ذلك؟ لنقم باختبار:
المنطق المنطقي الثابت الخاص ImplementByHashSet(قائمة<string> strs، قيمة السلسلة)
{
HashSet<string> set = new HashSet<string>(strs, StringComparer.CurrentCultureIgnoreCase);
إرجاع set.Contains(value);
}
ألق نظرة على نتائج التنفيذ:
استخدم ImplementByHashSet
5114 مللي ثانية
جي سي 0:38
تك 1: 38
تك 2: 38
النتيجة صحيحة
والمثير للدهشة أن السرعة أبطأ بأكثر من ضعف سرعة استخدام ToUpper، كما يتم جمع القمامة من الجيل الثاني 38 مرة (عند تنفيذ مجموعة القمامة من الجيل الثاني، سيتم فرض جمع القمامة من الجيل الأول والجيل الصفري).
ومع ذلك، فإن فكرة استخدام جدول التجزئة أو ما شابه لتسريع العملية هي فكرة صحيحة جدًا، ولكن الفرضية هي أن جدول التجزئة نفسه يمكن تخزينه مؤقتًا، على سبيل المثال:
Func<string, bool> ImplementByHashSet2(List<string> strs) ثابت خاص
{
HashSet<string> set = new HashSet<string>(strs, StringComparer.CurrentCultureIgnoreCase);
مجموعة العودة. تحتوي على؛
}
ثم قم بتعديل الطريقة الرئيسية إلى:
Console.WriteLine("استخدام ImplementByHashSet2");
النتيجة = قياس الأداء (الأداء =>
{
var f = ImplementByHashSet2(strs);
منطقية ret = خطأ؛
لـ (int i = 0; i < 1000; i++)
{
ret = f(s);
}
عودة متقاعد؛
}, "yZh", 1);
Console.WriteLine("النتيجة هي" + result.ToString());
Console.ReadLine();
دعونا نلقي نظرة على النتائج:
استخدم ImplementByHashSet2
6 مللي ثانية
جي سي 0:0
جي سي 1:0
جي سي 2:0
النتيجة صحيحة
لقد زاد الأداء بشكل كبير.
أكثر
ما الذي يبطئ c#/.net؟ بكل بساطة: إنشاء كائنات غير ضرورية، ومزامنة غير ضرورية، وأساليب غير فعالة لتنفيذ الحلقة (مثل الانعكاس الذي تم انتقاده بواسطة Firelong، لكن MS لا يسمح لك باستخدام الاستدعاء في الحلقة)، واستخدام هياكل البيانات والخوارزميات غير الفعالة (انظر في الأداء المذهل لبنية مماثلة لجدول التجزئة في حالة التخزين المؤقت، وسوف تعرف الفرق)
يساعد الحد الأدنى لـ c#/.net في جذب المزيد من المبرمجين إلى c#/.net إلى حد ما، ولكنه يقلل أيضًا من مستوى التعليمات البرمجية لبرنامج c#/.net بأكمله كثيرًا، وهو أمر مثير للإعجاب حقًا.
وأخيرًا، لا تنس أن أداء النظام لا يتم تحديده من خلال الجزء الأفضل أداءً في النظام، ولكن من خلال الجزء الأسوأ أداءً في النظام. مزود بذاكرة 16 جيجا، وقرص صلب 100 طن، بالإضافة إلى بطاقة رسومات من الدرجة الأولى، ولكن بدون وحدة المعالجة المركزية 386، فإن أداء هذا الكمبيوتر هو أداء 386. وبالمثل، بغض النظر عن مدى جودة C#/.net، إذا كانت مهارة المبرمج ضعيفة، فمن الطبيعي أن يكون أداء البرنامج المكتوب ضعيفًا.