ทำไมอยู่ที่ไหนและเลือกดีกว่าเพียงแค่เลือก?


145

ฉันมีชั้นเรียนเช่นนี้

public class MyClass
{
    public int Value { get; set; }
    public bool IsValid { get; set; }
}

อันที่จริงแล้วมันมีขนาดใหญ่กว่า แต่สิ่งนี้ทำให้เกิดปัญหาขึ้นอีกครั้ง

ฉันต้องการได้ผลรวมของValueซึ่งเป็นกรณีที่ถูกต้อง จนถึงตอนนี้ฉันได้พบสองวิธีนี้

คนแรกคือ:

int result = myCollection.Where(mc => mc.IsValid).Select(mc => mc.Value).Sum();

อย่างไรก็ตามอันที่สองคือ:

int result = myCollection.Select(mc => mc.IsValid ? mc.Value : 0).Sum();

ฉันต้องการวิธีที่มีประสิทธิภาพที่สุด ตอนแรกฉันคิดว่าอันที่สองจะมีประสิทธิภาพมากกว่า จากนั้นส่วนทางทฤษฎีของฉันเริ่มไป "อืม, อันหนึ่งคือ O (n + m + m), อีกอันหนึ่งคือ O (n + n). อันแรกควรทำงานได้ดีกว่าโดยมี invalids มากกว่า, ในขณะที่อันที่สองควรทำงานได้ดีขึ้น มีน้อย " ฉันคิดว่าพวกเขาจะแสดงอย่างเท่าเทียมกัน แก้ไข: และจากนั้น @Martin ชี้ให้เห็นว่า Where และ the Select รวมกันดังนั้นจึงควรเป็น O (m + n) อย่างไรก็ตามหากคุณดูด้านล่างดูเหมือนว่าสิ่งนี้จะไม่เกี่ยวข้องกัน


ดังนั้นฉันจึงนำไปทดสอบ

(มันมีมากกว่า 100 บรรทัดดังนั้นฉันคิดว่ามันจะดีกว่าถ้าจะโพสต์เป็นส่วนสำคัญ)
ผลลัพธ์เป็น ... น่าสนใจ

ด้วยความอดทนเสมอ 0%:

ตาชั่งอยู่ในความโปรดปรานของSelectและWhereประมาณ ~ 30 คะแนน

How much do you want to be the disambiguation percentage?
0
Starting benchmarking.
Ties: 0
Where + Select: 65
Select: 36

ด้วยความอดทนผูก 2%:

มันเหมือนกันยกเว้นบางอย่างที่พวกเขาอยู่ภายใน 2% ฉันจะบอกว่านั่นเป็นข้อผิดพลาดขั้นต่ำ SelectและWhereตอนนี้มีเพียงตะกั่ว ~ 20 แต้ม

How much do you want to be the disambiguation percentage?
2
Starting benchmarking.
Ties: 6
Where + Select: 58
Select: 37

ด้วยความอดทน 5% ผูก:

นี่คือสิ่งที่ฉันจะบอกว่าเป็นข้อผิดพลาดสูงสุดของฉัน มันทำให้ดีขึ้นเล็กน้อยสำหรับSelectแต่ไม่มาก

How much do you want to be the disambiguation percentage?
5
Starting benchmarking.
Ties: 17
Where + Select: 53
Select: 31

ด้วยความอดทนผูก 10%:

นี่เป็นข้อผิดพลาดเล็กน้อย แต่ฉันยังคงสนใจในผลลัพธ์ เพราะมันให้SelectและWhereตะกั่วยี่สิบจุดมันมีอยู่พักหนึ่งแล้ว

How much do you want to be the disambiguation percentage?
10
Starting benchmarking.
Ties: 36
Where + Select: 44
Select: 21

ด้วยความอดทน 25% ผูก:

นี่คือวิธีวิธีออกจากอัตรากำไรขั้นต้นของฉันของข้อผิดพลาด แต่ฉันยังคงอยู่ในความสนใจผลที่เพราะSelectและWhere ยังคง (เกือบ) เพื่อให้นำ 20 จุดของพวกเขา ดูเหมือนว่ามันจะดีกว่าในบางส่วนและนั่นคือสิ่งที่ทำให้มันเป็นผู้นำ

How much do you want to be the disambiguation percentage?
25
Starting benchmarking.
Ties: 85
Where + Select: 16
Select: 0


ตอนนี้ผมคาดเดาว่านำ 20 จุดมาจากตรงกลางที่พวกเขาทั้งสองที่ถูกผูกไว้ที่จะได้รับรอบผลการดำเนินงานเดียวกัน ฉันสามารถลองและบันทึกมันได้ แต่มันจะเป็นการโหลดข้อมูลทั้งหมดที่จะเข้ามากราฟจะดีกว่าฉันเดา

