เมื่อไม่กี่วันที่ผ่านมา มีการเผยแพร่บทความ 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 วัดประสิทธิภาพส่วนตัวคงที่ <TArg, TResult> (Func <TArg, TResult> func, TArg arg, int loop)
-
GC.รวบรวม();
int gc0 = GC.CollectionCount(0);
int gc1 = GC.CollectionCount (1);
int gc2 = GC.CollectionCount (2);
ผลลัพธ์ TResult = ค่าเริ่มต้น (TResult);
นาฬิกาจับเวลา sw = Stopwatch.StartNew();
สำหรับ (int i = 0; i < วนซ้ำ; i++)
-
ผลลัพธ์ = func(หาเรื่อง);
-
Console.WriteLine(sw.ElapsedMilliseconds.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()
-
รายการ<string> strs = รายการใหม่<string>(10,000);
ถ่าน [] chs = ถ่านใหม่ [3];
สำหรับ (int i = 0; i < 10,000; i++)
-
อินท์เจ = ฉัน;
สำหรับ (int k = 0; k < chs.Length; k++)
-
chs[k] = (ถ่าน)('a' + j % 26);
เจ = เจ / 26;
-
strs.Add(สตริงใหม่(chs));
-
กลับ STRS;
-
มาดูการใช้งาน ToUpper กัน:
บูลคงที่ส่วนตัว ImplementByToUpper (รายการ <string> strs, ค่าสตริง)
-
สำหรับ (int i = 0; i < strs.Count; i++)
ถ้า (value.ToUpper() == strs[i].ToUpper())
กลับเป็นจริง;
กลับเท็จ;
-
ในที่สุดก็เตรียมวิธีการหลัก:
รายการ<string> strs = CreateStrings();
ผลลัพธ์บูล;
Console.WriteLine("ใช้ ImplementByToUpper");
ผล = MeasurePerformance(s => ImplementByToUpper(strs, s), "yZh", 1,000);
Console.WriteLine("ผลลัพธ์คือ" + result.ToString());
Console.ReadLine();
มาดูผลการดำเนินการกัน:
ใช้ ImplementByToUpper
2192ms
กซ 0:247
กซ 1:0
กซ 2:0
ผลลัพธ์คือ True
มาทำการทดสอบเปรียบเทียบและใช้สตริง เท่ากับการทดสอบ:
บูลคงที่ส่วนตัว ImplementByStringEquals (List <string> strs, ค่าสตริง)
-
สำหรับ (int i = 0; i < strs.Count; i++)
ถ้า (string.Equals(value, strs[i], StringComparison.CurrentCultureIgnoreCase))
กลับเป็นจริง;
กลับเท็จ;
-
มาดูผลการดำเนินการกัน:
ใช้ ImplementByStringEquals
1117มิลลิวินาที
GC 0:0
กซ 1:0
กซ 2:0
ผลลัพธ์คือ True
เมื่อเปรียบเทียบกันแล้ว การใช้ ToUpper นั้นช้ากว่าสองเท่าและมีออบเจ็กต์ขยะรุ่นที่ 0 จำนวนมาก ผู้ที่อ้างว่าแลกพื้นที่เพื่อเวลาสามารถไตร่ตรองสิ่งนี้ได้ว่าพวกเขาได้อะไรมาแลกกับพื้นที่? เวลาติดลบ?
การใช้คลาสพจนานุกรม
ดำเนินการต่อด้วยสถานการณ์สตริง บางคนอาจนึกถึงการใช้ตาราง Hash และโครงสร้างอื่นที่คล้ายคลึงกันเพื่อเร่งความเร็ว ใช่ นี่เป็นความคิดที่ดี แต่ตาราง Hash ไม่ใช่ทางออกที่ดีที่สุดเสมอไป มาทำการทดสอบกัน:
บูลคงที่ส่วนตัว ImplementByHashSet (รายการ <string> strs, ค่าสตริง)
-
HashSet<string> set = ใหม่ HashSet<string>(strs, StringComparer.CurrentCultureIgnoreCase);
กลับ set.Contains (ค่า);
-
ดูผลการดำเนินการ:
ใช้ ImplementByHashSet
5114มิลลิวินาที
กจ 0:38
กซ 1:38
กซ 2:38
ผลลัพธ์คือ True
น่าแปลกที่ความเร็วนั้นช้ากว่าการใช้ ToUpper มากกว่าสองเท่า และขยะรุ่นที่สองก็ถูกรวบรวม 38 ครั้งด้วย (เมื่อดำเนินการรวบรวมขยะรุ่นที่สอง ระบบจะบังคับการรวบรวมขยะรุ่นแรกและศูนย์รุ่น)
อย่างไรก็ตาม แนวคิดในการใช้ตาราง Hash หรือสิ่งอื่นที่คล้ายกันเพื่อเร่งกระบวนการนั้นเป็นแนวคิดที่ถูกต้องมาก แต่สมมติฐานก็คือสามารถแคชตาราง Hash ได้เอง เช่น:
ส่วนตัว Func <string, bool> ImplementByHashSet2 (รายการ <string> strs)
-
HashSet<string> set = ใหม่ HashSet<string>(strs, StringComparer.CurrentCultureIgnoreCase);
ชุดขากลับประกอบด้วย;
-
จากนั้นแก้ไขวิธีการหลักเป็น:
Console.WriteLine("ใช้ ImplementByHashSet2");
ผล = วัดประสิทธิภาพ(s =>
-
var f = ImplementByHashSet2(strs);
บูล ret = เท็จ;
สำหรับ (int i = 0; i < 1,000; i++)
-
ret = f(s);
-
กลับ;
}, "yZh", 1);
Console.WriteLine("ผลลัพธ์คือ" + result.ToString());
Console.ReadLine();
ลองมาดูผลลัพธ์:
ใช้ ImplementByHashSet2
6ms
GC 0:0
กซ 1:0
กซ 2:0
ผลลัพธ์คือ True
ประสิทธิภาพเพิ่มขึ้นอย่างมาก
มากกว่า
อะไรทำให้ c#/.net ช้าลง พูดง่ายๆ ก็คือ การสร้างวัตถุโดยไม่จำเป็น การซิงโครไนซ์ที่ไม่จำเป็น วิธีการดำเนินการวนซ้ำที่ไม่มีประสิทธิภาพ (เช่น การสะท้อนกลับที่ถูกวิพากษ์วิจารณ์จาก firelong แต่ ms ไม่อนุญาตให้คุณใช้ Invoid ในลูป) การใช้โครงสร้างข้อมูลและอัลกอริทึมที่ไม่มีประสิทธิภาพ (ดู ด้วยประสิทธิภาพอันน่าทึ่งของโครงสร้างที่คล้ายกันของตาราง Hash ในกรณีของแคช แล้วคุณจะรู้ถึงความแตกต่าง)
เกณฑ์ขั้นต่ำของ c#/.net ช่วยดึงดูดโปรแกรมเมอร์เข้าสู่ c#/.net มากขึ้นในระดับหนึ่ง แต่ยังช่วยลดระดับโค้ดของโปรแกรม c#/.net ทั้งหมดได้มาก ซึ่งน่าประทับใจมาก
สุดท้ายนี้ อย่าลืมว่าประสิทธิภาพของระบบไม่ได้ถูกกำหนดโดยส่วนที่มีประสิทธิภาพดีที่สุดของระบบ แต่ถูกกำหนดโดยส่วนที่มีประสิทธิภาพแย่ที่สุดของระบบ มาพร้อมกับหน่วยความจำ 16g, ฮาร์ดไดรฟ์ 100t พร้อมด้วยกราฟิกการ์ดชั้นยอด แต่ไม่มี CPU 386 ประสิทธิภาพของคอมพิวเตอร์เครื่องนี้ก็เท่ากับประสิทธิภาพของ 386 ในทำนองเดียวกัน ไม่ว่า C#/.net จะดีแค่ไหน ถ้าทักษะของโปรแกรมเมอร์ไม่ดี ประสิทธิภาพของโปรแกรมที่เขียนก็จะแย่ตามธรรมชาติ