Existe um impacto no desempenho ao chamar ToList ()?

139

Ao usar ToList(), há um impacto no desempenho que precisa ser considerado?

Eu estava escrevendo uma consulta para recuperar arquivos de um diretório, que é a consulta:

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

No entanto, desde que eu gosto de trabalhar List<>, decidi colocar ...

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

Portanto, existe algum tipo de impacto no desempenho que deve ser considerado ao decidir fazer uma conversão como essa - ou somente quando se lida com um grande número de arquivos? Esta é uma conversão insignificante?

Cody
fonte
+1 interessado em saber a resposta aqui também. IMHO, a menos que o aplicativo seja crítico para o desempenho, acho que eu sempre usaria um a List<T>favor de a T[]se tornasse o código mais lógico / legível / sustentável (a menos que a conversão estivesse causando problemas visíveis de desempenho, nesse caso, eu visitá-lo, eu acho).
Sepster
Criar uma lista a partir de uma matriz deve ser super barato.
leppie
2
@Sepster Especifico apenas o tipo de dados tão especificamente quanto preciso para realizar um trabalho. Se eu não tenho que chamar Addou Remove, gostaria de deixá-lo como IEnumerable<T>(ou até melhor var)
PSWG
4
Eu acho que, neste caso, é melhor chamar em EnumerateFilesvez de GetFiles, para que apenas uma matriz seja criada.
27513 tukaef
3
GetFiles(directory), como é implementado no .NET atualmente, praticamente faz new List<string>(EnumerateFiles(directory)).ToArray(). Então, GetFiles(directory).ToList()cria uma lista, cria uma matriz a partir disso e cria uma lista novamente. Como 2kay diz, você deve preferir fazer EnumerateFiles(directory).ToList()aqui.
Joren

Respostas:

178

IEnumerable.ToList()

Sim, IEnumerable<T>.ToList()tem um impacto no desempenho, é uma operação O (n) , embora provavelmente exija apenas atenção em operações críticas de desempenho.

A ToList()operação usará o List(IEnumerable<T> collection)construtor. Este construtor deve fazer uma cópia da matriz (de maneira mais geral IEnumerable<T>), caso contrário, futuras modificações da matriz original também serão alteradas na fonte, o T[]que geralmente não seria desejável.

Gostaria de reiterar que isso só fará diferença com uma lista enorme, pois copiar pedaços de memória é uma operação bastante rápida de executar.

Dica útil, AsvsTo

Você notará no LINQ que existem vários métodos que começam com As(como AsEnumerable()) e To(como ToList()). Os métodos iniciados com Toexigem uma conversão como acima (isto é, podem afetar o desempenho), e os métodos iniciados com Asnão exigem e apenas exigirão alguma operação simples ou de conversão .

Detalhes adicionais sobre List<T>

Aqui está um pouco mais detalhadamente de como List<T>funciona, caso você esteja interessado :)

A List<T>também usa uma construção chamada matriz dinâmica que precisa ser redimensionada sob demanda; esse evento de redimensionamento copia o conteúdo de uma matriz antiga para a nova matriz. Por isso, começa pequeno e aumenta de tamanho, se necessário .

Essa é a diferença entre os atributos Capacitye . refere-se ao tamanho da matriz nos bastidores, é o número de itens nos quais é sempre . Portanto, quando um item é adicionado à lista, aumentando-o , o tamanho do é dobrado e a matriz é copiada.CountList<T>CapacityCountList<T><= CapacityCapacityList<T>

Daniel Imms
fonte
2
Eu só queria enfatizar que o List(IEnumerable<T> collection)construtor verifica se o parâmetro de coleção é ICollection<T>e cria uma nova matriz interna com o tamanho necessário imediatamente. Se a coleção de parâmetros não for ICollection<T>, o construtor itera e chama Addcada elemento.
Justinas Simanavicius
É importante observar que muitas vezes você pode ver o ToList () como uma operação enganosa e exigente. Isso acontece quando você cria uma IEnumerable <> através de uma consulta LINQ. a consulta linq é construída, mas não executada. chamando ToList () irá executar a consulta e, portanto, parecem recurso intensivo -, mas é a consulta que é intensivo e não o ToList () operação (A menos que seja uma lista realmente enorme)
dancer42
36

Existe um impacto no desempenho ao chamar toList ()?