นั่นคือสิ่งที่ฉันทำ

เลือก vs Select และตำแหน่ง

มันแสดงให้เห็นว่าSelectเส้นคงที่ (คาดว่า) และSelect + Whereเส้นปีนขึ้น (คาดว่า) แต่สิ่งที่ฉันปริศนาคือเหตุผลที่มันไม่ตรงกับSelectที่ 50 หรือก่อนหน้า: ในความเป็นจริงผมคาดหวังว่าก่อนหน้านี้กว่า 50, แจงนับเป็นพิเศษจะต้องมีการสร้างขึ้นสำหรับและSelect Whereฉันหมายความว่าสิ่งนี้แสดงให้เห็นถึงจุดนำ 20 จุด แต่มันไม่ได้อธิบายว่าทำไม นี่ฉันเดาว่าเป็นประเด็นหลักของคำถามของฉัน

ทำไมมันมีพฤติกรรมเช่นนี้? ฉันควรไว้ใจหรือไม่ ถ้าไม่ฉันควรใช้อันอื่นหรืออันนี้


ตามที่ @KongKong พูดถึงในความคิดเห็นคุณสามารถใช้Sumเกินของแลมบ์ดา ดังนั้นสองทางเลือกของฉันตอนนี้เปลี่ยนเป็น:

ครั้งแรก:

int result = myCollection.Where(mc => mc.IsValid).Sum(mc => mc.Value);

ประการที่สอง:

int result = myCollection.Sum(mc => mc.IsValid ? mc.Value : 0);

ฉันจะทำให้มันสั้นลงเล็กน้อย แต่:

How much do you want to be the disambiguation percentage?
0
Starting benchmarking.
Ties: 0
Where: 60
Sum: 41
How much do you want to be the disambiguation percentage?
2
Starting benchmarking.
Ties: 8
Where: 55
Sum: 38
How much do you want to be the disambiguation percentage?
5
Starting benchmarking.
Ties: 21
Where: 49
Sum: 31
How much do you want to be the disambiguation percentage?
10
Starting benchmarking.
Ties: 39
Where: 41
Sum: 21
How much do you want to be the disambiguation percentage?
25
Starting benchmarking.
Ties: 85
Where: 16
Sum: 0

จุดนำยี่สิบจุดยังคงอยู่ที่นั่นซึ่งหมายความว่าไม่จำเป็นต้องเกี่ยวข้องกับWhereและการSelectรวมกันที่ @Marcin ชี้ให้เห็นในความคิดเห็น

ขอบคุณที่อ่านข้อความของฉัน! นอกจากนี้หากคุณสนใจนี่คือรุ่นที่ปรับเปลี่ยนแล้วซึ่งจะบันทึก CSV ที่ Excel ใช้


1
ผมว่ามันขึ้นอยู่กับวิธีที่มีราคาแพงรวมและการเข้าถึงการmc.Valueมี
Medinoc

14
@ It'sNotALie Where+ Selectไม่ทำให้เกิดการวนซ้ำสองครั้งที่แยกกันระหว่างการรวบรวมอินพุต LINQ to Objects ปรับมันให้เป็นหนึ่งซ้ำ อ่านเพิ่มเติมในโพสต์บล็อก
MarcinJuraszek

4
น่าสนใจ ขอผมชี้ให้เห็นว่าการวนรอบอาเรย์จะเร็วกว่าโซลูชั่น LINQ ที่ดีที่สุดถึง 10 เท่า ดังนั้นหากคุณไปล่าสัตว์เพื่อความสมบูรณ์อย่าใช้ LINQ ตั้งแต่แรก
usr

2
บางครั้งผู้คนถามหลังจากการวิจัยจริงนี่เป็นคำถามตัวอย่างหนึ่ง: ฉันไม่ใช่ผู้ใช้ C # ที่มาจาก Hot-question-list
Grijesh Chauhan

2
@ WiSaGaN นั่นเป็นจุดที่ดี อย่างไรก็ตามหากเป็นเช่นนี้เนื่องจากการเคลื่อนไหวของสาขาเทียบกับเงื่อนไขเราคาดว่าจะเห็นความแตกต่างที่น่าทึ่งที่สุดที่ 50% / 50% ที่นี่เราเห็นความแตกต่างที่น่าทึ่งที่สุดในตอนท้ายซึ่งการแตกแขนงนั้นสามารถคาดเดาได้มากที่สุด หาก Where is a branch และ ternary เป็นการย้ายแบบมีเงื่อนไขเราจะคาดหวังว่า Where times จะกลับมาอีกเมื่อองค์ประกอบทั้งหมดถูกต้อง แต่ก็ไม่เคยกลับมาลง
John Tseng

