A imutabilidade prejudica o desempenho em JavaScript?


88

Parece haver uma tendência recente no JavaScript no sentido de tratar estruturas de dados como imutáveis. Por exemplo, se você precisar alterar uma única propriedade de um objeto, é melhor criar um objeto totalmente novo com a nova propriedade e copiar todas as outras propriedades do objeto antigo e permitir que o objeto antigo seja coletado como lixo. (Essa é a minha compreensão de qualquer maneira.)

Minha reação inicial é que parece que seria ruim para o desempenho.

Mas bibliotecas como Immutable.js e Redux.js são escritas por pessoas mais inteligentes que eu e parecem ter uma forte preocupação com o desempenho, por isso me faz pensar se meu entendimento sobre lixo (e seu impacto no desempenho) está errado.

Sinto falta de benefícios de desempenho para a imutabilidade, e eles superam as desvantagens de criar tanto lixo?


8
Eles têm uma forte preocupação com o desempenho, em parte porque a imutabilidade (às vezes) tem um custo de desempenho e eles querem minimizar esse custo de desempenho o máximo possível. A imutabilidade, por si só, possui apenas benefícios de desempenho no sentido em que facilita a gravação de código multithread.
Robert Harvey

8
Na minha experiência, o desempenho é apenas uma preocupação válida para dois cenários - um, quando uma ação é executada mais de 30 vezes em um segundo, e dois - quando seus efeitos aumentam a cada execução (o Windows XP encontrou um bug no tempo do Windows Update O(pow(n, 2))para todas as atualizações em seu histórico .) A maioria dos outros códigos é uma resposta imediata a um evento; um clique, solicitação de API ou similar e, enquanto o tempo de execução for constante, a limpeza de qualquer número de objetos dificilmente será importante.
precisa saber é o seguinte

4
Além disso, considere que existem implementações eficientes de estruturas de dados imutáveis. Talvez estes não sejam tão eficientes quanto os mutáveis, mas provavelmente ainda mais eficientes do que uma implementação ingênua. Veja, por exemplo, Estruturas de Dados Puramente Funcionais, por Chris Okasaki
Giorgio

1
@ Katana314: mais de 30 vezes para mim ainda não seriam suficientes para justificar a preocupação com o desempenho. Portei um pequeno emulador de CPU que escrevi para o node.js e o nó executou a CPU virtual em torno de 20 MHz (20 milhões de vezes por segundo). Portanto, eu só me preocuparia com o desempenho se estivesse fazendo algo mais de 1000 vezes por segundo (mesmo assim, eu realmente não me preocuparia até fazer 1000000 operações por segundo porque sei que posso fazer confortavelmente mais de 10 delas ao mesmo tempo) .
slebetman

2
@RobertHarvey "A imutabilidade, por si só, possui apenas benefícios de desempenho no sentido de facilitar a gravação de códigos multithread." Isso não é inteiramente verdade, a imutabilidade permite um compartilhamento muito difundido, sem consequências reais. O que é muito inseguro em um ambiente mutável. Isso dá a você a ideia de O(1)fatiar e O(log n)inserir uma matriz em uma árvore binária enquanto ainda é capaz de usar a antiga livremente, e outro exemplo é o de tailsque todas as caudas de uma lista tails [1, 2] = [[1, 2], [2], []]levam apenas O(n)tempo e espaço, mas estão O(n^2)na contagem de elementos
ponto

Respostas:


59

Por exemplo, se você precisar alterar uma única propriedade de um objeto, é melhor criar um objeto totalmente novo com a nova propriedade e copiar todas as outras propriedades do objeto antigo e permitir que o objeto antigo seja coletado como lixo.

Sem imutabilidade, talvez você precise passar um objeto entre diferentes escopos e não sabe de antemão se e quando o objeto será alterado. Portanto, para evitar efeitos colaterais indesejados, você começa a criar uma cópia completa do objeto "apenas por precaução" e a distribui, mesmo que ache que nenhuma propriedade precise ser alterada. Isso deixará muito mais lixo do que no seu caso.

