เหตุใดคอมไพเลอร์ C # จึงคลั่งไคล้ในแบบสอบถาม LINQ ที่ซ้อนกันนี้


97

ลองคอมไพล์โค้ดต่อไปนี้แล้วคุณจะพบว่าคอมไพเลอร์ใช้ RAM มากกว่า 3 GB (หน่วยความจำว่างทั้งหมดในเครื่องของฉัน) และคอมไพล์นานมาก (อันที่จริงฉันได้รับข้อยกเว้น IO หลังจาก 10 นาที)

using System;
using System.Linq;

public class Test
{
    public static void Main()
    {
        Enumerable.Range(0, 1).Sum(a =>
        Enumerable.Range(0, 1).Sum(b =>
        Enumerable.Range(0, 1).Sum(c =>
        Enumerable.Range(0, 1).Sum(d =>
        Enumerable.Range(0, 1).Sum(e =>
        Enumerable.Range(0, 1).Sum(f =>
        Enumerable.Range(0, 1).Count(g => true)))))));
    }
}

ใครสามารถอธิบายพฤติกรรมที่น่าสงสัยนี้ได้หรือไม่?

เวอร์ชัน CS: Microsoft (R) Visual C # Compiler เวอร์ชัน 4.0.30319.17929
ชื่อระบบปฏิบัติการ: Microsoft Windows 7 Ultimate
เวอร์ชันระบบปฏิบัติการ: 6.1.7601 Service Pack 1 Build 7601

การใช้ความจำ


5
โทรดี! ฉันเพิ่งวางรหัสลงในวิชวลสตูดิโอและใช้ 4Gb ทั้งหมดที่อนุญาตให้ใช้กระบวนการ 32 บิตแล้วล้มเหลว (2013 Ultimate บน Windows 8.1)
เสาร์

2
เพิ่มรหัสนี้ลงในรหัสฐานที่ใช้ร่วมกัน (โดยใช้แผ่นจดบันทึก) และดูเครื่องจักรของเพื่อนร่วมงานของคุณขัดข้อง
usr

3
ดูเหมือนจะเป็นเรื่องดีที่จะรายงานเกี่ยวกับ Microsoft Connect และต่อทีม Roslyn หากคอมไพเลอร์ของพวกเขาแสดงพฤติกรรมเดียวกัน
Trillian

3
ฉันเชื่อว่าฉันเคยได้ยิน Eric Lippert พูดที่ไหนสักแห่ง (แม้ว่าฉันจะจำไม่ได้ว่าที่ไหนก็ตาม) ว่า lambdas ที่ทำรังบ่อยเกินไปด้วยการอนุมานประเภทอาจทำให้ผู้รวบรวมปวดหัวที่น่ารังเกียจได้ ฉันไม่คิดว่าฉันเคยเห็นที่ไหนมา แต่ก็ไม่สามารถอ้างอิงได้ หวังว่าผู้ชายคนนี้อาจจะเห็นสิ่งนี้และแสดงความคิดเห็น ...
คริส

2
ทำได้ดีตัดมันลงและคุณอาจมีคำตอบที่ดีสำหรับสิ่งนี้: ชนคอมไพเลอร์ที่คุณชื่นชอบ
Nathan Cooper

คำตอบ:


40

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

รหัสต่อไปนี้ซึ่งควรเป็นรหัสที่เทียบเท่าจากของคุณในทางตรรกะหลังจากวิเคราะห์แลมบ์ดาแล้วคอมไพล์โดยไม่มีปัญหา:

static void Main()
{
    var x = Enumerable.Range(0, 1).Sum(a);
}

private static int a(int a)
{
    return Enumerable.Range(0, 1).Sum(b);
}
private static int b(int b)
{
    return Enumerable.Range(0, 1).Sum(c);
}
private static int c(int c)
{
    return Enumerable.Range(0, 1).Sum(d);
}
private static int d(int d)
{
    return Enumerable.Range(0, 1).Sum(e);
}
private static int e(int e)
{
    return Enumerable.Range(0, 1).Sum(f);
}
private static int f(int f)
{
    return Enumerable.Range(0, 1).Count(g);
}
private static bool g(int g)
{
    return true;
}

ฉันเชื่อว่า Eric Lippert ได้โพสต์ก่อนการอนุมานประเภทนั้นเป็นหนึ่งในสถานที่ในคอมไพเลอร์ C # ที่ (ปัญหาบางอย่าง) อาจบังคับให้คอมไพเลอร์พยายามแก้ปัญหา NP-Complete และกลยุทธ์ที่แท้จริงเพียงอย่างเดียว (ตามที่นี่) คือกำลังดุร้าย หากฉันสามารถค้นหาข้อมูลอ้างอิงที่เกี่ยวข้องได้ฉันจะเพิ่มที่นี่


ข้อมูลอ้างอิงที่ดีที่สุดที่ฉันสามารถหาได้คือที่นี่ซึ่ง Eric กำลังพูดถึงความจริงที่ว่ามันเป็นงานแก้ปัญหาโอเวอร์โหลดที่ทำให้เกิดต้นทุนจริง - จำไว้ว่าEnumerable Sumมี 10 overloads ที่ยอมรับ lambda / method


1
ดังนั้นโดยพื้นฐานแล้วคอมไพเลอร์ bruteforces วิธีการของเขาผ่าน10^nชุดค่าผสม ( nจำนวนของวิธีการผูกมัดอยู่ที่ไหน) ฟังดูสมเหตุสมผล (เป็นคำอธิบายนั่นคือ)
DarkWanderer

1
@DarkWanderer:that^numberofpossibletypes
leppie

@Damien_The_Unbeliever ฉันเข้าใจความคิดของคุณตะเข็บสมเหตุสมผลจริงๆ (โดยวิธีที่รหัสไม่รวบรวม )
Eugene D. Gubenkov

15
การวิเคราะห์ของคุณที่นี่ถูกต้อง แลมบ์ดาแต่ละตัวจะแนะนำตัวประกอบของการโอเวอร์โหลดที่เป็นไปได้ 10 ค่าและต้องตรวจสอบชุดค่าผสมทั้งหมด ฉันพิจารณาการเพิ่มรหัสที่ประกันตัวเมื่อจำนวนชุดค่าผสมมีมาก แต่ไม่ได้นำไปใช้
Eric Lippert

2
@EricLippert ถึงเวลาขอดึง! : D
Rob H
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.