คำตอบ:


131

Selectวนซ้ำทั้งชุดและสำหรับแต่ละรายการจะดำเนินการตามเงื่อนไขสาขา (ตรวจสอบความถูกต้อง) และการ+ดำเนินการ

Where+Selectสร้างตัววนซ้ำที่ข้ามองค์ประกอบที่ไม่ถูกต้อง (ไม่ใช่yieldพวกมัน) โดยแสดง+เฉพาะรายการที่ถูกต้องเท่านั้น

ดังนั้นราคาสำหรับ a Select:

t(s) = n * ( cost(check valid) + cost(+) )

และสำหรับWhere+Select:

t(ws) = n * ( cost(check valid) + p(valid) * (cost(yield) + cost(+)) )

ที่ไหน:

  • p(valid) ความน่าจะเป็นที่รายการในรายการนั้นถูกต้อง
  • cost(check valid) เป็นค่าใช้จ่ายของสาขาที่ตรวจสอบความถูกต้อง
  • cost(yield)คือค่าใช้จ่ายในการสร้างสถานะใหม่ของตัวwhereวนซ้ำซึ่งซับซ้อนกว่าตัววนซ้ำทั่วไปที่Selectเวอร์ชันใช้

ที่คุณสามารถดูเพื่อรับnการSelectรุ่นที่เป็นค่าคงที่ในขณะที่Where+Selectรุ่นเป็นสมการเชิงเส้นที่มีp(valid)เป็นตัวแปร ค่าจริงของต้นทุนกำหนดจุดตัดของสองบรรทัดและเนื่องจากcost(yield)อาจแตกต่างจากค่าcost(+)เหล่านั้นจึงไม่จำเป็นต้องตัดกันที่p(valid)= 0.5


34
+1 สำหรับการเป็นคำตอบเดียว (จนถึงตอนนี้) ที่ตอบคำถามไม่ได้เดาคำตอบและไม่เพียงสร้าง "ฉันด้วย!" สถิติ.
ไบนารี Worrier

4
ในทางเทคนิคแล้ววิธี LINQ จะสร้างทรีนิพจน์ที่ทำงานบนคอลเลกชันทั้งหมดเพียงครั้งเดียวแทนที่จะเป็น "ชุด"
Spoike

อะไรนะcost(append)? แม้ว่าคำตอบที่ดีจริงๆมองจากมุมที่แตกต่างมากกว่าแค่สถิติ
It'sNotALie

5
Whereไม่ได้สร้างอะไรเลยเพียงแค่คืนองค์ประกอบหนึ่งในเวลาจากsourceลำดับถ้าเพียงกรอกองค์ประกอบของคำกริยา
MarcinJuraszek

13
@Spoike - ทรีนิพจน์ไม่เกี่ยวข้องที่นี่เนื่องจากนี่คือlinq-to-objectsไม่ใช่ linq-to-something-else (Entity เป็นต้น) นั่นคือความแตกต่างระหว่างและIEnumerable.Select(IEnumerable, Func) IQueryable.Select(IQueryable, Expression<Func>)คุณพูดถูกว่า LINQ ไม่ทำอะไรเลยจนกว่าคุณจะทำซ้ำสิ่งที่สะสมไว้ซึ่งอาจเป็นสิ่งที่คุณหมายถึง
Kobi

33

นี่คือคำอธิบายเชิงลึกเกี่ยวกับสิ่งที่ทำให้เกิดความแตกต่างของเวลา


Sum()ฟังก์ชั่นสำหรับIEnumerable<int>ลักษณะเช่นนี้

public static int Sum(this IEnumerable<int> source)
{
    int sum = 0;
    foreach(int item in source)
    {
        sum += item;
    }
    return sum;
}

ใน C #, foreachน้ำตาลเพียงประโยคสำหรับรุ่นสุทธิของของ iterator, (เพื่อไม่ให้สับสนกับ ) ดังนั้นโค้ดข้างต้นจึงถูกแปลเป็นอย่างนี้:IEnumerator<T> IEnumerable<T>

public static int Sum(this IEnumerable<int> source)
{
    int sum = 0;

    IEnumerator<int> iterator = source.GetEnumerator();
    while(iterator.MoveNext())
    {
        int item = iterator.Current;
        sum += item;
    }
    return sum;
}

จำไว้ว่าโค้ดสองบรรทัดที่คุณเปรียบเทียบมีดังต่อไปนี้

