Memoização esquecer-me-bomba


Você já ouviu falar sobre memoization ? A propósito, é uma coisa super simples - apenas memorize qual resultado você obteve de uma primeira chamada de função e use-o em vez de chamá-lo pela segunda vez - não ligue para coisas reais sem razão, não perca seu tempo .


Ignorar algumas operações intensivas é uma técnica de otimização muito comum. Toda vez que você pode não fazer algo - não faça. Tente usar o cache - memcache , file cache , local cache - qualquer cache! Um item essencial para os sistemas de back-end e uma parte crucial de qualquer sistema de back-end do passado e do presente.



Memoization vs Caching


Memoização é como cache. Um pouco diferente. Não cache, vamos chamá-lo de kashe.

Para encurtar a história, mas a memorização não é um cache, não é um cache persistente. Pode ser no lado do servidor, mas não pode e não deve ser um cache no lado do cliente. É mais sobre recursos disponíveis, padrões de uso e os motivos para usar.


Problema - O cache precisa de uma "chave de cache"


O cache está armazenando e buscando dados usando uma key cache de sequência . Já é um problema construir uma chave única e utilizável, mas é necessário serializar e desserializar dados para armazenar novamente o meio baseado em string ... em suma - o cache pode não ser tão rápido quanto você imagina. Cache especialmente distribuído.


A memorização não precisa de nenhuma chave de cache


Ao mesmo tempo - nenhuma chave é necessária para memorização. Geralmente * ele usa argumentos como estão, não tentando criar uma única chave a partir deles, e não usa algum objeto compartilhado disponível globalmente para armazenar resultados, como o cache geralmente faz.


A diferença entre memorização e cache está na API INTERFACE !

Geralmente * não significa sempre. Lodash.memoize , por padrão, usa JSON.stringify para converter argumentos passados ​​em um cache de strings (existe alguma outra maneira? Não!). Só porque eles vão usar essa chave para acessar um objeto interno, mantendo um valor em cache. O memoize rápido , "a biblioteca de memoização mais rápida possível", faz o mesmo. Ambas as bibliotecas nomeadas não são bibliotecas de memorização, mas bibliotecas de cache.


Vale ressaltar - JSON.stringify pode ser 10 vezes mais lento que uma função, você vai memorizar.

Obviamente - a solução simples para o problema NÃO é usar uma chave de cache e NÃO acessar algum cache interno usando essa chave. Então - lembre-se dos últimos argumentos com os quais você foi chamado. Como memoizerific ou re - select do.


Memoizerific é provavelmente a única biblioteca de cache geral que você gostaria de usar.

O tamanho do cache


A segunda grande diferença entre todas as bibliotecas é sobre o tamanho do cache e a estrutura do cache.


Você já pensou - por que reselect ou memoize-one detém apenas um, último resultado? Não para "não usar a chave de cache para poder armazenar mais de um resultado" , mas porque não há motivos para armazenar mais do que apenas um último resultado .


... É mais sobre:


  • recursos disponíveis - uma única linha de cache é muito amiga dos recursos
  • padrões de uso - lembrar de algo "no lugar" é um bom padrão. Normalmente, você precisa apenas de um último resultado.
  • a razão para usar -modularidade, isolamento e segurança da memória são boas razões. Não compartilhar cache com o restante do seu aplicativo é apenas mais seguro em termos de colisões de cache.

Um único resultado ?!


Sim - o único resultado. Com um resultado memorizado, algumas coisas clássicas , como a geração de números de fibonacci memorizados ( você pode encontrar como exemplo em todo artigo sobre memorização ), não seria possível . Mas, geralmente, você está fazendo outra coisa - quem precisa de um fibonacci no Frontend? No back-end? Um exemplo do mundo real está longe de ser questionário abstrato de TI .


Mas, ainda assim, existem dois GRANDES problemas sobre um tipo de memorização de valor único.


Problema 1 - é "frágil"