Sim, claro. Teoricamente, mesmo i++tendo um impacto no desempenho, ele atrasa o programa por talvez alguns tiques.

O que .ToListfaz?

Quando você invoca .ToList, o código chama Enumerable.ToList()que é um método de extensão que return new List<TSource>(source). No construtor correspondente, na pior circunstância, ele passa pelo contêiner de itens e os adiciona um a um em um novo contêiner. Portanto, seu comportamento afeta pouco o desempenho. É impossível ser um gargalo de desempenho do seu aplicativo.

O que há de errado com o código na pergunta

Directory.GetFilespassa pela pasta e retorna os nomes de todos os arquivos imediatamente para a memória, corre o risco de que a string [] gaste muita memória, diminuindo a velocidade de tudo.

O que deve ser feito então

Depende. Se você (assim como sua lógica comercial) garantir que o valor do arquivo na pasta seja sempre pequeno, o código será aceitável. Mas ainda é sugerido o uso de uma versão lenta: Directory.EnumerateFilesem C # 4. Isso é muito mais parecido com uma consulta, que não será executada imediatamente, você pode adicionar mais consultas como:

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

que irá parar de procurar o caminho assim que um arquivo cujo nome contenha "meuarquivo" for encontrado. Obviamente, isso tem um desempenho melhor então .GetFiles.

Cheng Chen
fonte
19

Existe um impacto no desempenho ao chamar toList ()?

Sim existe. O uso do método de extensão Enumerable.ToList()criará um novo List<T>objeto a partir da IEnumerable<T>coleção de fontes que, é claro, tem um impacto no desempenho.

No entanto, o entendimento List<T>pode ajudá-lo a determinar se o impacto no desempenho é significativo.

List<T>usa um array ( T[]) para armazenar os elementos da lista. As matrizes não podem ser estendidas depois de serem alocadas, portanto List<T>, usará uma matriz de tamanho grande para armazenar os elementos da lista. Quando o List<T>tamanho aumenta para além do tamanho da matriz subjacente, uma nova matriz precisa ser alocada e o conteúdo da matriz antiga deve ser copiado para a nova matriz maior antes que a lista possa crescer.

Quando um novo List<T>é construído a partir de um, IEnumerable<T>existem dois casos:

  1. A coleção de origem implementa ICollection<T>: Then ICollection<T>.Counté usada para obter o tamanho exato da coleção de origem e uma matriz de apoio correspondente é alocada antes de todos os elementos da coleção de origem serem copiados para a matriz de apoio usando ICollection<T>.CopyTo(). Esta operação é bastante eficiente e provavelmente será mapeada para algumas instruções da CPU para copiar blocos de memória. No entanto, em termos de desempenho, a memória é necessária para a nova matriz e os ciclos da CPU são necessários para copiar todos os elementos.

  2. Caso contrário, o tamanho da coleção de fontes é desconhecido e o enumerador de IEnumerable<T>é usado para adicionar cada elemento de origem, um de cada vez, ao novo List<T>. Inicialmente, a matriz de apoio está vazia e uma matriz de tamanho 4 é criada. Então, quando essa matriz é muito pequena, o tamanho é dobrado, aumentando assim a matriz de apoio 4, 8, 16, 32 etc. Toda vez que a matriz de apoio cresce, ela deve ser realocada e todos os elementos armazenados até agora devem ser copiados. Essa operação é muito mais cara em comparação com o primeiro caso em que uma matriz do tamanho correto pode ser criada imediatamente.

    Além disso, se sua coleção de fontes contiver, digamos, 33 elementos, a lista terminará usando uma matriz de 64 elementos que estão desperdiçando alguma memória.

No seu caso, a coleção de fontes é uma matriz implementada ICollection<T>para que o impacto no desempenho não seja algo com que você deva se preocupar, a menos que sua matriz de origem seja muito grande. A chamada ToList()simplesmente copiará a matriz de origem e a envolverá em um List<T>objeto. Mesmo o desempenho do segundo caso não é motivo de preocupação para pequenas coleções.

Martin Liversage
fonte
5

"existe um impacto no desempenho que precisa ser considerado?"

O problema com seu cenário preciso é que, em primeiro lugar, sua verdadeira preocupação com o desempenho seria a velocidade e a eficiência do disco rígido do cache da unidade.

Nessa perspectiva, o impacto é certamente insignificante, a ponto de NÃO ser necessário considerar.