int result1 = myCollection.Where(mc => mc.IsValid).Sum(mc => mc.Value);
int result2 = myCollection.Sum(mc => mc.IsValid ? mc.Value : 0);

ตอนนี้นี่คือนักเตะ:

LINQ ใช้การดำเนินการที่ถูกเลื่อนออกไป ดังนั้นในขณะที่มันอาจปรากฏว่าresult1iterates มากกว่าการเก็บสองครั้งจริง ๆ แล้วมันซ้ำ iterates มันเพียงครั้งเดียว Where()สภาพถูกนำไปใช้จริงในช่วงSum()ภายในของการเรียกร้องให้MoveNext() (ซึ่งเป็นไปได้ที่จะขอบคุณความมหัศจรรย์ของyield return )

ซึ่งหมายความว่าสำหรับresult1รหัสภายในwhileวง

{
    int item = iterator.Current;
    sum += item;
}

mc.IsValid == trueจะถูกดำเนินการเพียงครั้งเดียวสำหรับแต่ละรายการด้วย โดยการเปรียบเทียบresult2จะรันรหัสนั้นสำหรับทุกรายการในคอลเลกชัน นั่นคือเหตุผลที่result1เร็วกว่าปกติ

(แม้ว่าทราบว่าการเรียกWhere()สภาพภายในMoveNext()ยังคงมีค่าใช้จ่ายบางส่วนที่มีขนาดเล็กดังนั้นหากส่วนใหญ่ / รายการทั้งหมดได้mc.IsValid == true, result2จะจริงจะได้เร็วขึ้น!)


หวังว่าตอนนี้มันชัดเจนว่าทำไมresult2มักจะช้ากว่า ตอนนี้ฉันต้องการที่จะอธิบายว่าทำไมฉันระบุไว้ในความคิดเห็นที่การเปรียบเทียบประสิทธิภาพของ LINQ เหล่านี้ไม่สำคัญไม่ได้เรื่อง

การสร้างการแสดงออก LINQ ราคาถูก การเรียกฟังก์ชั่นของผู้ร่วมประชุมมีราคาถูก การจัดสรรและวนซ้ำผ่านตัววนซ้ำมีราคาถูก แต่มันก็ถูกกว่าที่จะไม่ทำสิ่งเหล่านี้ ดังนั้นหากคุณพบว่าคำสั่ง LINQ เป็นคอขวดในโปรแกรมของคุณในประสบการณ์ของฉันการเขียนใหม่โดยไม่มี LINQ จะเป็นเสมอทำให้มันเร็วกว่าที่ใด ๆ ของวิธีการ LINQ ต่างๆ

ดังนั้นกระบวนการทำงานของ LINQ ของคุณควรมีลักษณะเช่นนี้:

  1. ใช้ LINQ ได้ทุกที่
  2. ข้อมูลส่วนตัว.
  3. หากผู้สร้างโปรไฟล์ระบุว่า LINQ เป็นสาเหตุของปัญหาคอขวดให้เขียนโค้ดใหม่โดยไม่ต้องใช้ LINQ

โชคดีที่คอขวด LINQ นั้นหายาก เฮคคอขวดเป็นของหายาก ฉันเขียนคำสั่ง LINQ หลายร้อยรายการในช่วงไม่กี่ปีที่ผ่านมาและจบลงด้วยการแทนที่ <1% และส่วนใหญ่นั้นเกิดจากLINQ2EF SQL ของไม่ได้เป็นความผิดของ LINQ

ดังนั้นเช่นเคยเขียนโค้ดที่ชัดเจนและสมเหตุสมผลก่อนแล้วรอจนกระทั่งหลังจากที่คุณทำโปรไฟล์ให้กังวลเกี่ยวกับการปรับให้เหมาะสมแบบไมโคร


3
ภาคผนวกเล็ก: คำตอบที่ได้รับการแก้ไขแล้ว
It'sNotALie

16

สิ่งที่ตลก คุณรู้วิธีการที่Sum(this IEnumerable<TSource> source, Func<TSource, int> selector)กำหนดไว้? มันใช้Selectวิธีการ!

public static int Sum<TSource>(this IEnumerable<TSource> source, Func<TSource, int> selector)
{
    return source.Select(selector).Sum();
}

ดังนั้นจริง ๆ แล้วมันควรจะทำงานเหมือนกันเกือบทั้งหมด ฉันทำการวิจัยอย่างรวดเร็วด้วยตัวเองและนี่คือผลลัพธ์:

Where -- mod: 1 result: 0, time: 371 ms
WhereSelect -- mod: 1  result: 0, time: 356 ms
Select -- mod: 1  result 0, time: 366 ms
Sum -- mod: 1  result: 0, time: 363 ms
-------------
Where -- mod: 2 result: 4999999, time: 469 ms
WhereSelect -- mod: 2  result: 4999999, time: 429 ms
Select -- mod: 2  result 4999999, time: 362 ms
Sum -- mod: 2  result: 4999999, time: 358 ms
-------------
Where -- mod: 3 result: 9999999, time: 441 ms
WhereSelect -- mod: 3  result: 9999999, time: 452 ms
Select -- mod: 3  result 9999999, time: 371 ms
Sum -- mod: 3  result: 9999999, time: 380 ms
-------------
Where -- mod: 4 result: 7500000, time: 571 ms
WhereSelect -- mod: 4  result: 7500000, time: 501 ms
Select -- mod: 4  result 7500000, time: 406 ms
Sum -- mod: 4  result: 7500000, time: 397 ms
-------------
Where -- mod: 5 result: 7999999, time: 490 ms
WhereSelect -- mod: 5  result: 7999999, time: 477 ms
Select -- mod: 5  result 7999999, time: 397 ms
Sum -- mod: 5  result: 7999999, time: 394 ms
-------------
Where -- mod: 6 result: 9999999, time: 488 ms
WhereSelect -- mod: 6  result: 9999999, time: 480 ms
Select -- mod: 6  result 9999999, time: 391 ms
Sum -- mod: 6  result: 9999999, time: 387 ms
-------------
Where -- mod: 7 result: 8571428, time: 489 ms
WhereSelect -- mod: 7  result: 8571428, time: 486 ms
Select -- mod: 7  result 8571428, time: 384 ms
Sum -- mod: 7  result: 8571428, time: 381 ms
-------------
Where -- mod: 8 result: 8749999, time: 494 ms
WhereSelect -- mod: 8  result: 8749999, time: 488 ms
Select -- mod: 8  result 8749999, time: 386 ms
Sum -- mod: 8  result: 8749999, time: 373 ms
-------------
Where -- mod: 9 result: 9999999, time: 497 ms
WhereSelect -- mod: 9  result: 9999999, time: 494 ms
Select -- mod: 9  result 9999999, time: 386 ms
Sum -- mod: 9  result: 9999999, time: 371 ms

สำหรับการใช้งานดังต่อไปนี้:

result = source.Where(x => x.IsValid).Sum(x => x.Value);
result = source.Select(x => x.IsValid ? x.Value : 0).Sum();
result = source.Sum(x => x.IsValid ? x.Value : 0);
result = source.Where(x => x.IsValid).Select(x => x.Value).Sum();

modหมายถึง: ทุก 1 จากmodรายการไม่ถูกต้อง: สำหรับmod == 1ทุกรายการไม่ถูกต้องสำหรับmod == 2รายการคี่ไม่ถูกต้อง ฯลฯ การรวบรวมมี10000000รายการ

ป้อนคำอธิบายรูปภาพที่นี่

และผลลัพธ์สำหรับการรวบรวมด้วย100000000รายการ:

ป้อนคำอธิบายรูปภาพที่นี่

อย่างที่คุณเห็นSelectและSumผลลัพธ์ค่อนข้างสอดคล้องกันในทุกmodค่า อย่างไรก็ตามwhereและwhere+ selectไม่ใช่


1
เป็นที่น่าสนใจมากในผลลัพธ์ของคุณวิธีการทั้งหมดเริ่มต้นในที่เดียวกันและแตกต่างกันในขณะที่ผลลัพธ์ของ It'sNotALie ข้ามตรงกลาง
John Tseng

6

ฉันเดาว่ารุ่นที่มีตัวกรอง 0s และพวกเขาไม่ใช่หัวข้อสำหรับผลรวม (เช่นคุณไม่ได้ดำเนินการเพิ่ม) นี่คือการคาดเดาเนื่องจากฉันไม่สามารถอธิบายได้ว่าการใช้แลมบ์ดานิพจน์เพิ่มเติมและการเรียกหลายวิธีมีประสิทธิภาพสูงกว่าการเพิ่ม 0 อย่างง่าย

เพื่อนของฉันแนะนำว่าข้อเท็จจริงที่ว่า 0 ในผลรวมอาจทำให้เกิดการลงโทษที่รุนแรงเนื่องจากการตรวจสอบมากเกินไป มันน่าสนใจที่จะเห็นว่าสิ่งนี้จะดำเนินการอย่างไรในบริบทที่ไม่ได้ตรวจสอบ


ทดสอบบางอย่างกับuncheckedจะทำให้มันเล็ก ๆ นิด ๆ หน่อย ๆ Selectดีกว่าสำหรับ
It'sNotALie