Por padrão - todos os argumentos devem corresponder, exatamente o mesmo "===". Se um argumento não corresponder - o jogo acabou. Mesmo que isso provenha da idéia de memorização - isso pode não ser algo que você deseja atualmente. Quero dizer - você quer memorizar o máximo possível e com a maior frequência possível.


Mesmo falta de cache é um tiro na cabeça que limpa o cache.

Há uma pequena diferença entre "hoje em dia" e "ontem" - estruturas de dados imutáveis, usadas, por exemplo, no Redux.


 const getSomeDataFromState = memoize(state => compute(state.tasks)); 

Parece bom? Parecendo certo? No entanto, o estado pode mudar quando as tarefas não mudam, e você precisa apenas de tarefas para corresponder.


Os seletores estruturais estão aqui para salvar o dia com o seu guerreiro mais forte - a seleção de novo - à sua disposição. A nova seleção não é apenas uma biblioteca de memoização, mas seu poder vem de cascatas de memoização ou lentes (que não são, mas pensam nos seletores como lentes ópticas).


 // every time `state` changes, cached value would be rejected const getTasksFromState = createSelector(state => state.tasks); const getSomeDataFromState = createSelector( // `tasks` "without" `state` getTasksFromState, // <---------- // and this operation would be memoized "more often" tasks => compute(state.tasks) ); 

Como resultado, no caso de dados imutáveis ​​- você sempre precisa "focar" primeiro no pedaço de dados que realmente precisa e, em seguida - executar cálculos; caso contrário, o cache será rejeitado e toda a idéia por trás da memorização desaparecerá.


Este é realmente um grande problema, especialmente para os recém-chegados, mas, como A Idéia por trás de estruturas de dados imutáveis, tem um benefício significativo - se algo não for alterado - ele não será alterado. Se algo for alterado - provavelmente será alterado . Isso nos dá uma comparação super rápida, mas com alguns falsos negativos, como no primeiro exemplo.


A idéia é "focar" nos dados dos quais você depende

Há dois momentos que eu deveria ter mencionado:


  • lodash.memoize e fast-memoize estão convertendo seus dados em uma string para ser usada como chave. Isso significa que eles são 1) não rápidos 2) não seguros 3) podem produzir falsos positivos - alguns dados diferentes podem ter a mesma representação de string . Isso pode melhorar a "taxa de cache quente", mas na verdade é uma coisa MUITO MAU.
  • existe uma abordagem de proxy ES6, sobre como rastrear todas as partes usadas de variáveis ​​fornecidas e verificar apenas as chaves importantes. Embora eu pessoalmente queira criar uma infinidade de seletores de dados - talvez você não goste ou entenda o processo, mas queira ter uma memoização adequada pronta para uso -, use o estado de memorização .

Problema 2 - é "uma linha de cache"


O tamanho infinito do cache é um assassino. Qualquer cache não controlado é um assassino, desde que a memória seja bastante finita. Então - todas as melhores bibliotecas são "um cache de linha de comprimento". Essa é uma característica e uma forte decisão de design. Acabei de escrever como está correto e, acredite, é uma coisa realmente certa , mas ainda é um problema. Um grande problema.


 const tasks = getTasks(state); // let's get some data from state1 (function was defined above) getDataFromTask(tasks[0]); // Yep! equal(getDataFromTask(tasks[0]), getDataFromTask(tasks[0])) // Ok! getDataFromTask(tasks[1]); // a different task? What the heck? // oh! That's another argument? How dare you!? // TLDR -> task[0] in the cache got replaced by task[1] you cannot use getDataFromTask to get data from different tasks 

