ข้อมูลโค้ดใดจะให้ประสิทธิภาพดีกว่ากัน ส่วนโค้ดด้านล่างเขียนด้วย C #
1.
for(int counter=0; counter<list.Count; counter++)
{
list[counter].DoSomething();
}
2.
foreach(MyType current in list)
{
current.DoSomething();
}
ข้อมูลโค้ดใดจะให้ประสิทธิภาพดีกว่ากัน ส่วนโค้ดด้านล่างเขียนด้วย C #
1.
for(int counter=0; counter<list.Count; counter++)
{
list[counter].DoSomething();
}
2.
foreach(MyType current in list)
{
current.DoSomething();
}
list
จริงๆไม่ได้มีสมาชิกแทนcount
Count
คำตอบ:
ส่วนหนึ่งขึ้นอยู่กับประเภทที่แน่นอนของlist
. นอกจากนี้ยังขึ้นอยู่กับ CLR ที่คุณใช้
ไม่ว่าจะมีความสำคัญในทางใดก็ตามจะขึ้นอยู่กับว่าคุณกำลังทำงานจริงหรือไม่ ในเกือบทุกกรณีความแตกต่างของประสิทธิภาพจะไม่สำคัญ แต่ความแตกต่างของความสามารถในการอ่านจะสนับสนุนการforeach
วนซ้ำ
ฉันใช้ LINQ เป็นการส่วนตัวเพื่อหลีกเลี่ยง "if" ด้วย:
foreach (var item in list.Where(condition))
{
}
แก้ไข: สำหรับพวกคุณที่อ้างว่าการทำซ้ำList<T>
กับa foreach
จะสร้างรหัสเดียวกับfor
ลูปนี่คือหลักฐานว่ามันไม่:
static void IterateOverList(List<object> list)
{
foreach (object o in list)
{
Console.WriteLine(o);
}
}
ผลิต IL ของ:
.method private hidebysig static void IterateOverList(class [mscorlib]System.Collections.Generic.List`1<object> list) cil managed
{
// Code size 49 (0x31)
.maxstack 1
.locals init (object V_0,
valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<object> V_1)
IL_0000: ldarg.0
IL_0001: callvirt instance valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<!0> class [mscorlib]System.Collections.Generic.List`1<object>::GetEnumerator()
IL_0006: stloc.1
.try
{
IL_0007: br.s IL_0017
IL_0009: ldloca.s V_1
IL_000b: call instance !0 valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<object>::get_Current()
IL_0010: stloc.0
IL_0011: ldloc.0
IL_0012: call void [mscorlib]System.Console::WriteLine(object)
IL_0017: ldloca.s V_1
IL_0019: call instance bool valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<object>::MoveNext()
IL_001e: brtrue.s IL_0009
IL_0020: leave.s IL_0030
} // end .try
finally
{
IL_0022: ldloca.s V_1
IL_0024: constrained. valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<object>
IL_002a: callvirt instance void [mscorlib]System.IDisposable::Dispose()
IL_002f: endfinally
} // end handler
IL_0030: ret
} // end of method Test::IterateOverList
ถือว่าคอมไพเลอร์อาร์เรย์ที่แตกต่างกันแปลงforeach
ห่วงพื้นให้เป็นfor
ห่วง List<T>
แต่ไม่ได้ นี่คือรหัสเทียบเท่าสำหรับอาร์เรย์:
static void IterateOverArray(object[] array)
{
foreach (object o in array)
{
Console.WriteLine(o);
}
}
// Compiles into...
.method private hidebysig static void IterateOverArray(object[] 'array') cil managed
{
// Code size 27 (0x1b)
.maxstack 2
.locals init (object V_0,
object[] V_1,
int32 V_2)
IL_0000: ldarg.0
IL_0001: stloc.1
IL_0002: ldc.i4.0
IL_0003: stloc.2
IL_0004: br.s IL_0014
IL_0006: ldloc.1
IL_0007: ldloc.2
IL_0008: ldelem.ref
IL_0009: stloc.0
IL_000a: ldloc.0
IL_000b: call void [mscorlib]System.Console::WriteLine(object)
IL_0010: ldloc.2
IL_0011: ldc.i4.1
IL_0012: add
IL_0013: stloc.2
IL_0014: ldloc.2
IL_0015: ldloc.1
IL_0016: ldlen
IL_0017: conv.i4
IL_0018: blt.s IL_0006
IL_001a: ret
} // end of method Test::IterateOverArray
ที่น่าสนใจฉันไม่พบเอกสารนี้ในข้อมูลจำเพาะ C # 3 ทุกที่ ...
List<T>
.
foreach
เหนืออาร์เรย์นั้นเทียบเท่ากับfor
อย่างไรก็ตาม ควรเขียนโค้ดเพื่อให้อ่านง่ายก่อนจากนั้นจึงปรับให้เหมาะสมเพียงเล็กน้อยเมื่อคุณมีหลักฐานว่าให้ประโยชน์ด้านประสิทธิภาพที่วัดได้
for
วงได้รับเรียบเรียงรหัสประมาณเทียบเท่ากับนี้:
int tempCount = 0;
while (tempCount < list.Count)
{
if (list[tempCount].value == value)
{
// Do something
}
tempCount++;
}
โดยที่เป็นforeach
ลูปถูกคอมไพล์เป็นโค้ดโดยประมาณเทียบเท่าสิ่งนี้:
using (IEnumerator<T> e = list.GetEnumerator())
{
while (e.MoveNext())
{
T o = (MyClass)e.Current;
if (row.value == value)
{
// Do something
}
}
}
ดังที่คุณเห็นทั้งหมดจะขึ้นอยู่กับวิธีการใช้งานตัวแจงนับเทียบกับวิธีการใช้งานตัวทำดัชนีรายการ ตามปกติแล้วตัวแจงนับสำหรับประเภทตามอาร์เรย์จะเขียนดังนี้:
private static IEnumerable<T> MyEnum(List<T> list)
{
for (int i = 0; i < list.Count; i++)
{
yield return list[i];
}
}
ดังที่คุณเห็นในกรณีนี้จะไม่สร้างความแตกต่างมากนัก แต่ตัวแจงนับสำหรับรายการที่เชื่อมโยงอาจมีลักษณะดังนี้:
private static IEnumerable<T> MyEnum(LinkedList<T> list)
{
LinkedListNode<T> current = list.First;
do
{
yield return current.Value;
current = current.Next;
}
while (current != null);
}
ใน. NETคุณจะพบว่าคลาส LinkedList <T> ไม่มีแม้แต่ตัวสร้างดัชนีดังนั้นคุณจะไม่สามารถทำ for loop ในรายการที่เชื่อมโยงได้ แต่ถ้าทำได้ตัวสร้างดัชนีจะต้องเขียนดังนี้:
public T this[int index]
{
LinkedListNode<T> current = this.First;
for (int i = 1; i <= index; i++)
{
current = current.Next;
}
return current.value;
}
อย่างที่คุณเห็นการเรียกสิ่งนี้หลาย ๆ ครั้งในลูปนั้นจะช้ากว่าการใช้ตัวแจงนับที่จำได้ว่าอยู่ที่ไหนในรายการ
การทดสอบที่ง่ายในการตรวจสอบกึ่ง ฉันได้ทำการทดสอบเล็ก ๆ เพื่อดู นี่คือรหัส:
static void Main(string[] args)
{
List<int> intList = new List<int>();
for (int i = 0; i < 10000000; i++)
{
intList.Add(i);
}
DateTime timeStarted = DateTime.Now;
for (int i = 0; i < intList.Count; i++)
{
int foo = intList[i] * 2;
if (foo % 2 == 0)
{
}
}
TimeSpan finished = DateTime.Now - timeStarted;
Console.WriteLine(finished.TotalMilliseconds.ToString());
Console.Read();
}
และนี่คือส่วน foreach:
foreach (int i in intList)
{
int foo = i * 2;
if (foo % 2 == 0)
{
}
}
เมื่อฉันแทนที่ for ด้วย foreach - foreach เร็วขึ้น 20 มิลลิวินาที - สม่ำเสมออย่างต่อเนื่องสำหรับคือ 135-139ms ในขณะที่ foreach คือ 113-119ms ฉันสลับไปมาหลายครั้งเพื่อให้แน่ใจว่ามันไม่ใช่กระบวนการที่เพิ่งเริ่มต้น
อย่างไรก็ตามเมื่อฉันลบคำสั่ง foo และ if ออกไปค่า for นั้นเร็วขึ้น 30 ms (foreach คือ 88ms และเป็น 59ms) พวกเขาทั้งสองเป็นเปลือกหอยที่ว่างเปล่า ฉันสมมติว่า foreach ส่งผ่านตัวแปรโดยที่ for เป็นเพียงการเพิ่มตัวแปร ถ้าฉันเพิ่ม
int foo = intList[i];
จากนั้นให้ช้าลงประมาณ 30ms ฉันสมมติว่าสิ่งนี้เกี่ยวข้องกับการสร้าง foo และคว้าตัวแปรในอาร์เรย์และกำหนดให้กับ foo หากคุณเพิ่งเข้าถึง intList [i] คุณก็จะไม่มีบทลงโทษนั้น
ด้วยความสัตย์จริง .. ฉันคาดว่า foreach จะช้าลงเล็กน้อยในทุกสถานการณ์ แต่ไม่เพียงพอสำหรับการใช้งานส่วนใหญ่
แก้ไข: นี่คือรหัสใหม่โดยใช้คำแนะนำ Jons (134217728 เป็น int ที่ใหญ่ที่สุดที่คุณสามารถมีได้ก่อนที่ข้อยกเว้น System.OutOfMemory จะถูกโยนทิ้ง):
static void Main(string[] args)
{
List<int> intList = new List<int>();
Console.WriteLine("Generating data.");
for (int i = 0; i < 134217728 ; i++)
{
intList.Add(i);
}
Console.Write("Calculating for loop:\t\t");
Stopwatch time = new Stopwatch();
time.Start();
for (int i = 0; i < intList.Count; i++)
{
int foo = intList[i] * 2;
if (foo % 2 == 0)
{
}
}
time.Stop();
Console.WriteLine(time.ElapsedMilliseconds.ToString() + "ms");
Console.Write("Calculating foreach loop:\t");
time.Reset();
time.Start();
foreach (int i in intList)
{
int foo = i * 2;
if (foo % 2 == 0)
{
}
}
time.Stop();
Console.WriteLine(time.ElapsedMilliseconds.ToString() + "ms");
Console.Read();
}
และนี่คือผลลัพธ์:
กำลังสร้างข้อมูล การคำนวณสำหรับลูป: 2458ms การคำนวณ foreach loop: 2005ms
การสลับไปมาเพื่อดูว่ามันเกี่ยวข้องกับลำดับของสิ่งต่างๆหรือไม่ให้ผลลัพธ์เหมือนกัน (เกือบ)
หมายเหตุ: คำตอบนี้ใช้กับ Java ได้มากกว่า C # เนื่องจาก C # ไม่มีตัวทำดัชนีLinkedLists
แต่ฉันคิดว่าประเด็นทั่วไปยังคงมีอยู่
ถ้า list
คุณกำลังทำงานอยู่เกิดขึ้นเป็น a LinkedList
ประสิทธิภาพของรหัสดัชนี ( การเข้าถึงแบบอาร์เรย์ ) จะแย่กว่าการใช้IEnumerator
จากforeach
รายการขนาดใหญ่มาก
เมื่อคุณเข้าถึงองค์ประกอบ 10.000 LinkedList
โดยใช้ไวยากรณ์ตัวทำดัชนี: list[10000]
รายการที่เชื่อมโยงจะเริ่มต้นที่โหนดส่วนหัวและข้ามไปที่Next
-pointer หนึ่งหมื่นครั้งจนกว่าจะถึงวัตถุที่ถูกต้อง เห็นได้ชัดว่าหากคุณทำสิ่งนี้แบบวนซ้ำคุณจะได้รับ:
list[0]; // head
list[1]; // head.Next
list[2]; // head.Next.Next
// etc.
เมื่อคุณโทร GetEnumerator
(โดยปริยายโดยใช้forach
-syntax) คุณจะได้รับIEnumerator
วัตถุที่มีตัวชี้ไปที่โหนดหัว ทุกครั้งที่คุณโทรMoveNext
ตัวชี้นั้นจะถูกย้ายไปยังโหนดถัดไปดังนี้:
IEnumerator em = list.GetEnumerator(); // Current points at head
em.MoveNext(); // Update Current to .Next
em.MoveNext(); // Update Current to .Next
em.MoveNext(); // Update Current to .Next
// etc.
อย่างที่คุณเห็นในกรณีของLinkedList
s เมธอดตัวทำดัชนีอาร์เรย์จะช้าลงและช้าลงยิ่งคุณวนซ้ำนานขึ้น (ต้องผ่านตัวชี้หัวเดียวกันซ้ำแล้วซ้ำเล่า) ในขณะที่IEnumerable
ทำงานในเวลาคงที่
แน่นอนอย่างที่จอนพูดสิ่งนี้ขึ้นอยู่กับประเภทของlist
ถ้าlist
ไม่ใช่ a LinkedList
แต่เป็นอาร์เรย์พฤติกรรมจะแตกต่างกันอย่างสิ้นเชิง
LinkedList<T>
เอกสารบน MSDN และมี API ที่ดีทีเดียว ที่สำคัญที่สุดคือไม่มีget(int index)
วิธีการเช่นเดียวกับ Java แต่ถึงกระนั้นผมคิดว่าจุดยังคงถือสำหรับรายชื่อเหมือนโครงสร้างข้อมูลอื่น ๆ ที่ exposes IEnumerator
ดัชนีที่ช้ากว่าที่เฉพาะเจาะจงหนึ่ง
เช่นเดียวกับที่คนอื่น ๆ พูดถึงแม้ว่าประสิทธิภาพจะไม่สำคัญมากนัก foreach มักจะช้าลงเล็กน้อยเนื่องจากIEnumerable
/ IEnumerator
การใช้งานในลูป คอมไพลเลอร์จะแปลโครงสร้างเป็นการเรียกบนอินเทอร์เฟซนั้นและสำหรับทุกขั้นตอนจะมีการเรียกใช้ฟังก์ชัน + คุณสมบัติในโครงสร้าง foreach
IEnumerator iterator = ((IEnumerable)list).GetEnumerator();
while (iterator.MoveNext()) {
var item = iterator.Current;
// do stuff
}
นี่คือการขยายตัวที่เทียบเท่ากันของโครงสร้างใน C # คุณสามารถจินตนาการได้ว่าผลกระทบด้านประสิทธิภาพจะแตกต่างกันไปอย่างไรโดยขึ้นอยู่กับการใช้งาน MoveNext และ Current ในขณะที่ในการเข้าถึงอาร์เรย์คุณไม่มีการอ้างอิงนั้น
List<T>
ที่นี่แสดงว่ายังคงมีการเรียกใช้ตัวสร้างดัชนี มันไม่เหมือนกับการเข้าถึงอาร์เรย์โลหะเปล่า
หลังจากอ่านข้อโต้แย้งมากพอแล้วว่า "foreach loop ควรเป็นที่ต้องการเพื่อให้อ่านง่าย" ฉันสามารถพูดได้ว่าปฏิกิริยาแรกของฉันคือ "อะไร"? โดยทั่วไปแล้วความสามารถในการอ่านจะเป็นแบบอัตนัยและโดยเฉพาะอย่างยิ่งเช่นนี้ สำหรับคนที่มีพื้นฐานในการเขียนโปรแกรม (จริงๆแล้วทุกภาษาก่อน Java) สำหรับลูปนั้นอ่านง่ายกว่า foreach ลูปมาก นอกจากนี้คนกลุ่มเดียวกันที่อ้างว่า foreach loops อ่านได้ง่ายกว่ายังสนับสนุน linq และ "คุณสมบัติ" อื่น ๆ ที่ทำให้โค้ดอ่านและบำรุงรักษายากซึ่งเป็นสิ่งที่พิสูจน์ประเด็นข้างต้น
เกี่ยวกับผลกระทบต่อประสิทธิภาพการทำงานให้ดูคำตอบนี้คำถาม
แก้ไข: มีคอลเล็กชันใน C # (เช่น HashSet) ที่ไม่มีตัวสร้างดัชนี ในคอลเลกชันเหล่านี้foreachเป็นวิธีเดียวที่จะย้ำและมันก็เป็นกรณีเดียวที่ฉันคิดว่ามันควรจะใช้มากกว่าสำหรับ
มีข้อเท็จจริงที่น่าสนใจเพิ่มเติมซึ่งอาจพลาดได้ง่ายเมื่อทดสอบความเร็วของลูปทั้งสอง: การใช้โหมดดีบักไม่อนุญาตให้คอมไพเลอร์ปรับแต่งโค้ดโดยใช้การตั้งค่าเริ่มต้น
สิ่งนี้ทำให้ฉันได้ผลลัพธ์ที่น่าสนใจที่ foreach เร็วกว่าในโหมดดีบัก ในขณะที่ for ist เร็วกว่า foreach ในโหมด release เห็นได้ชัดว่าคอมไพเลอร์มีวิธีที่ดีกว่าในการเพิ่มประสิทธิภาพสำหรับลูปมากกว่า foreach loop ซึ่งทำให้การเรียกใช้เมธอดหลาย ๆ สำหรับลูปเป็นพื้นฐานที่เป็นไปได้ว่าซีพียูได้รับการปรับให้เหมาะสมด้วยซ้ำ
ในตัวอย่างที่คุณให้เป็นที่แน่นอนดีกว่าที่จะใช้foreach
ห่วงแทนfor
ห่วง
โครงสร้างมาตรฐานforeach
สามารถทำได้เร็วกว่า (1,5 รอบต่อขั้นตอน) มากกว่าแบบธรรมดาfor-loop
(2 รอบต่อขั้นตอน) เว้นแต่จะคลายการวนซ้ำแล้ว (1.0 รอบต่อขั้นตอน)
ดังนั้นสำหรับรหัสประจำวันประสิทธิภาพการทำงานไม่ได้เป็นเหตุผลที่จะใช้ที่ซับซ้อนมากขึ้นfor
, while
หรือdo-while
โครงสร้าง
ตรวจสอบลิงค์นี้: http://www.codeproject.com/Articles/146797/Fast-and-Less-Fast-Loops-in-C
╔══════════════════════╦═══════════╦═══════╦════════════════════════╦═════════════════════╗
║ Method ║ List<int> ║ int[] ║ Ilist<int> onList<Int> ║ Ilist<int> on int[] ║
╠══════════════════════╬═══════════╬═══════╬════════════════════════╬═════════════════════╣
║ Time (ms) ║ 23,80 ║ 17,56 ║ 92,33 ║ 86,90 ║
║ Transfer rate (GB/s) ║ 2,82 ║ 3,82 ║ 0,73 ║ 0,77 ║
║ % Max ║ 25,2% ║ 34,1% ║ 6,5% ║ 6,9% ║
║ Cycles / read ║ 3,97 ║ 2,93 ║ 15,41 ║ 14,50 ║
║ Reads / iteration ║ 16 ║ 16 ║ 16 ║ 16 ║
║ Cycles / iteration ║ 63,5 ║ 46,9 ║ 246,5 ║ 232,0 ║
╚══════════════════════╩═══════════╩═══════╩════════════════════════╩═════════════════════╝
คุณสามารถอ่านเกี่ยวกับเรื่องนี้ได้ใน Deep .NET - ตอนที่ 1 การวนซ้ำ
มันครอบคลุมผลลัพธ์ (โดยไม่ต้องเริ่มต้นครั้งแรก) จากซอร์สโค้ด. NET ไปจนถึงการถอดชิ้นส่วน
ตัวอย่างเช่น - การวนซ้ำอาร์เรย์ด้วย foreach loop: