¿Hay un impacto en el rendimiento al llamar a ToList ()?

139

Cuando se usa ToList(), ¿hay un impacto en el rendimiento que deba considerarse?

Estaba escribiendo una consulta para recuperar archivos de un directorio, que es la consulta:

string[] imageArray = Directory.GetFiles(directory);

Sin embargo, como me gusta trabajar con él List<>, decidí poner ...

List<string> imageList = Directory.GetFiles(directory).ToList();

Entonces, ¿hay algún tipo de impacto en el rendimiento que deba tenerse en cuenta al decidir hacer una conversión como esta, o que solo se tenga en cuenta al tratar con una gran cantidad de archivos? ¿Es esta una conversión insignificante?

Cody
fuente
+1 interesado en saber la respuesta aquí también. En mi humilde opinión a menos que la aplicación es crítica de rendimiento, creo que siempre haría uso de una List<T>a favor de una T[]si se hace que el código sea más lógico / lectura / mantenible (a menos que, por supuesto, la conversión fue causando notables problemas de rendimiento en cuyo caso me re- visitarlo, supongo).
Sepster
Crear una lista a partir de una matriz debería ser súper barato.
leppie
2
@Sepster Solo especifico el tipo de datos tan específicamente como necesito hacer un trabajo. Si no tengo que llamar Addo Remove, lo dejaría como IEnumerable<T>(o incluso mejor var)
pswg
44
Creo que, en este caso, es mejor llamar en EnumerateFileslugar de GetFiles, por lo que solo se creará una matriz.
tukaef
3
GetFiles(directory), como se implementa actualmente en .NET, prácticamente lo hace new List<string>(EnumerateFiles(directory)).ToArray(). Entonces GetFiles(directory).ToList()crea una lista, crea una matriz a partir de eso, luego crea una lista nuevamente. Como dice 2kay, deberías preferir hacer EnumerateFiles(directory).ToList()aquí.
Joren

Respuestas:

178

IEnumerable.ToList()

Sí, IEnumerable<T>.ToList()tiene un impacto en el rendimiento, es una operación O (n) , aunque es probable que solo requiera atención en las operaciones críticas de rendimiento.

La ToList()operación utilizará el List(IEnumerable<T> collection)constructor. Este constructor debe hacer una copia de la matriz (más generalmente IEnumerable<T>), de lo contrario, las modificaciones futuras de la matriz original también cambiarán en la fuente T[], lo que generalmente no sería deseable.

Me gustaría reiterar que esto solo marcará la diferencia con una lista enorme, la copia de fragmentos de memoria es una operación bastante rápida de realizar.

Consejo práctico, AsvsTo

Notarás que en LINQ hay varios métodos que comienzan con As(como AsEnumerable()) y To(como ToList()). Los métodos que comienzan con Torequieren una conversión como la anterior (es decir, pueden afectar el rendimiento), y los métodos que comienzan con Asno requieren y solo requerirán un poco de conversión o una operación simple.

Detalles adicionales sobre List<T>

Aquí hay un poco más de detalles sobre cómo List<T>funciona en caso de que esté interesado :)

A List<T>también utiliza una construcción llamada matriz dinámica que debe redimensionarse a pedido, este evento de cambio de tamaño copia el contenido de una matriz antigua a la nueva matriz. Por lo tanto, comienza pequeño y aumenta de tamaño si es necesario .

Esta es la diferencia entre los atributos Capacityy Counten List<T>. Capacityse refiere al tamaño de la matriz detrás de escena, Countes el número de elementos en el List<T>que siempre está <= Capacity. Entonces, cuando se agrega un elemento a la lista, incrementándolo Capacity, el tamaño del mismo List<T>se duplica y la matriz se copia.

Daniel Imms
fuente
2
Solo quería enfatizar que el List(IEnumerable<T> collection)constructor verifica si el parámetro de colección es ICollection<T>y luego crea una nueva matriz interna con el tamaño requerido de inmediato. Si la colección de parámetros no lo es ICollection<T>, el constructor lo itera y llama Adda cada elemento.
Justinas Simanavicius
Es importante tener en cuenta que a menudo puede ver ToList () como una operación engañosamente exigente. Esto sucede cuando crea una consulta IEnumerable <> througha LINQ. la consulta linq se construye pero no se ejecuta. llamar a ToList () ejecutará la consulta y, por lo tanto, parecerá intensivo en recursos, pero es la consulta la que es intensiva y no la operación ToList () (a menos que sea una lista realmente enorme)
dancer42
36

¿Hay un impacto en el rendimiento al llamar a toList ()?

Sí, por supuesto. Teóricamente, incluso i++tiene un impacto en el rendimiento, ralentiza el programa por unos pocos tics.

¿Qué .ToListhacer?

Cuando invocas .ToList, el código llama, Enumerable.ToList()que es un método de extensión que return new List<TSource>(source). En el constructor correspondiente, en la peor de las circunstancias, pasa por el contenedor del elemento y los agrega uno por uno a un nuevo contenedor. Por lo tanto, su comportamiento afecta poco al rendimiento. Es imposible ser un cuello de botella de rendimiento de su aplicación.

¿Qué hay de malo con el código en la pregunta?

Directory.GetFilespasa a través de la carpeta y devuelve todos los nombres de los archivos inmediatamente a la memoria, tiene el riesgo potencial de que la cadena [] cueste mucha memoria, ralentizando todo.

¿Qué se debe hacer entonces?

Depende. Si usted (así como su lógica empresarial) garantiza que la cantidad de archivos en la carpeta siempre es pequeña, el código es aceptable. Pero aún se sugiere usar una versión perezosa: Directory.EnumerateFilesen C # 4. Esto es mucho más como una consulta, que no se ejecutará de inmediato, puede agregar más consultas como:

Directory.EnumerateFiles(myPath).Any(s => s.Contains("myfile"))

que dejará de buscar la ruta tan pronto como se encuentre un archivo cuyo nombre contenga "myfile". Obviamente, esto tiene un mejor rendimiento entonces .GetFiles.

Cheng Chen
fuente
19

¿Hay un impacto en el rendimiento al llamar a toList ()?

Sí hay. El uso del método de extensión Enumerable.ToList()construirá un nuevo List<T>objeto de la IEnumerable<T>colección de origen que, por supuesto, tiene un impacto en el rendimiento.

Sin embargo, la comprensión List<T>puede ayudarlo a determinar si el impacto en el rendimiento es significativo.

List<T>usa una matriz ( T[]) para almacenar los elementos de la lista. Las matrices no se pueden extender una vez que se asignan, por List<T>lo que utilizarán una matriz sobredimensionada para almacenar los elementos de la lista. Cuando List<T>crece más allá del tamaño de la matriz subyacente, debe asignarse una nueva matriz y el contenido de la matriz anterior debe copiarse en la nueva matriz más grande antes de que la lista pueda crecer.

Cuando List<T>se construye una nueva a partir de una, IEnumerable<T>hay dos casos:

  1. La colección de origen se implementa ICollection<T>: luego ICollection<T>.Countse utiliza para obtener el tamaño exacto de la colección de origen y se asigna una matriz de respaldo coincidente antes de que todos los elementos de la colección de origen se copien en la matriz de respaldo mediante ICollection<T>.CopyTo(). Esta operación es bastante eficiente y probablemente se asignará a algunas instrucciones de la CPU para copiar bloques de memoria. Sin embargo, en términos de rendimiento, se requiere memoria para la nueva matriz y se requieren ciclos de CPU para copiar todos los elementos.

  2. De lo contrario, se desconoce el tamaño de la colección de origen y IEnumerable<T>se utiliza el enumerador de para agregar cada elemento de origen, uno por vez, al nuevo List<T>. Inicialmente, la matriz de respaldo está vacía y se crea una matriz de tamaño 4. Luego, cuando esta matriz es demasiado pequeña, el tamaño se duplica, por lo que la matriz de respaldo crece de esta manera 4, 8, 16, 32, etc. Esta operación es mucho más costosa en comparación con el primer caso en el que se puede crear una matriz del tamaño correcto de inmediato.

    Además, si su colección de origen contiene, digamos, 33 elementos, la lista terminará usando una matriz de 64 elementos que desperdiciarán algo de memoria.

En su caso, la colección fuente es una matriz que se implementa, ICollection<T>por lo que el impacto en el rendimiento no es algo de lo que deba preocuparse a menos que su matriz fuente sea muy grande. Las llamadas ToList()simplemente copiarán la matriz fuente y la envolverán en un List<T>objeto. Incluso el rendimiento del segundo caso no es algo de qué preocuparse para las colecciones pequeñas.

Martin Liversage
fuente
5

"¿Hay un impacto en el rendimiento que deba considerarse?"

El problema con su escenario preciso es que, ante todo, su verdadera preocupación sobre el rendimiento sería la velocidad del disco duro y la eficiencia de la memoria caché del disco.

Desde esa perspectiva, el impacto seguramente es insignificante hasta el punto de que NO no necesita ser considerado.

