memoization

pjfb·2023년 2월 19일

memoization

ReactツリーでO(Node)が少しでも(非常に本当の少しでも、それともまったく変わらず内部状態だけが変わっても)EFNADGM~JKLはすべてレンダリング(実行)する必要があります。

Reactはなぜこのような銅の構造になっているのでしょうか? ほんの少しの変更だけを感知して変わった部分だけレンダリングしてくれればいいのではないでしょうか?

この部分を理解するためには、fpについて学ぶ必要があります。

function fibonacci(num) {
     if(num < 2) {
         return num;
     }
     else {
         return fibonacci(num - 1) + fibonacci(num - 2);
     }
}

上記のコードは、フィボナッチ値を取得する単純な関数です。

const a = fibonacci(5);
const b = fibonacci(6);
const c = fibonacci(7);

そしてこれはfibonacci関数を呼び出すコードです。

上記のコードは効率的ですか?

一目で見てもわかりますが、絶対に効率的ではありません。

私たちはコンピュータではなく人なので、上記の状況でいくつかのことを考えることができます。

  • b は a + fibonacci(4) と同じ値です。
  • c は a + b です。

しかし、コンピュータは愚かなので、コンピュータにとって、a、b、c演算は完全に別々の領域です。 以前の結果値を全く活用できません。

実際に上記のa、b、cを求めるコードを実行すると、出てくる呼び出し順序(…)

> fibonacci(6)
VM1472 fib 6
VM1472 fib 5
VM1472 fib 4
VM1472 fib 3
VM1472 fib 2
VM1472 fib 1
VM1472 fib 0
VM1472 fib 1
VM1472 fib 2
VM1472 fib 1
VM1472 fib 0
VM1472 fib 3
VM1472 fib 2
VM1472 fib 1
VM1472 fib 0
VM1472 fib 1
VM1472 fib 4
VM1472 fib 3
VM1472 fib 2
VM1472 fib 1
VM1472 fib 0
VM1472 fib 1
VM1472 fib 2
VM1472 fib 1
VM1472 fib 0

> fibonacci(5)
VM1472 fib 5
VM1472 fib 4
VM1472 fib 3
VM1472 fib 2
VM1472 fib 1
VM1472 fib 0
VM1472 fib 1
VM1472 fib 2
VM1472 fib 1
VM1472 fib 0
VM1472 fib 3
VM1472 fib 2
VM1472 fib 1
VM1472 fib 0
VM1472 fib 1

> fibonacci(4)
VM1472 fib 4
VM1472 fib 3
VM1472 fib 2
VM1472 fib 1
VM1472 fib 0
VM1472 fib 1
VM1472 fib 2
VM1472 fib 1
VM1472 fib 0

人が計算した場合、絶対にしない計算はコンピュータが愚かなので実行し続けます。

幸いなことに、人の頭よりもコンピュータが非常に速いので、非常に短い瞬間に結果が出てくるでしょう。

それでは、私たちの最初のコードを以下のように単語だけを少し変えてみましょう。

const domA = React.render(stateA);
const domB = React.render(stateB);
const domC = React.render(stateC);

フィボナッチのように再帰ではありませんが、なぜかReactコードも似ていませんか?

しばらくリアクトコードを見たので、もう一度リアクトからフィボナッチに戻りましょう。

そして再びリアクトに戻って仕上げます。

(私たちが書いたフィボナッチコードも実際にはツリーです!Reactとコードだけが似ているのではなく、構造も似ています。

矢印で巡回シーケンスも描かれています)

こうなると気付いたでしょうが、重複するノードの計算をしなくてしまうのを「memoization」と言います。

メモを作成する方法はとても簡単です。

以下のコードはメモの最も簡単な実装です。

let m = {};

const fibo = (n) => {
   // 既に作成された値がある場合に使用します。
   if (m[n] !== undefined) return m[n];

   if(num < 2) {
       return num;
   }
   else {
       const result = fibonacci(num - 1) + fibonacci(num - 2);
       //計算後に保存します。
       m[n] = result;
       return result;
   }
};

fibo(5); //
fibo(5); // cached!

メモは、入力値が同じ場合、出力値も常に同じ「純粋関数」の性質に基づいています。

( fibo(5) の結果値は外部変数、あるいは時間にまったく影響を受けず、常に同じ値を返すことを考えてみてください)

メモは高次関数でも実装できます。

const memoize = (fn) => {
   const m = {};
   reutrn(n) => {
      if (m[n] !== undefined) return m[n];
      const result = fn(n);
      m[n] = result;
      return result;
   };
};