O que isso demonstra é: se você criar o cenário hipotético certo, poderá provar qualquer coisa, especialmente quando se trata de desempenho. Meu exemplo, no entanto, não é tão hipotético quanto possa parecer. Trabalhei no mês passado em um programa em que nos deparamos exatamente com esse problema, porque inicialmente decidimos não usar uma estrutura de dados imutável e hesitamos em refatorá-lo mais tarde, porque não parecia valer a pena.

Portanto, quando você analisa casos como este em uma publicação antiga do SO , a resposta para suas perguntas provavelmente fica clara - depende . Em alguns casos, a imutabilidade prejudicará o desempenho; para outros, o oposto pode ser verdadeiro; em muitos casos, dependerá do grau de inteligência da sua implementação e, em outros casos, a diferença será insignificante.

Uma observação final: um problema do mundo real que você pode encontrar é que precisa decidir cedo a favor ou contra a imutabilidade para algumas estruturas básicas de dados. Então você cria muito código com base nisso e, várias semanas ou meses depois, verá se a decisão foi boa ou ruim.

Minha regra pessoal para esta situação é:

  • Se você projetar uma estrutura de dados com apenas alguns atributos com base em tipos primitivos ou outros tipos imutáveis, tente primeiro a imutabilidade.
  • Se você deseja criar um tipo de dados em que matrizes com tamanho grande (ou indefinido), acesso aleatório e alteração de conteúdo estejam envolvidas, use a mutabilidade.

Para situações entre esses dois extremos, use seu julgamento. Mas YMMV.


8
That will leave a lot more garbage than in your case.e para piorar a situação, seu tempo de execução provavelmente não será capaz de detectar a duplicação inútil e, portanto, (diferentemente de um objeto imutável expirado que ninguém está usando), ele nem será elegível para coleta.
Jacob Raihle

37

Primeiro de tudo, sua caracterização de estruturas de dados imutáveis ​​é imprecisa. Em geral, a maior parte de uma estrutura de dados não é copiada, mas compartilhada , e apenas as partes alteradas são copiadas. Isso é chamado de estrutura de dados persistente . A maioria das implementações é capaz de tirar proveito das estruturas de dados persistentes na maioria das vezes. O desempenho é próximo o suficiente de estruturas de dados mutáveis ​​que os programadores funcionais geralmente consideram insignificantes.

Segundo, acho que muitas pessoas têm uma idéia bastante imprecisa do tempo de vida típico dos objetos em programas imperativos típicos. Talvez isso se deva à popularidade das linguagens gerenciadas por memória. Sente-se em algum momento e observe realmente quantos objetos temporários e cópias defensivas você cria em comparação com estruturas de dados de longa duração. Eu acho que você ficará surpreso com a proporção.

Já tive pessoas comentando nas aulas de programação funcional que ensino sobre a quantidade de lixo que um algoritmo cria, depois mostro a versão imperativa típica do mesmo algoritmo que cria o mesmo. Por algum motivo, as pessoas não percebem mais.

Ao incentivar o compartilhamento e desencorajar a criação de variáveis ​​até que você tenha um valor válido para colocar nelas, a imutabilidade tende a incentivar práticas de codificação mais limpas e estruturas de dados com vida útil mais longa. Isso geralmente leva a níveis comparáveis, se não mais baixos, de lixo, dependendo do seu algoritmo.


8
"... então mostro a versão imperativa típica do mesmo algoritmo que cria o mesmo." Este. Além disso, as pessoas que são novas neste estilo, e especialmente se são novas no estilo funcional em geral, podem inicialmente produzir implementações funcionais subótimas.
Wd

1
"desencorajar a criação de variáveis" Isso não é válido apenas para idiomas em que o comportamento padrão é copiar na atribuição / construção implícita? Em JavaScript, uma variável é apenas um identificador; não é um objeto por si só. Ele ainda ocupa espaço em algum lugar, mas isso é insignificante (principalmente porque a maioria das implementações de JavaScript, ainda assim, usa uma pilha para chamadas de função, ou seja, a menos que você tenha muita recursão, acabará reutilizando o mesmo espaço de pilha para a maioria variáveis ​​temporárias). A imutabilidade não tem relação com esse aspecto.
quer