ใครบางคนสามารถพูดได้ว่าถ้าไม่ถูกตรวจสอบมีผลต่อวิธีการที่ถูกเรียกลงมาในกองหรือการดำเนินงานระดับบนสุดเท่านั้น?
Stilgar

1
@Stilgar ใช้ได้กับระดับสูงสุดเท่านั้น
Branko Dimitrijevic

ดังนั้นเราอาจต้องใช้ผลรวมที่ไม่ จำกัด และลองใช้วิธีนี้
Stilgar

5

การรันตัวอย่างต่อไปนี้เป็นที่ชัดเจนสำหรับฉันว่ามีเพียงครั้งเดียวที่ + Select สามารถมีประสิทธิภาพสูงกว่า Select ในความเป็นจริงเมื่อมีการทิ้งจำนวนที่ดี (ประมาณครึ่งหนึ่งของการทดสอบแบบไม่เป็นทางการของฉัน) ของรายการที่มีศักยภาพ ในตัวอย่างเล็ก ๆ ด้านล่างฉันได้ตัวเลขคร่าวๆจากตัวอย่างทั้งสองเมื่อที่ไหนที่ข้ามรายการ 4mil ประมาณ 10mil จาก 10mil ฉันทำงานในรีลีสและสั่งการทำงานอีกครั้งที่ + select vs select พร้อมผลลัพธ์เดียวกัน

static void Main(string[] args)
        {
            int total = 10000000;
            Random r = new Random();
            var list = Enumerable.Range(0, total).Select(i => r.Next(0, 5)).ToList();
            for (int i = 0; i < 4000000; i++)
                list[i] = 10;

            var sw = new Stopwatch();
            sw.Start();

            int sum = 0;

            sum = list.Where(i => i < 10).Select(i => i).Sum();            

            sw.Stop();
            Console.WriteLine(sw.ElapsedMilliseconds);

            sw.Reset();
            sw.Start();
            sum = list.Select(i => i).Sum();            

            sw.Stop();

            Console.WriteLine(sw.ElapsedMilliseconds);
        }

อาจจะไม่ว่าจะเป็นเพราะคุณไม่ทิ้งภายใต้การนับในSelect?
It'sNotALie

3
การใช้การดีบักนั้นไม่มีประโยชน์
MarcinJuraszek

1
@MarcinJuraszek อย่างชัดเจน หมายจริงๆจะบอกว่าผมวิ่งในรุ่น :)
DavidN

@ It'sNotALie นั่นคือประเด็น ฉันคิดว่าวิธีเดียวที่การเลือก + สามารถทำได้ดีกว่า Select คือเมื่อการกรองรายการจำนวนมากถูกรวม
DavidN

2
นั่นเป็นสิ่งที่คำถามของฉันระบุ พวกเขาผูกที่ประมาณ 60% เช่นเดียวกับตัวอย่างนี้ คำถามคือทำไมซึ่งไม่ตอบที่นี่
It'sNotALie

4

หากคุณต้องการความเร็วเพียงแค่การวนลูปตรงไปตรงมาน่าจะเป็นทางออกที่ดีที่สุดของคุณ และการทำforมีแนวโน้มที่จะดีกว่าforeach(สมมติว่าคอลเลกชันของคุณเป็นการเข้าถึงแบบสุ่มแน่นอน)

นี่คือการกำหนดเวลาที่ฉันได้รับด้วยองค์ประกอบ 10% ที่ไม่ถูกต้อง:

Where + Select + Sum:   257
Select + Sum:           253
foreach:                111
for:                    61

และด้วยองค์ประกอบที่ไม่ถูกต้อง 90%:

Where + Select + Sum:   177
Select + Sum:           247
foreach:                105
for:                    58

และนี่คือรหัสมาตรฐานของฉัน ...

public class MyClass {
    public int Value { get; set; }
    public bool IsValid { get; set; }
}

class Program {