const memoizedFibonacci = memoize(fibonacci);

memoizedFibonacci(5);
memoizedFibonacci(4);
memoizedFibonacci(3);

上記のコードを見て、React.memo関数が浮かんでいるなら正解です。

React.memoの実際のコードをもう一度見てみましょうか?

const Fibo = ({ n }) => {
   return <div>{fibonacci(n)}</div>;
};

// memoは、関数(コンポーネント)を受け取り、関数(コンポーネントを)返す高次関数です。
export default React.memo(Fibo);

memo は React の方法で React コンポーネントをmemoするコンポーネントです。 当然不要な計算を減らします。

そして先ほど話したのと同じように、これも「純粋関数」の性質に基づいています。

よく分離されたReactコンポーネントも純粋関数です。

以下の例を見てください:

const Profile = ({ user }) => {
   // body
   return <div>{user.name}さんこんにちは</div>;
};

const UserA = { id: 1, name: 'Park' };
const UserB = { id: 2, name: 'Kim' };

// domAとdomBは本当です。
// strictEqualではなく、Reactのvdomによって本当に同じです。
const domA = Profile({ user:UserA });
const domB = Profile({ user: UserA });

//入力が変わるので、出力も変わります。
const domC = Profile({ user:UserB });

// 今、メモをしましょう。
const MemoizedProfile = React.memo(Profile);
// export default React.memo(Profile); //

const domD = MemoizedProfile({ user:UserA });
//以下の行は `Profile`を実行せずにdomEを吐きます。
// それでも問題はありません。 (純粋関数だから)
const domE = MemoizedProfile({ user: UserA });

ここまで来ると、「memoization」が何であったのか、そして「React.memo」が実際に何をしている子供なのかがわかります。

そして以下の注意事項を覚えておいてください:

  • Reactのメモは、ツリー内の場所が異なるノードにもメモがありません。
    • 上図参照(ツリーにX表記されているもの)
  • memoを使って自分自身とサブツリーが再実行されないようにしてください。
    • memoは自分だけメモするのではなく、再実行が下に広がるのを防ぎます。
    • またまた上図参照
  • コンポーネントを純粋関数に近づけて作成してください。
    • propsに「同じことを比較するのが難しいもの」を入れないでください。 (WebSocket、Blob、ArrayBuffer、または各種クラスのインスタンス)
    • 関数インスタンスは、過去の時間にわかった関数参照メモで解決できます。

仕上げ

これで、Reactがなぜこのような「最適化されていないような構造」になっているのかを理解できます。

実は遅いのはReactではなく私のコードだったんですね…。

React(少なくともReact開発者)の立場では、Oが変わるとEFNADGM~JKLもレンダリングされるのではなく、EFNA~~~はただNO-OPです。 (そうする必要があります。)

(no-opは実行は行われたが、その実行で何もしないことを意味します。)

なぜReactは再実行による反応性を選んだのですか?

しかし、なぜこれを行うのですか? いくらnoopと言っても実行は起こり、memoizationのためのコストは発生しますが、ただ変わった部分だけ正確に変更すればいいのではないでしょうか?

これに対する私の推測は2つあります。

  • 状態とDOMを一致させるのは難しいです。
const Profile = ({ user }) => {
   return(
     <div>
       <div>名前:{user.name} </div>
       <div>年齢:{user.age} </div>
       <div>性別:{user.gender} </div>
     </div>;
};

もし上記のコードがやり直しではなく、本当に変更された部分だけをコックでレンダリングする仕組みになると考えてみてください。

いったんjavascriptの文法が支えられません。

Vue3の場合を一度見てください。

「状態とDOMをマッチングさせる」ことに近い(実際に完全に合うこともできる)実装体です。

ただし、toRefs computedのような追加の関数が必要になります。 (おそらくいくつかあることを知っています。)

これは追加の複雑さを作成します。

  • Reactでは、すべての作業は明示的という基本となる哲学があります。 (私がワンウェイバインディングを見て想像したもの)

この部分はブログ記事を参照してください。

  • コンピュータは高速でReactは遅くはありません。

ReactにはすでにVDOMなどの追加の効率的なアルゴリズムが含まれています。

そして、コンピュータとスマートフォンは十分に高速です。

memoを書いていないコードも十分に高速ではありませんか?

useSelector(state => state.a);
<Profile style={{ width: '100px}} onPress={() => {}} />

메모이제이션

0개의 댓글