¿Por qué el compilador de C # se vuelve loco con esta consulta LINQ anidada?

97

Intente compilar el siguiente código y encontrará que el compilador toma> 3 GB de RAM (toda la memoria libre en mi máquina) y mucho tiempo para compilar (en realidad, obtengo una excepción IO después de 10 minutos).

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)))))));
    }
}

¿Alguien puede explicar este curioso comportamiento?

Versión de CS: Microsoft (R) Visual C # Compiler versión 4.0.30319.17929
Nombre del sistema operativo: Microsoft Windows 7 Ultimate
Versión del SO: 6.1.7601 Service Pack 1 Build 7601

Uso de memoria

Eugene D. Gubenkov
fuente
5
¡Buena llamada! Acabo de pegar el código en Visual Studio y consumió todos los 4 Gb que se permiten en un proceso de 32 bits y luego se bloqueó (2013 Ultimate en Windows 8.1).
satnhak
2
Agregue este código a una base de código compartida (usando el bloc de notas) y observe cómo fallan las máquinas de sus compañeros de trabajo.
usr
3
Parece algo bueno informar sobre Microsoft Connect y para el equipo de Roslyn si su compilador muestra el mismo comportamiento.
Trillian
3
Creo que escuché a Eric Lippert decir en alguna parte (aunque no recuerdo dónde) que anidar lambdas con demasiada frecuencia con inferencia de tipo puede causarle al compilador algunos dolores de cabeza desagradables. No puedo pensar dónde lo he visto, así que no puedo citar una referencia. Con suerte, el hombre mismo puede ver esto y comentar ...
Chris
2
Bien hecho, recórtelo y puede tener una buena respuesta para esto: Crash su compilador favorito
Nathan Cooper

Respuestas:

40

Creo que está relacionado con la inferencia de tipos y / o la generación de lambda (cuando la inferencia de tipos tiene que ir en la dirección opuesta a la normal), combinada con la resolución de sobrecarga. Desafortunadamente, solo proporcionar los parámetros de tipo no ayuda a la situación (donde presumiblemente todavía tiene que realizar la verificación de tipo).

El siguiente código, que lógicamente debería ser el código equivalente al suyo, después de analizar las lambdas, se compila sin problemas:

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;
}

Creo que Eric Lippert ha publicado antes que la inferencia de tipos es uno de los lugares en el compilador de C # donde (ciertos problemas) pueden obligar al compilador a intentar resolver un problema NP-Complete y su única estrategia real (como aquí) es la fuerza bruta. Si puedo encontrar las referencias relevantes, las agregaré aquí.


La mejor referencia que puedo encontrar es aquí, donde Eric está discutiendo el hecho de que es el trabajo de resolución de sobrecarga lo que causa el costo real; recuerde, Enumerable.Sum tiene 10 sobrecargas que aceptan un método lambda /.

Damien_The_Unbeliever
fuente
1
Entonces, básicamente, el compilador se abre paso a través de 10^ncombinaciones (donde nestá la cantidad de métodos encadenados). Suena razonable (como explicación, claro).
DarkWanderer
1
@DarkWanderer:that^numberofpossibletypes
leppie
@Damien_The_Unbeliever, entiendo su pensamiento, parece realmente razonable (por cierto, el código no se compila )
Eugene D. Gubenkov
15
Su análisis aquí es correcto. Cada lambda introduce un factor de diez posibles sobrecargas y deben comprobarse todas las combinaciones. Consideré agregar código que se rescató cuando la cantidad de combinaciones aumentó pero nunca terminé de implementarlo.
Eric Lippert
2
@EricLippert ¡Es hora de una solicitud de extracción! : D
Rob H