33

Cheguei atrasado a essas perguntas e respostas com ótimas respostas, mas eu queria me intrometer como estrangeiro acostumado a ver as coisas do ponto de vista de baixo nível de bits e bytes na memória.

Estou muito empolgado com designs imutáveis, mesmo vindo de uma perspectiva C e da perspectiva de encontrar novas maneiras de programar efetivamente esse hardware bestial que temos atualmente.

Mais lento / mais rápido

Quanto à questão de tornar as coisas mais lentas, seria uma resposta robótica yes. Nesse tipo de nível conceitual muito técnico, a imutabilidade só pode tornar as coisas mais lentas. O hardware funciona melhor quando não está alocando esporadicamente a memória e pode apenas modificar a memória existente (por que temos conceitos como localidade temporal).

No entanto, uma resposta prática é maybe. O desempenho ainda é em grande parte uma métrica de produtividade em qualquer base de código não trivial. Normalmente, não consideramos as bases de código de manutenção horrível que tropeçam nas condições de corrida como as mais eficientes, mesmo se desconsiderarmos os erros. A eficiência geralmente é uma função da elegância e simplicidade. O pico das micro-otimizações pode entrar em conflito, mas geralmente é reservado para as seções menores e mais críticas do código.

Transformando bits e bytes imutáveis

Vindo do ponto de vista de baixo nível, se fizermos radiografar conceitos como objectse stringsassim por diante, no centro estão apenas bits e bytes em várias formas de memória com diferentes características de velocidade / tamanho (velocidade e tamanho do hardware da memória sendo tipicamente Mutualmente exclusivo).

insira a descrição da imagem aqui

A hierarquia de memória do computador gosta quando acessamos repetidamente o mesmo pedaço de memória, como no diagrama acima, pois ele mantém esse pedaço de memória acessado com frequência na forma mais rápida de memória (cache L1, por exemplo, que é quase tão rápido quanto um registro). Podemos acessar repetidamente exatamente a mesma memória (reutilizando-a várias vezes) ou acessar repetidamente diferentes seções do pedaço (por exemplo, percorrendo os elementos em um pedaço contíguo que acessa várias seções desse pedaço de memória).

Acabamos jogando uma chave de boca nesse processo, se modificar essa memória quiser criar um novo bloco de memória ao lado, da seguinte maneira:

insira a descrição da imagem aqui

... nesse caso, acessar o novo bloco de memória pode exigir falhas de página obrigatórias e falhas de cache para movê-lo de volta às formas mais rápidas de memória (até o registro). Isso pode ser um verdadeiro matador de desempenho.

Existem maneiras de atenuar isso, no entanto, usando um pool de reserva de memória pré-alocada, já tocada.

Agregados grandes

Outra questão conceitual que surge de uma visão de nível um pouco mais alto é simplesmente fazer cópias desnecessárias de agregados realmente grandes a granel.

Para evitar um diagrama excessivamente complexo, vamos imaginar que esse bloco de memória simples seja caro (talvez caracteres UTF-32 em um hardware incrivelmente limitado).

insira a descrição da imagem aqui

Nesse caso, se quiséssemos substituir "HELP" por "KILL" e esse bloco de memória fosse imutável, teríamos que criar um bloco totalmente novo para criar um novo objeto único, mesmo que apenas partes dele tenham mudado :

insira a descrição da imagem aqui

Ampliando um pouco a nossa imaginação, esse tipo de cópia profunda de todo o resto apenas para tornar uma pequena parte única pode ser bastante caro (nos casos do mundo real, esse bloco de memória seria muito, muito maior para representar um problema).

No entanto, apesar de tal despesa, esse tipo de design tende a ser muito menos propenso a erros humanos. Qualquer pessoa que tenha trabalhado em uma linguagem funcional com funções puras provavelmente pode apreciar isso, e especialmente em casos multithread, em que podemos multithread esse código sem se preocupar com o mundo. Em geral, programadores humanos tendem a tropeçar em mudanças de estado, especialmente aquelas que causam efeitos colaterais externos a estados fora do escopo de uma função atual. Mesmo a recuperação de um erro externo (exceção) nesse caso pode ser incrivelmente difícil com alterações de estado externas mutáveis ​​no mix.