PERO SOLO si realmente necesita las características de la List<>estructura para posiblemente hacerlo más productivo, o su algoritmo más amigable, o alguna otra ventaja. De lo contrario, solo está agregando deliberadamente un éxito de rendimiento insignificante, sin ninguna razón. En cuyo caso, naturalmente, ¡no deberías hacerlo! :)

jross
fuente
4

ToList()crea una nueva Lista y coloca los elementos en ella, lo que significa que hay un costo asociado con hacerlo ToList(). En el caso de una colección pequeña, no será un costo muy notable, pero tener una colección enorme puede causar un impacto en el rendimiento en caso de usar ToList.

En general, no debe usar ToList () a menos que el trabajo que está haciendo no se pueda hacer sin convertir la colección a List. Por ejemplo, si solo desea iterar por la colección, no necesita realizar ToList

Si está realizando consultas en una fuente de datos, por ejemplo, una base de datos que utiliza LINQ to SQL, el costo de hacer ToList es mucho mayor porque cuando usa ToList con LINQ to SQL en lugar de hacer una ejecución demorada, es decir, cargar elementos cuando sea necesario (lo que puede ser beneficioso) en muchos escenarios) carga instantáneamente elementos de la base de datos en la memoria

Haris Hasan
fuente
Haris: de lo que no estoy seguro acerca de la fuente original, qué pasará con la fuente original después de llamar a ToList ()
TalentTuner
@Saurabh GC lo limpiará
pswg
@Saurabh no pasará nada a la fuente original. Los elementos de la fuente original serán referenciados por la lista recién creada
Haris Hasan
"si solo desea recorrer la colección, no necesita realizar ToList", entonces, ¿cómo debe iterar?
SharpC
4

Será tan (in) eficiente como hacer:

var list = new List<T>(items);

Si desmonta el código fuente del constructor que toma un IEnumerable<T>, verá que hará algunas cosas:

  • Llame collection.Count, así que si collectiones un IEnumerable<T>, forzará la ejecución. Si collectiones una matriz, lista, etc., debería ser O(1).

  • Si se collectionimplementa ICollection<T>, guardará los elementos en una matriz interna utilizando el ICollection<T>.CopyTométodo Se debe ser O(n), siendo nla longitud de la colección.

  • Si collectionno se implementa ICollection<T>, iterará a través de los elementos de la colección y los agregará a una lista interna.

Entonces, sí, consumirá más memoria, ya que tiene que crear una nueva lista, y en el peor de los casos, lo seráO(n) , ya que iterará a través de collectionpara hacer una copia de cada elemento.

Oscar Mederos
fuente
3
close, 0(n)donde nestá la suma total de bytes que ocupan las cadenas de la colección original, no el recuento de los elementos (para ser más exactos n = bytes / tamaño de palabra)
user1416420
@ user1416420 Podría estar equivocado, pero ¿por qué es eso? ¿Y si se trata de una colección de algún otro tipo (por ejemplo. bool, int, Etc.)? Realmente no tiene que hacer una copia de cada cadena de la colección. Simplemente agréguelos a la nueva lista.
Oscar Mederos
todavía no importa la nueva asignación de memoria y la copia de bytes es lo que está matando este método. Un bool también ocupará 4 bytes en .NET. En realidad, cada referencia de un objeto en .NET tiene al menos 8 bytes de longitud, por lo que es bastante lenta. los primeros 4 bytes apuntan a la tabla de tipos y los segundos 4 bytes apuntan al valor o la ubicación de la memoria donde encontrar el valor
user1416420
3

Teniendo en cuenta el rendimiento de la recuperación de la lista de archivos, ToList()es insignificante. Pero en realidad no para otros escenarios. Eso realmente depende de dónde lo estés usando.

  • Al invocar una matriz, lista u otra colección, crea una copia de la colección como a List<T>. El rendimiento aquí depende del tamaño de la lista. Deberías hacerlo cuando sea realmente necesario.

    En su ejemplo, lo llama en una matriz. Se itera sobre la matriz y agrega los elementos uno por uno a una lista recién creada. Por lo tanto, el impacto en el rendimiento depende de la cantidad de archivos.

  • Al llamar a un IEnumerable<T>, materializa el IEnumerable<T>(generalmente una consulta).

Mohammad Dehghan
fuente
2

ToList creará una nueva lista y copiará elementos de la fuente original a la lista recién creada, por lo que lo único es copiar los elementos de la fuente original y depende del tamaño de la fuente

TalentTuner
fuente