    static void Main(string[] args) {

        const int count = 10000000;
        const int percentageInvalid = 90;

        var rnd = new Random();
        var myCollection = new List<MyClass>(count);
        for (int i = 0; i < count; ++i) {
            myCollection.Add(
                new MyClass {
                    Value = rnd.Next(0, 50),
                    IsValid = rnd.Next(0, 100) > percentageInvalid
                }
            );
        }

        var sw = new Stopwatch();
        sw.Restart();
        int result1 = myCollection.Where(mc => mc.IsValid).Select(mc => mc.Value).Sum();
        sw.Stop();
        Console.WriteLine("Where + Select + Sum:\t{0}", sw.ElapsedMilliseconds);

        sw.Restart();
        int result2 = myCollection.Select(mc => mc.IsValid ? mc.Value : 0).Sum();
        sw.Stop();
        Console.WriteLine("Select + Sum:\t\t{0}", sw.ElapsedMilliseconds);
        Debug.Assert(result1 == result2);

        sw.Restart();
        int result3 = 0;
        foreach (var mc in myCollection) {
            if (mc.IsValid)
                result3 += mc.Value;
        }
        sw.Stop();
        Console.WriteLine("foreach:\t\t{0}", sw.ElapsedMilliseconds);
        Debug.Assert(result1 == result3);

        sw.Restart();
        int result4 = 0;
        for (int i = 0; i < myCollection.Count; ++i) {
            var mc = myCollection[i];
            if (mc.IsValid)
                result4 += mc.Value;
        }
        sw.Stop();
        Console.WriteLine("for:\t\t\t{0}", sw.ElapsedMilliseconds);
        Debug.Assert(result1 == result4);

    }

}

BTW ฉันเห็นด้วยกับการเดาของ Stilgar : ความเร็วสัมพัทธ์ของทั้งสองกรณีของคุณแตกต่างกันไปขึ้นอยู่กับเปอร์เซ็นต์ของรายการที่ไม่ถูกต้องเพียงเพราะจำนวนของงานที่Sumต้องทำแตกต่างกันไปในกรณี "Where"


1

แทนที่จะพยายามอธิบายผ่านคำอธิบายฉันจะใช้วิธีการทางคณิตศาสตร์มากขึ้น

รับรหัสด้านล่างซึ่งควรจะประมาณว่า LINQ กำลังทำอะไรภายในค่าใช้จ่ายที่เกี่ยวข้องมีดังนี้:
เลือกเท่านั้น: Nd + Na
โดยที่ + เลือก:Nd + Md + Ma

หากต้องการทราบจุดที่พวกเขาจะข้ามเราต้องทำพีชคณิตเล็กน้อย:
Nd + Md + Ma = Nd + Na => M(d + a) = Na => (M/N) = a/(d+a)

สิ่งนี้หมายความว่าเพื่อให้จุดโรคติดเชื้ออยู่ที่ 50% ค่าใช้จ่ายของการเรียกตัวแทนจะต้องประมาณเท่ากันกับค่าใช้จ่ายของการเพิ่ม เนื่องจากเรารู้ว่าจุดเปลี่ยนความจริงนั้นอยู่ที่ประมาณ 60% เราสามารถทำงานย้อนหลังและกำหนดว่าค่าใช้จ่ายของการมอบหมายผู้แทนสำหรับ @ It'sNotALie นั้นจริงแล้วประมาณ 2/3 ค่าใช้จ่ายของการเพิ่มซึ่งน่าแปลกใจ แต่นั่นคือสิ่งที่ ตัวเลขของเขาพูด

static void Main(string[] args)
{
    var set = Enumerable.Range(1, 10000000)
                        .Select(i => new MyClass {Value = i, IsValid = i%2 == 0})
                        .ToList();

    Func<MyClass, int> select = i => i.IsValid ? i.Value : 0;
    Console.WriteLine(
        Sum(                        // Cost: N additions
            Select(set, select)));  // Cost: N delegate
    // Total cost: N * (delegate + addition) = Nd + Na

    Func<MyClass, bool> where = i => i.IsValid;
    Func<MyClass, int> wSelect = i => i.Value;
    Console.WriteLine(
        Sum(                        // Cost: M additions
            Select(                 // Cost: M delegate
                Where(set, where),  // Cost: N delegate
                wSelect)));
    // Total cost: N * delegate + M * (delegate + addition) = Nd + Md + Ma
}

// Cost: N delegate calls
static IEnumerable<T> Where<T>(IEnumerable<T> set, Func<T, bool> predicate)
{
    foreach (var mc in set)
    {
        if (predicate(mc))
        {
            yield return mc;
        }
    }
}

// Cost: N delegate calls
static IEnumerable<int> Select<T>(IEnumerable<T> set, Func<T, int> selector)
{
    foreach (var mc in set)
    {
        yield return selector(mc);
    }
}

// Cost: N additions
static int Sum(IEnumerable<int> set)
{
    unchecked
    {
        var sum = 0;
        foreach (var i in set)
        {
            sum += i;
        }

        return sum;
    }
}

0

