O LINQ requer significativamente mais ciclos de processamento e memória do que as técnicas de iteração de dados de nível inferior?

8

fundo

Recentemente, estou no processo de duradouras entrevistas técnicas cansativas para posições que usam a pilha .NET, algumas das quais incluem perguntas tolas como essa e algumas que são mais válidas. Recentemente, deparei com um problema que pode ser válido, mas quero verificar com a comunidade aqui para ter certeza.

Quando perguntado por um entrevistador como eu contaria a frequência das palavras em um documento de texto e classificaria os resultados, respondi que

  1. Use um objeto de fluxo e coloque o arquivo de texto na memória como uma sequência.
  2. Divida a string em uma matriz nos espaços, ignorando a pontuação.
  3. Use LINQ contra a matriz para .GroupBy()e .Count(), em seguida, OrderBy()disse contagem.

Entendi errado esta resposta por dois motivos:

  1. A transmissão de um arquivo de texto inteiro na memória pode ser desastrosa. E se fosse uma enciclopédia inteira? Em vez disso, devo transmitir um bloco de cada vez e começar a construir uma tabela de hash.
  2. O LINQ é muito caro e requer muitos ciclos de processamento. Em vez disso, eu deveria ter criado uma tabela de hash e, para cada iteração, apenas adicionaria uma palavra à tabela de hash se ela não existisse e aumentaria sua contagem.

A primeira razão parece, bem, razoável. Mas o segundo me dá mais pausa. Eu pensei que um dos pontos de venda do LINQ é que ele simplesmente abstrai operações de nível inferior como tabelas de hash, mas que, sob o véu, ainda é a mesma implementação.

Questão

Além de alguns ciclos de processamento adicionais para chamar métodos abstraídos, o LINQ requer significativamente mais ciclos de processamento para realizar uma tarefa de iteração de dados do que uma tarefa de nível inferior (como construir uma tabela de hash) exigiria?

Matt Cashatt
fonte
2
Pergunte a ele que idiota colocou uma enciclopédia inteira em um único arquivo de texto?
Jeffo
4
É uma daquelas coisas que devem ser medidas. Crie 2 ou 3 implementações e registre o desempenho. Generalizações sobre LINQ ou técnica X não são úteis. Eu diria que é ruim do entrevistador declarar que o LINQ é uma resposta "errada". Embora em processamento pesado no lado do servidor, cada milissegundo conta.
Senhor Tydus
1
Um rápido google para "testes de desempenho de linq para objetos e loops" encontrou alguns hits. Alguns incluem código-fonte que você pode usar para testar por si mesmo. Veja isto , isto e isto .
Oded
1
Quanto às entrevistas, lembre-se de que alguns programadores C ++ da "velha escola" acham que você deve reinventar a roda em vez de usar as bibliotecas .NET. Você também encontrará os VB da velha escola que desejam fazer todo o código de acesso a dados manualmente, em vez de usar LINQ e EF.
precisa saber é o seguinte
1
Oded, esses exemplos nos links que você forneceu estão muito errados. Não posso entrar em todos os detalhes em um comentário, mas pegue o segundo link. Ele compara "foreach x if x = toFind stop" com uma consulta linq que faz o equivalente a "select * da lista onde x gosta de toFind" A diferença é que o primeiro para quando encontra a primeira instância, a consulta linq sempre repete cada entrada e retornará uma coleção de TODOS os itens correspondentes ao padrão de pesquisa. Muito diferente. Isso não é porque o LINQ está quebrado, é porque ele usou a consulta errada.
31412 Ian

Respostas:

9

Eu diria que a principal fraqueza dessa resposta é menos o uso do Linq e mais os operadores específicos escolhidos. GroupBypega cada elemento e o projeta em uma chave e um valor que entram em uma pesquisa. Em outras palavras, cada palavra adicionará algo à pesquisa.

A implementação ingênua .GroupBy(e => e)armazenará uma cópia de cada palavra no material de origem, tornando a pesquisa final quase tão grande quanto o material de origem original. Mesmo se projetarmos o valor .GroupBy(e => e, e => null), estamos criando uma grande pesquisa de pequenos valores.

O que queremos é um operador que preserve apenas as informações necessárias, que são uma cópia de cada palavra e a contagem dessa palavra até o momento. Para isso, podemos usar Aggregate:

words.Aggregate(new Dictionary<string, int>(), (counts, word) => 
{
    int currentCount;
    counts.TryGetValue(word, currentCount);
    counts[word] = currentCount + 1;
    return counts;
} 

A partir daqui, há várias maneiras pelas quais podemos tentar tornar isso mais rápido:

  1. Em vez de criar muitas strings durante a divisão, poderíamos passar por structs que fazem referência à string original e ao segmento que contém a palavra, e apenas copiar o segmento quando ele se tornar uma chave exclusiva
  2. Use o Linq paralelo para agregar vários núcleos e combinar os resultados . Isso é trivial comparado ao trabalho de perna necessário para fazer isso manualmente.
Chris Pitman
fonte
Tudo de bom Chris, obrigado. Vou me abster de aceitar um pouco, pois a pergunta é mais geral e essencialmente respondida por Oded nos comentários acima. Eu só quero dar a ele a oportunidade de fornecer a resposta primeiro. Mais uma vez obrigado pela sua compreensão, o que é ótimo.
precisa saber é o seguinte
6

Eu acho que você teve uma fuga por pouco, o entrevistador realmente não sabia do que estava falando. Pior ainda, ele acredita que há uma resposta "certa". Se ele era alguém em quem você gostaria de trabalhar, eu esperaria que ele tomasse sua resposta inicial, descubra por que você a escolheu e depois o desafie a melhorar se ele encontrar problemas com ela.

O LINQ assusta as pessoas porque parece mágica. Na verdade, é muito simples (tão simples que você precisaria ser um gênio para sugerir isso)

var result = from item in collection where item=>item.Property > 3 select item;

É compilado em:

IEnumerable<itemType> result = collection.Where(item=>item.property >3);

(Por favor, não grite se eu tenho a sintaxe errada, é depois da meia-noite e estou na cama :))

Onde está um método de extensão no IEnumerable que leva um lambda. O lambda é simplesmente (neste caso) compilado para um delegado:

bool AMethod(ItemType item)
{
    return item.property >3;
}

O método Where simplesmente adiciona TODAS as instâncias do item em que AMethod retorna true a uma coleção retornada.

Não há razão para ser mais lento do que fazer um foreach e adicionar todos os itens correspondentes a uma coleção nesse loop. De fato, o método de extensão Where provavelmente está fazendo exatamente isso. A verdadeira magia vem quando você precisa injetar uma alternativa em que critérios.

Como mencionei acima no meu comentário, alguns dos exemplos vinculados estão muito errados. E é esse tipo de desinformação que causa os problemas.

Finalmente, se a entrevista tivesse lhe dado uma chance, você poderia ter dito o seguinte:

  • O LINQ é fácil de ler, especialmente onde você começa a apresentar projeções e agrupamentos interessantes. O código de fácil leitura é fácil [y | ier] para manter o código que é uma vitória.

  • Seria realmente fácil medir o desempenho se fosse realmente um gargalo e substituí-lo por outra coisa.

Ian
fonte
No geral, concordo com você, mas o comportamento do método Where - Where não adiciona todos os itens correspondentes a uma coleção. Ele armazena as informações necessárias para filtrar itens na árvore de expressão. Se o iterador retornado não for realmente usado, nenhuma filtragem ocorrerá.
Codism
Excelente ponto, eu deveria ter mencionado isso. No exemplo deles, eles usam o iterador retornado. Essa foi a loucura do teste deles. Para extrair o valor encontrado (todos os itens em seus dados de teste eram únicos), eles tinham um foreach que iterava o enumerável resultante para exibir o resultado. Obviamente, houve apenas um resultado, portanto, apenas imprimiu a resposta. Loucura :)
Ian
Embora eu não tenha usado o LINQ, uma coisa que considero desagradável é que otimiza coisas como Countalguns cenários estreitos que funcionam mal com o encapsulamento. Concatene uma lista de um milhão de itens e um iterador de quatro itens e Countdeve exigir cerca de 5 operações, mas exigirá um milhão. Desejo que a MS defina IEnhancedEnumeratorcom um int Move(int)método que retorne 0 em caso de sucesso ou retorne a quantidade de déficit em caso de falha (o que seria feito Move(1000003)em um recém-criado a List<T>.Enumeratorpartir de uma lista de milhões de itens retornaria 3). Qualquer implementação ...
supercat 4/15
... IEnumerable<T>pode ser envolvido em uma implementação de IEnhancedEnumerator, mas tipos implementados IEnhancedEnumeratordiretamente podem permitir acelerações de ordem de magnitude em muitas operações, e até coisas como o retorno Appendpodem expor a capacidade de busca rápida dos constituintes.
Supercat 4/15