MAS SOMENTE se você realmente precisar dos recursos da List<>estrutura para torná-lo mais produtivo ou seu algoritmo mais amigável ou outra vantagem. Caso contrário, você está apenas propositalmente adicionando um desempenho insignificante, sem nenhuma razão. Nesse caso, naturalmente, você não deve fazê-lo! :)

jross
fonte
4

ToList()cria uma nova lista e coloca os elementos nela, o que significa que há um custo associado ao fazer ToList(). No caso de coleção pequena, não será um custo muito perceptível, mas ter uma coleção enorme pode causar um impacto no desempenho no caso de usar a ToList.

Geralmente você não deve usar ToList (), a menos que o trabalho que você está fazendo não possa ser realizado sem converter a coleção em Lista. Por exemplo, se você deseja apenas percorrer a coleção, não precisa executar a ToList

Se você estiver executando consultas em uma fonte de dados, por exemplo, um banco de dados usando LINQ to SQL, o custo de fazer a ToList é muito mais alto porque, quando você usa a ToList com LINQ to SQL em vez de executar a execução atrasada, ou seja, carregar itens quando necessário (o que pode ser benéfico em muitos cenários) carrega instantaneamente itens do banco de dados na memória

Haris Hasan
fonte
Haris: O que eu não estou certo sobre a fonte original que vai acontecer com a fonte original depois de chamar à ToList ()
TalentTuner
@Saurabh GC vai limpá-lo
PSWG
@Saurabh nada vai acontecer com a fonte original. Elementos da fonte original será referenciado pela lista recém-criado
Haris Hasan
"se você apenas deseja percorrer a coleção, não precisa executar o ToList" - então, como você deve percorrer?
SharpC
4

Será tão (in) eficiente quanto fazer:

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

Se você desmontar o código-fonte do construtor que recebe um IEnumerable<T>, verá que ele fará algumas coisas:

  • Chamada collection.Count, portanto, se collectionfor um IEnumerable<T>, forçará a execução. Se collectioné uma matriz, lista, etc., deve ser O(1).

  • Se collectionimplementado ICollection<T>, ele salvará os itens em uma matriz interna usando o ICollection<T>.CopyTométodo Ele deve ser O(n), sendo no comprimento da coleção.

  • Se collectionnão for implementado ICollection<T>, ele percorrerá os itens da coleção e os adicionará a uma lista interna.

Portanto, sim, ele consumirá mais memória, pois precisará criar uma nova lista e, na pior das hipóteses, seráO(n) , pois irá percorrer a página collectionpara fazer uma cópia de cada elemento.

Oscar Mederos
fonte
3
perto, 0(n)onde né a soma total de bytes as cadeias na coleção original ocupam, e não a contagem dos elementos (bem para ser mais exacto n = bytes / palavra tamanho)
user1416420
@ user1416420 Posso estar errado, mas por que isso? E se for uma coleção de algum outro tipo (por exemplo. bool, intEtc.)? Você realmente não precisa fazer uma cópia de cada sequência na coleção. Você acabou de adicioná-los à nova lista.
Oscar Mederos
ainda não importa a nova alocação de memória e a cópia de bytes é o que está matando esse método. Um bool também ocupará 4 bytes no .NET. Na verdade, cada referência de um objeto no .NET tem pelo menos 8 bytes, por isso é bem lenta. os primeiros 4 bytes apontar para a tabela do tipo e as segundas 4 bytes apontar para o valor ou posição de memória onde encontrar o valor
user1416420
3

Considerando o desempenho da recuperação da lista de arquivos, ToList()é insignificante. Mas não para outros cenários. Isso realmente depende de onde você o está usando.

  • Ao chamar uma matriz, lista ou outra coleção, você cria uma cópia da coleção como uma List<T>. O desempenho aqui depende do tamanho da lista. Você deve fazê-lo quando realmente necessário.

    No seu exemplo, você o chama em uma matriz. Ele itera sobre a matriz e adiciona os itens um por um a uma lista recém-criada. Portanto, o impacto no desempenho depende do número de arquivos.

  • Ao chamar um IEnumerable<T>, você materializa o IEnumerable<T>(geralmente uma consulta).

Mohammad Dehghan
fonte
2

ToList Criará uma nova lista e copiará os elementos da fonte original para a lista recém-criada, portanto, o único é copiar os elementos da fonte original e depender do tamanho da fonte.

TalentTuner
fonte