Uma vez que o mesmo seletor precise trabalhar com dados de origem diferentes, com mais de um - tudo está quebrado. E é fácil encontrar o problema:


  • Desde que usássemos seletores para obter tarefas de um estado - poderíamos usar os mesmos seletores para obter algo de uma tarefa. Intenso vem da própria API. Mas como não funciona, você pode memorizar apenas a última chamada, mas precisa trabalhar com várias fontes de dados.
  • O mesmo problema ocorre com vários componentes do React - todos iguais, e um pouco diferentes, buscando tarefas diferentes, limpando os resultados uns dos outros.

Existem 3 soluções possíveis:


  • no caso de redux - use a fábrica mapStateToProps. Isso criaria memorização por instância.
     const mapStateToProps = () => { const selector = createSelector(...); // ^ you have to define per-instance selectors here // usually that's not possible :) return state => ({ data: selector(data), // a usual mapStateToProps }); } 
  • a segunda variante é quase a mesma (e também para redux) - trata-se de usar a re-seleção . É uma biblioteca complexa, que pode salvar o dia distinguindo componentes. Ele apenas entendeu que a nova chamada foi feita para "outro" componente e pode manter o cache do "anterior".


Essa biblioteca ajudaria você a "manter" o cache de memorização, mas não a excluí-lo. Especialmente porque está implementando 5 (CINCO!) Estratégias de cache diferentes para atender a qualquer caso. Isso é um cheiro ruim. E se você escolher a pessoa errada?
Todos os dados que você memorizou - você precisa esquecê-los, mais cedo ou mais tarde. O ponto é não lembrar a última chamada de função - o ponto é ESQUECER no momento certo. Não muito cedo, e estragar a memorização, e não muito tarde.


Entendeu a ideia? Agora esqueça! E onde está a terceira variante?

Vamos fazer uma pausa


Parar Relaxe Respire fundo. E responda a uma pergunta simples: qual é o objetivo? O que temos que fazer para alcançar a meta? O que salvaria o dia?


DICA: Onde está esse f *** "cache" LOCALIZADO!


Onde está esse "cache" LOCALIZADO? Sim - essa é a pergunta certa. Obrigado por perguntar. E a resposta é simples - está localizada em um fechamento. Em um local escondido dentro * de uma função memorizada. Por exemplo - aqui está o código memoize-one :


 function(fn) { let lastArgs; // the last arguments let lastResult;// the last result <--- THIS IS THE CACHE // the memoized function const memoizedCall = function(...newArgs) { if (isEqual(newArgs, lastArgs)) { return lastResult; } lastResult = resultFn.apply(this, newArgs); lastArgs = newArgs; return lastResult; }; return memoizedCall; } 

Você receberá um memoizedCall e ele manterá o último resultado próximo, dentro de seu fechamento local, não acessível a ninguém, exceto memoizedCall. Um lugar seguro. "this" é um lugar seguro.


Reselect faz o mesmo e a única maneira de criar um "fork", com outro cache - crie um novo fechamento de memorização.


Mas a (outra) questão principal - quando (cache) seria "desaparecido"?


TLDR: "ficaria" com uma função, quando a instância da função fosse consumida pelo Garbage Collector.

Instância? Instância! Então - o que acontece com a memorização por instância? Há um artigo completo sobre isso na documentação do React


Em resumo - se você estiver usando Componentes de reação com base em classe, poderá:


 import memoize from "memoize-one"; class Example extends Component { filter = memoize( // <-- bound to the instance (list, filterText) => list.filter(...); // ^ that is "per instance" memoization // we are creating "own" memoization function // with the "own" lastResult render() { // Calculate the latest filtered list. // If these arguments haven't changed since the last render, // `memoize-one` will reuse the last return value. const filteredList = this.filter(something, somehow); return <ul>{filteredList.map(item => ...}</ul> } } 

Então - onde "lastResult" é armazenado? Dentro de um escopo local de filtro memorizado, dentro desta instância de classe. E quando seria "ido"?


Desta vez, "desapareceria" com uma instância de classe. Uma vez que o componente foi desmontado - ele ficou sem deixar rastro. É um "por instância" real, e você pode usar this.lastResult para manter um resultado temporal, com exatamente o mesmo efeito de "memorização".


O que é o React.Hooks


Estamos nos aproximando. Os ganchos Redux têm alguns comandos suspeitos, que provavelmente são sobre memorização. Like - useMemo , useCallback , useRef



Mas a pergunta - ONDE está armazenando um valor memorizado desta vez?

Em resumo - ele o armazena em "ganchos", dentro de uma parte especial de um elemento VDOM conhecido como fibra associada a um elemento atual. Dentro de uma estrutura de dados paralela.


Os ganchos não tão curtos estão mudando a maneira como o programa funciona, movendo sua função para outra, com algumas variáveis ​​em um local oculto dentro do fechamento dos pais . Tais funções são conhecidas como funções suspensas ou retomadas - corotinas. Em JavaScript, eles são geralmente conhecidos como generators ou async functions .


Mas isso é um pouco extremo. Em um uso muito curto, o Memo está armazenando valor memorizado nisso. É apenas um pouco diferente "isso".


Se queremos criar uma melhor biblioteca de memorização, devemos encontrar um "isto" melhor.

Zing!


WeakMaps!


Sim WeakMaps! Para armazenar o valor-chave, onde a chave seria essa, desde que o WeakMap não aceite nada além disso, ou seja, "objetos".


Vamos criar um exemplo simples:


 const createHiddenSpot = (fn) => { const map = new WeakMap(); // a hidden "closure" const set = (key, value) => (map.set(key, value), value); return (key) => { return map.get(key) || set(key, fn(key)) } } const weakSelect = createHiddenSpot(selector); weakSelect(todos); // create a new entry weakSelect(todos); // return an existing entry weakSelect(todos[0]); // create a new entry weakSelect(todos[1]); // create a new entry weakSelect(todos[0]); // return an existing entry! weakSelect(todos[1]); // return an existing entry!! weakSelect(todos); // return an existing entry!!! 

É estupidamente simples e bastante "certo". Então "quando isso se foi"?


  • esqueça fraco Selecione e todo um "mapa" desapareceria
  • esqueça todos [0] e sua fraca entrada desapareceria
  • esqueça todos - e dados memorizados desapareceriam!

Está claro quando algo "foi" - somente quando deveria!

Magicamente - todos os problemas de nova seleção desapareceram. Problemas com memoização agressiva - também um caso perdido.


Essa abordagem LEMBRE - SE dos dados até a hora de ESQUECER . É inacreditável, mas para se lembrar melhor de algo, é necessário esquecê-lo melhor.


A única coisa que dura: crie uma API mais robusta para este caso


Kashe - é um cache


kashe é uma biblioteca de memorização baseada no WeakMap, que pode salvar seu dia.


Esta biblioteca expõe 4 funções


  • kashe -para memorização.
  • box - para memorização prefixada, para aumentar a chance de memorização.
  • inbox - memorização prefixada aninhada, para diminuir a alteração da memorização
  • fork - to fork (obviamente).

kashe (fn) => memoizedFn (... args)


Na verdade, é um createHiddenSpot de um exemplo anterior. Ele usará um primeiro argumento como chave para um WeakMap interno.


 const selector = (state, prop) => ({result: state[prop]}); const memoized = kashe(selector); const old = memoized(state, 'x') memoized(state, 'x') === old memoized(state, 'y') === memoized(state, 'y') // ^^ another argument // but old !== memoized(state, 'x') // 'y' wiped 'x' cache in `state` 

O primeiro argumento é uma chave, se você chamar a função novamente da mesma chave, mas diferentes argumentos - o cache seria substituído, ainda será uma memoização de uma linha de cache. Para fazê-lo funcionar - é necessário fornecer chaves diferentes para casos diferentes, como fiz com um exemplo fraco de Select, para fornecer isso diferente para manter resultados. Selecionar novamente as cascatas A ainda é o ideal.
Nem todas as funções são kashe-memorizáveis. O primeiro argumento deve ser um objeto, matriz ou função. Deve ser utilizável como uma chave para o WeakMap.


caixa (fn) => memoizedFn2 (caixa, ... args)


esta é a mesma função, apenas aplicada duas vezes. Uma vez para fn, uma vez para memoizedFn, adicionando uma chave inicial aos argumentos. Pode tornar qualquer função kashe-memoizable.


É bastante declarativo - ei função! Vou armazenar os resultados nesta caixa.

 // could not be "kashe" memoized const addTwo = (a,b) => ({ result: a+b }); const bAddTwo = boxed(addTwo); const cacheKey = {}; // any object bAddTwo(cacheKey, 1, 2) === bAddTwo(cacheKey, 1, 2) === { result: 3} 

Se você marcar a função já memorizada - você aumentará a chance de memorização, como por exemplo, memorização - você poderá criar uma cascata de memorização.


 const selectSomethingFromTodo = (state, prop) => ... const selector = kashe(selectSomethingFromTodo); const boxedSelector = kashe(selector); class Component { render () { const result = boxedSelector(this, todos, this.props.todoId); // 1. try to find result in `this` // 2. try to find result in `todos` // 3. store in `todos` // 4. store in `this` // if multiple `this`(components) are reading from `todos` - // selector is not working (they are wiping each other) // but data stored in `this` - exists. ... } } 

caixa de entrada (fn) => memoizedFn2 (caixa, ... args)


este é o oposto da caixa, mas fazendo quase o mesmo, comandando o cache aninhado para armazenar dados na caixa fornecida. De um ponto de vista - reduz a probabilidade de memoização (não há cascata de memoização), mas de outro - remove as colisões de cache e ajuda a isolar os processos se eles não devem interferir um com o outro por qualquer motivo.


É bastante declarativo - ei! Todo mundo lá dentro! Aqui está uma caixa para usar

 const getAndSet = (task, number) => task.value + number; const memoized = kashe(getAndSet); const inboxed = inbox(getAndSet); const doubleBoxed = inbox(memoized); memoized(task, 1) // ok memoized(task, 2) // previous result wiped inboxed(key1, task, 1) // ok inboxed(key2, task, 2) // ok // inbox also override the cache for any underlaying kashe calls doubleBoxed(key1, task, 1) // ok doubleBoxed(key2, task, 2) // ok 

fork (kashe-memoized) => kashe-memoized


Fork é um fork real - ele obtém qualquer função kashe-memoized e retorna a mesma, mas com outra entrada de cache interna. Lembre-se do método de fábrica redux mapStateToProps?


 const mapStateToProps = () => { // const selector = createSelector(...); // const selector = fork(realSelector); // just fork existing selector. Or box it, or don't do anything // kashe is more "stable" than reselect. return state => ({ data: selector(data), }); } 

Selecionar novamente


E há mais uma coisa que você deve saber - o kashe poderia substituir a nova seleção. Literalmente.


 import { createSelector } from 'kashe/reselect'; 

Na verdade, é a mesma nova seleção, criada com o kashe como uma função de memorização.


Codesandbox


Aqui está um pequeno exemplo para brincar. Além disso, você pode checar os testes - eles são compactos e sólidos.
Se você quiser saber mais sobre armazenamento em cache e memorização - verifique como eu escrevi a biblioteca de memorização mais rápida de um ano atrás.


PS: Vale ressaltar, que a versão mais simples dessa abordagem - memoize fraca - é usada nos js de emoção por um tempo. Não há queixas. O nano-memoize também usa o WeakMaps para um único argumento.

Entendeu? Uma abordagem mais "fraca" ajudaria você a se lembrar melhor de algo e a esquecê-lo melhor.


https://github.com/theKashey/kashe


Sim, sobre o esquecimento de alguma coisa, - você poderia procurar aqui?


Source: https://habr.com/ru/post/pt446390/


All Articles