¿Qué garantías hay sobre la complejidad en tiempo de ejecución (Big-O) de los métodos LINQ?


Recientemente he comenzado a usar LINQ bastante, y realmente no he visto ninguna mención de la complejidad en tiempo de ejecución para ninguno de los métodos LINQ. Obviamente, hay muchos factores en juego aquí, así que vamos a restringir la discusión al proveedor simple IEnumerable LINQ-to-Objects. Además, supongamos que cualquier Func pasado como un selector / mutador / etc. es una operación barata O (1).

Parece obvio que todas las operaciones de un solo paso(Select, Where, Count, Take/Skip, Any/All, etc.) será O (n), ya que solo necesitan caminar la secuencia una vez; aunque incluso esto está sujeto a la pereza.

Las cosas son más turbias para las operaciones más complejas; los operadores (Union, Distinct, Except, etc.) trabajar usando GetHashCode por defecto (afaik), por lo que parece razonable suponer que están usando una tabla hash internamente, haciendo estas operaciones O(n) también, en general. ¿Qué pasa con las versiones que utilizan un IEqualityComparer?

OrderBy necesitaría una clase, así que lo más probable es que estemos mirando O (n log y). ¿Y si ya está arreglado? ¿Qué tal si digo OrderBy().ThenBy() y doy la misma clave a ambos?

Pude ver GroupBy (y Join) usando ya sea ordenación, o hashing. ¿Cuál es?

Contains sería O(n) en un List, pero O (1) en un HashSet - ¿LINQ comprueba el contenedor subyacente para ver si puede acelerar las cosas?

Y la verdadera pregunta - hasta ahora, he estado tomando la fe de que las operaciones se realizan. Sin embargo, ¿puedo contar con eso? Contenedores STL, para ejemplo, especificar claramente la complejidad de cada operación. ¿Existen garantías similares sobre el rendimiento de LINQ en la especificación de la biblioteca. NET?

Más pregunta (en respuesta a los comentarios):
Realmente no había pensado en los gastos generales, pero no esperaba que hubiera mucho para Linq-to-Objects simples. La publicación de CodingHorror está hablando de Linq-to-SQL, donde puedo entender que el análisis de la consulta y hacer SQL agregaría costos - ¿hay un costo similar para el proveedor de objetos también? Si entonces, ¿es diferente si estás usando la sintaxis declarativa o funcional?

Author: tzaman, 2010-05-10

5 answers

Hay muy, muy pocas garantías, pero hay algunas optimizaciones:{[19]]}

  • Métodos de extensión que utilizan acceso indexado, como ElementAt, Skip, Last o LastOrDefault, comprobará si el tipo subyacente implementa o no IList<T>, de modo que obtenga acceso O(1) en lugar de O(N).

  • El método Count comprueba si hay una implementación ICollection, de modo que esta operación es O(1) en lugar de O(N).

  • Distinct, GroupBy Join, y creo que también el métodos de agregación de conjuntos (Union, Intersect y Except) usan hashing, por lo que deben estar cerca de O(N) en lugar de O(N2).

  • Contains comprueba una implementación ICollection, por lo que puede ser O(1) si la colección subyacente también es O(1), como a HashSet<T>, pero esto depende de la estructura de datos real y no está garantizado. Los conjuntos de hash anulan el método Contains, por eso son O(1).

  • OrderBy los métodos utilizan un quicksort estable, por lo que son O (N log N) un caso normal.

Creo que eso cubre la mayoría, si no todos, de los métodos de extensión integrados. Realmente hay muy pocas garantías de rendimiento; Linq en sí tratará de aprovechar las estructuras de datos eficientes, pero no es un pase libre para escribir código potencialmente ineficiente.

 97
Author: Aaronaught,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-05-09 23:16:36

Todo lo que puede contar es que los métodos Enumerables están bien escritos para el caso general y no utilizarán algoritmos ingenuos. Probablemente hay cosas de terceros (blogs, etc.) que describen los algoritmos realmente en uso, pero estos no son oficiales o garantizados en el sentido de que los algoritmos STL lo son.

Para ilustrar, aquí está el código fuente reflejado (cortesía de ILSpy) para Enumerable.Count del Sistema.Núcleo:

// System.Linq.Enumerable
public static int Count<TSource>(this IEnumerable<TSource> source)
{
    checked
    {
        if (source == null)
        {
            throw Error.ArgumentNull("source");
        }
        ICollection<TSource> collection = source as ICollection<TSource>;
        if (collection != null)
        {
            return collection.Count;
        }
        ICollection collection2 = source as ICollection;
        if (collection2 != null)
        {
            return collection2.Count;
        }
        int num = 0;
        using (IEnumerator<TSource> enumerator = source.GetEnumerator())
        {
            while (enumerator.MoveNext())
            {
                num++;
            }
        }
        return num;
    }
}

Como se puede ver, va a un cierto esfuerzo para evitar el ingenuo solución de simplemente enumerar cada elemento.

 5
Author: Marcelo Cantos,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-11-08 10:57:04

Hace tiempo que sé que .Count() devuelve .Count si la enumeración es un IList.

Pero siempre estaba un poco cansado de la complejidad en tiempo de ejecución de las operaciones de conjunto: .Intersect(), .Except(), .Union().

Aquí está la implementación de BCL (. NET 4.0/4.5) descompilada para .Intersect() (comentarios míos):

private static IEnumerable<TSource> IntersectIterator<TSource>(IEnumerable<TSource> first, IEnumerable<TSource> second, IEqualityComparer<TSource> comparer)
{
  Set<TSource> set = new Set<TSource>(comparer);
  foreach (TSource source in second)                    // O(M)
    set.Add(source);                                    // O(1)

  foreach (TSource source in first)                     // O(N)
  {
    if (set.Remove(source))                             // O(1)
      yield return source;
  }
}

Conclusiones:

  • el rendimiento es O (M + N)
  • la implementación no aprovechar cuando las colecciones ya son conjuntos. (Se puede no ser necesariamente sencillo, porque el IEqualityComparer<T> usado también necesita coincidir.)

Para completar, aquí están las implementaciones para .Union() y .Except().

Alerta de Spoiler: ellos también tienen O(N+M) complejidad.

private static IEnumerable<TSource> UnionIterator<TSource>(IEnumerable<TSource> first, IEnumerable<TSource> second, IEqualityComparer<TSource> comparer)
{
  Set<TSource> set = new Set<TSource>(comparer);
  foreach (TSource source in first)
  {
    if (set.Add(source))
      yield return source;
  }
  foreach (TSource source in second)
  {
    if (set.Add(source))
      yield return source;
  }
}


private static IEnumerable<TSource> ExceptIterator<TSource>(IEnumerable<TSource> first, IEnumerable<TSource> second, IEqualityComparer<TSource> comparer)
{
  Set<TSource> set = new Set<TSource>(comparer);
  foreach (TSource source in second)
    set.Add(source);
  foreach (TSource source in first)
  {
    if (set.Add(source))
      yield return source;
  }
}
 4
Author: Cristi Diaconescu,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-10-05 07:59:16

Acabo de sacar el reflector y comprueban el tipo subyacente cuando se llama Contains.

public static bool Contains<TSource>(this IEnumerable<TSource> source, TSource value)
{
    ICollection<TSource> is2 = source as ICollection<TSource>;
    if (is2 != null)
    {
        return is2.Contains(value);
    }
    return source.Contains<TSource>(value, null);
}
 2
Author: ChaosPandion,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-05-09 22:46:59

La respuesta correcta es "depende". depende de qué tipo de subyacente IEnumerable es. sé que para algunas colecciones (como las colecciones que implementan ICollection o IList) hay rutas de código especiales que se utilizan, sin embargo, la implementación real no está garantizada para hacer nada especial. por ejemplo, sé que ElementAt () tiene un caso especial para colecciones indexables, de manera similar con Count (). Pero en general, probablemente debería asumir el peor caso O (n) rendimiento.

En general, no creo que vaya a encontrar el tipo de garantías de rendimiento que desea, aunque si se encuentra con un problema de rendimiento particular con un operador linq, siempre puede volver a implementarlo para su colección particular. También hay muchos blogs y proyectos de extensibilidad que extienden Linq a Objetos para agregar este tipo de garantías de rendimiento. echa un vistazo a Indexed LINQ que se extiende y agrega al conjunto de operadores para obtener más rendimiento beneficio.

 2
Author: luke,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-05-09 23:17:04