Uma maneira de mitigar esse trabalho redundante de cópia é transformar esses blocos de memória em uma coleção de ponteiros (ou referências) para caracteres, da seguinte maneira:

Desculpas, não percebi que não precisamos ser Lúnicos ao fazer o diagrama.

Azul indica dados copiados rasos.

insira a descrição da imagem aqui

... infelizmente, isso seria incrivelmente caro pagar um custo de referência / ponteiro por personagem. Além disso, podemos espalhar o conteúdo dos caracteres por todo o espaço de endereço e acabar pagando por ele na forma de um monte de falhas de página e falhas de cache, tornando esta solução ainda pior do que copiar a coisa toda na sua totalidade.

Mesmo se tivéssemos o cuidado de alocar esses caracteres de forma contígua, digamos que a máquina possa carregar 8 caracteres e 8 ponteiros para um caractere em uma linha de cache. Acabamos carregando uma memória assim para percorrer a nova string:

insira a descrição da imagem aqui

Nesse caso, acabamos exigindo que 7 linhas de cache diferentes, com memória contígua, sejam carregadas para atravessar essa string, quando idealmente precisamos apenas de 3.

Repartir os dados

Para atenuar o problema acima, podemos aplicar a mesma estratégia básica, mas com um nível mais grosso de 8 caracteres, por exemplo

insira a descrição da imagem aqui

O resultado requer que 4 linhas de cache com valor de dados (1 para os 3 ponteiros e 3 para os caracteres) sejam carregadas para percorrer essa sequência, que é apenas 1 a menos do ideal teórico.

Então isso não é ruim. Há algum desperdício de memória, mas a memória é abundante e o uso de mais não diminui a velocidade se a memória extra for apenas para dados frios que não são acessados ​​com frequência. É apenas para dados contíguos e quentes, onde o uso e a velocidade reduzidos da memória geralmente andam de mãos dadas, onde queremos colocar mais memória em uma única página ou linha de cache e acessar tudo isso antes da remoção. Essa representação é bastante amigável ao cache.

Rapidez

Portanto, utilizar uma representação como a acima pode fornecer um equilíbrio decente de desempenho. Provavelmente, os usos mais críticos para o desempenho de estruturas de dados imutáveis ​​assumem essa natureza de modificação de pedaços de dados em pedaços e os tornam únicos no processo, enquanto copia superficialmente partes não modificadas. Também implica em algumas despesas gerais das operações atômicas para fazer referência às partes copiadas rasas com segurança em um contexto multithread (possivelmente com alguma contagem de referência atômica acontecendo).

No entanto, desde que esses dados robustos sejam representados em um nível suficientemente grosseiro, grande parte dessa sobrecarga diminui e é possivelmente trivializada, enquanto ainda nos oferece a segurança e a facilidade de codificar e multithreading mais funções de uma forma pura, sem lado externo efeitos

Mantendo dados novos e antigos

Onde eu vejo a imutabilidade como potencialmente mais útil do ponto de vista do desempenho (em um sentido prático) é quando podemos ser tentados a fazer cópias inteiras de dados grandes, a fim de torná-los únicos em um contexto mutável, onde o objetivo é produzir algo novo. algo que já existe de uma maneira em que queremos manter novos e antigos, quando poderíamos apenas tornar pequenos pedaços dele únicos com um design imutável cuidadoso.

Exemplo: Desfazer Sistema

Um exemplo disso é um sistema de desfazer. Podemos alterar uma pequena parte de uma estrutura de dados e queremos manter o formulário original para o qual podemos desfazer e o novo formulário. Com esse tipo de design imutável que apenas torna pequenas seções modificadas da estrutura de dados, podemos simplesmente armazenar uma cópia dos dados antigos em uma entrada de desfazer, pagando apenas o custo da memória dos dados de porções exclusivas adicionados. Isso fornece um equilíbrio muito eficaz de produtividade (tornando a implementação de um sistema de desfazer um pedaço de bolo) e desempenho.