ฉันคิดว่ามันน่าสนใจที่ผลลัพธ์ของ MarcinJuraszek นั้นแตกต่างจากของ It'sNotALie โดยเฉพาะอย่างยิ่งผลลัพธ์ของ MarcinJuraszek เริ่มต้นด้วยการใช้งานทั้งสี่อย่างในสถานที่เดียวกันในขณะที่ผลลัพธ์ของ It'sNotALie ข้ามไปตรงกลาง ฉันจะอธิบายวิธีการทำงานจากแหล่งที่มา

ให้เราสมมติว่ามีnองค์ประกอบทั้งหมดและmองค์ประกอบที่ถูกต้อง

Sumฟังก์ชั่นสวยเรียบง่าย มันวนซ้ำผ่านตัวแจงนับ: http://typedescriptor.net/browse/members/367300-System.Linq.Enumerable.Sum(IEnumerable%601)

เพื่อความง่ายสมมติว่าคอลเลกชันเป็นรายการ ทั้งเลือกและWhereSelectWhereSelectListIteratorจะสร้าง ซึ่งหมายความว่าตัววนซ้ำจริงที่สร้างขึ้นเหมือนกัน ในทั้งสองกรณีมีSumที่ loops กว่า iterator WhereSelectListIteratorที่ ส่วนที่น่าสนใจที่สุดของตัววนซ้ำคือเมธอดMoveNext

เนื่องจากตัววนซ้ำเหมือนกันจึงเป็นลูปเดียวกัน ข้อแตกต่างอยู่ในร่างกายของลูปเท่านั้น

เนื้อแกะเหล่านี้มีราคาใกล้เคียงกันมาก ส่วนคำสั่ง where ส่งคืนค่าเขตข้อมูลและส่วนที่ประกอบไปด้วยสามภาคจะส่งคืนค่าเขตข้อมูลด้วย ส่วนคำสั่ง select ส่งคืนค่าฟิลด์และทั้งสองสาขาของผู้ประกอบการที่ประกอบไปด้วยส่งคืนค่าฟิลด์หรือค่าคงที่ ประโยคเลือกแบบรวมมีสาขาเป็นตัวดำเนินการที่ประกอบไปด้วยสามส่วน แต่ WhereSelect ใช้สาขาในMoveNextใช้สาขาใน

อย่างไรก็ตามการดำเนินการทั้งหมดเหล่านี้ค่อนข้างถูก การดำเนินการที่แพงที่สุดคือสาขาที่การคาดการณ์ผิด ๆ จะทำให้เรา

การดำเนินการที่มีราคาแพงอื่นที่นี่คือ Invokeอีกงานราคาแพงที่นี่เป็นการเรียกใช้ฟังก์ชันใช้เวลานานกว่าการเพิ่มค่าเล็กน้อยเนื่องจาก Branko Dimitrijevic แสดง

การชั่งน้ำหนักยังเป็นการสะสมที่ตรวจสอบด้วย Sumด้วย หากตัวประมวลผลไม่มีแฟล็ก overflow ทางคณิตศาสตร์นี่อาจเป็นค่าใช้จ่ายในการตรวจสอบเช่นกัน

ดังนั้นต้นทุนที่น่าสนใจคือ:

  1. ( n+ m) * วิงวอน + m*checked+=
  2. n* วิงวอน + n*checked+=

ดังนั้นหากค่าใช้จ่ายของ Invoke สูงกว่าค่าใช้จ่ายของการสะสมที่ตรวจสอบแล้วกรณีที่ 2 จะดีกว่าเสมอ หากพวกมันใกล้เคียงกันเราจะเห็นยอดคงเหลือเมื่อองค์ประกอบครึ่งหนึ่งถูกต้อง

ดูเหมือนว่าในระบบของ MarcinJuraszek ที่ถูกตรวจสอบ + = มีค่าใช้จ่ายเล็กน้อย แต่ในระบบของ ItNotALie และ Branko Dimitrijevic การตรวจสอบ + = มีค่าใช้จ่ายที่สำคัญ ดูเหมือนว่ามันแพงที่สุดในระบบของ It'sNotALie เนื่องจากจุดคุ้มทุนสูงกว่ามาก ดูเหมือนว่าไม่มีใครโพสต์ผลลัพธ์จากระบบที่มีค่าใช้จ่ายสะสมมากกว่า Invoke


@ It'sNotALie ฉันไม่คิดว่าใครจะมีผลที่ผิด ฉันไม่สามารถอธิบายบางสิ่งได้ ฉันสันนิษฐานว่าค่าใช้จ่ายของ Invoke นั้นสูงกว่า + = มาก แต่เป็นไปได้ที่พวกเขาจะได้ใกล้ชิดมากขึ้นขึ้นอยู่กับการปรับแต่งฮาร์ดแวร์ให้เหมาะสม
John Tseng
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.