Interfaces de alto nível

No entanto, algo estranho surge com o caso acima. Em um tipo local de contexto de função, dados mutáveis ​​costumam ser os mais fáceis e diretos de modificar. Afinal, a maneira mais fácil de modificar uma matriz é frequentemente percorrê-la e modificar um elemento de cada vez. Podemos acabar aumentando a sobrecarga intelectual se tivéssemos um grande número de algoritmos de alto nível para escolher para transformar uma matriz e tivéssemos que escolher o apropriado para garantir que todas essas cópias superficiais e grossas sejam feitas enquanto as partes modificadas são feito único.

Provavelmente, a maneira mais fácil nesses casos é usar buffers mutáveis ​​localmente dentro do contexto de uma função (onde eles normalmente não nos enganam) que confirmam alterações atomicamente na estrutura de dados para obter uma nova cópia imutável (acredito que alguns idiomas chamam esses "transitórios") ...

... ou podemos simplesmente modelar funções de transformação de nível superior e superior sobre os dados para ocultar o processo de modificação de um buffer mutável e comprometê-lo com a estrutura sem a lógica mutável envolvida. De qualquer forma, esse ainda não é um território amplamente explorado, e temos nosso trabalho cortado se abraçarmos projetos imutáveis ​​mais para criar interfaces significativas de como transformar essas estruturas de dados.

Estruturas de dados

Outra coisa que surge aqui é que a imutabilidade usada em um contexto crítico de desempenho provavelmente desejará que as estruturas de dados se dividam em dados em pedaços onde os pedaços não são muito pequenos em tamanho, mas também não são muito grandes.

As listas vinculadas podem querer mudar um pouco para acomodar isso e se transformar em listas não roladas. Matrizes grandes e contíguas podem se transformar em uma matriz de ponteiros em blocos contíguos com indexação de módulo para acesso aleatório.

Ele potencialmente muda a maneira como olhamos para as estruturas de dados de uma maneira interessante, enquanto pressiona as funções de modificação dessas estruturas de dados para se parecer com uma natureza mais volumosa e ocultar a complexidade extra na cópia superficial de alguns bits aqui e na criação de outros bits únicos lá.

atuação

Enfim, esta é minha pequena visão de nível inferior sobre o tópico. Teoricamente, a imutabilidade pode ter um custo que varia de muito grande a menor. Mas uma abordagem muito teórica nem sempre faz com que os aplicativos sejam rápidos. Pode torná-los escaláveis, mas a velocidade do mundo real geralmente exige a adoção de uma mentalidade mais prática.

Do ponto de vista prático, qualidades como desempenho, manutenção e segurança tendem a se transformar em um grande borrão, especialmente para uma base de código muito grande. Embora o desempenho, em certo sentido absoluto, seja degradado pela imutabilidade, é difícil argumentar sobre os benefícios que ele tem sobre a produtividade e a segurança (incluindo a segurança da rosca). Com esse aumento, muitas vezes pode haver um aumento no desempenho prático, mesmo que os desenvolvedores tenham mais tempo para ajustar e otimizar seu código sem serem invadidos por bugs.

Então, acho que, desse sentido prático, estruturas de dados imutáveis ​​podem realmente ajudar o desempenho em muitos casos, por mais estranho que pareça. Um mundo ideal pode procurar uma mistura desses dois: estruturas de dados imutáveis ​​e mutáveis, com as mutáveis ​​normalmente sendo muito seguras para uso em um escopo muito local (ex: local para uma função), enquanto as imutáveis ​​podem evitar o lado externo efetua completamente e transforma todas as alterações em uma estrutura de dados em uma operação atômica, produzindo uma nova versão sem risco de condições de corrida.


11

O ImmutableJS é realmente bastante eficiente. Se dermos um exemplo:

var x = {
    Foo: 1,
    Bar: { Baz: 2 }
    Qux: { AnotherVal: 3 }
}

Se o objeto acima se tornar imutável, você modificará o valor da propriedade 'Baz', o que obteria é:

var y = x.setIn('/Bar/Baz', 3);
y !== x; // Different object instance
y.Bar !== x.Bar // As the Baz property was changed, the Bar object is a diff instance
y.Qux === y.Qux // Qux is the same object instance

Isso cria algumas melhorias de desempenho muito interessantes para modelos de objetos profundos, nos quais você só precisa copiar tipos de valor em objetos no caminho para a raiz. Quanto maior o modelo de objeto e menores as alterações, melhor o desempenho da memória e da CPU da estrutura de dados imutáveis, pois eles acabam compartilhando muitos objetos.

Como as outras respostas disseram, se você comparar isso com a tentativa de fornecer as mesmas garantias, copiando defensivamente xantes de passá-lo para uma função que possa manipulá-lo, o desempenho será significativamente melhor.


4

Em uma linha reta, o código imutável possui a sobrecarga da criação de objetos, que é mais lenta. No entanto, existem muitas situações em que o código mutável se torna muito difícil de gerenciar com eficiência (resultando em muitas cópias defensivas, o que também é caro), e há muitas estratégias inteligentes para reduzir o custo de 'copiar' um objeto , como mencionado por outros.

Se você tem um objeto como um contador e ele é incrementado várias vezes por segundo, fazer com que esse contador seja imutável pode não valer a pena de desempenho. Se você tiver um objeto que está sendo lido por muitas partes diferentes do seu aplicativo e cada um deles quiser ter seu próprio clone ligeiramente diferente do objeto, será mais fácil orquestrá-lo de maneira eficiente usando uma boa implementação de objetos imutáveis.


4

Para adicionar a esta pergunta (já com excelente resposta):

A resposta curta é sim ; isso prejudicará o desempenho porque você só cria objetos em vez de alterar os existentes, resultando em mais sobrecarga na criação de objetos.


No entanto, a resposta longa não é realmente .

Do ponto de vista real do tempo de execução, em JavaScript, você já cria muitos objetos de tempo de execução - funções e literais de objetos estão em toda parte no JavaScript e ninguém parece pensar duas vezes em usá-los. Eu argumentaria que a criação de objetos é realmente muito barata, embora eu não tenha citações para isso, então não a usaria como argumento independente.

Para mim, o maior aumento de "desempenho" não está no desempenho em tempo de execução, mas no desempenho do desenvolvedor . Uma das primeiras coisas que aprendi enquanto trabalhava em aplicativos do Mundo Real (TM) é que a mutabilidade é realmente perigosa e confusa. Perdi muitas horas perseguindo um encadeamento (não o tipo de simultaneidade) de execução tentando descobrir o que está causando um bug obscuro quando se trata de uma mutação do outro lado do maldito aplicativo!

O uso da imutabilidade facilita muito o raciocínio. Você pode saber imediatamente que o objeto X não será alterado durante sua vida útil e a única maneira de mudar é cloná-lo. Eu valorizo ​​isso muito mais (especialmente em ambientes de equipe) do que quaisquer micro-otimizações que a mutabilidade possa trazer.

Existem exceções, principalmente estruturas de dados, conforme observado acima. Raramente me deparei com um cenário em que desejava alterar um mapa após a criação (embora eu esteja falando de mapas pseudo-objeto-literais em vez de mapas ES6), o mesmo para matrizes. Quando você lida com estruturas de dados maiores, a mutabilidade pode valer a pena. Lembre-se de que todo objeto JavaScript é passado como referência e não como valor.


Dito isto, um ponto levantado acima foi o GC e sua incapacidade de detectar duplicações. Essa é uma preocupação legítima, mas, na minha opinião, é apenas uma preocupação quando a memória é uma preocupação, e há maneiras muito mais fáceis de se codificar em um canto - por exemplo, referências circulares nos fechamentos.


Por fim, eu preferiria ter uma base de código imutável com muito poucas (se houver) seções mutáveis ​​e ter um desempenho ligeiramente menor do que a mutabilidade em todos os lugares. Você sempre pode otimizar mais tarde se a imutabilidade, por algum motivo, se tornar uma preocupação pelo desempenho.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.