lodashにおけるリストキャッシュの実装解析

JavaScriptライブラリであるlodashでは、Mapのような機能をエミュレートするためにListCacheというデータ構造が実装されています。このキャッシュ機構は、環境がネイティブなMapをサポートしていない場合に代替手段として利用されます。

目的と利用方法

Hashキャッシュではオブジェクトベースのストレージを使用しており、キーとして文字列またはSymbol型しか受け付けません。これに対してMapは任意の型をキーとして使用できるため、Hashキャッシュでは配列やオブジェクトなどの複雑な型をキーとした場合に対応できません。

ListCacheは二次元配列構造を利用してデータを保存し、以下のように使用します:

const cacheInstance = new ListCache([
  [{identifier: 'ObjectBasedKey'}, 100],
  [['ArrayBasedKey'], 200],
  [function(){return 'FunctionAsKey'}, 300]
]);

この構造は次のような形式でデータを保持します:

{
  size: 3,
  __storage__: [
    [{identifier: 'ObjectBasedKey'}, 100],
    [['ArrayBasedKey'], 200],
    [function(){return 'FunctionAsKey'}, 300]
  ]
}

インターフェース設計

ListCacheはMapと同様のAPIを提供し、標準的なマップ操作メソッドを実装しています。

依存関係

import findEntryIndex from './findEntryIndex.js'

実装詳細

class ListCache {
  
  constructor(initialEntries) {
    let currentIndex = -1;
    const totalEntries = initialEntries == null ? 0 : initialEntries.length;

    this.reset();
    while (++currentIndex < totalEntries) {
      const currentEntry = initialEntries[currentIndex];
      this.assign(currentEntry[0], currentEntry[1]);
    }
  }

  reset() {
    this.__storage__ = [];
    this.size = 0;
  }

  remove(targetKey) {
    const storage = this.__storage__;
    const targetIndex = findEntryIndex(storage, targetKey);

    if (targetIndex < 0) {
      return false;
    }
    const finalIndex = storage.length - 1;
    if (targetIndex === finalIndex) {
      storage.pop();
    } else {
      storage.splice(targetIndex, 1);
    }
    --this.size;
    return true;
  }

  retrieve(targetKey) {
    const storage = this.__storage__;
    const foundIndex = findEntryIndex(storage, targetKey);
    return foundIndex < 0 ? undefined : storage[foundIndex][1];
  }

  exists(targetKey) {
    return findEntryIndex(this.__storage__, targetKey) > -1;
  }

  assign(key, value) {
    const storage = this.__storage__;
    const existingIndex = findEntryIndex(storage, key);

    if (existingIndex < 0) {
      ++this.size;
      storage.push([key, value]);
    } else {
      storage[existingIndex][1] = value;
    }
    return this;
  }
}

コンストラクタ

初期化処理ではresetメソッドを呼び出して内部状態を初期化し、渡されたエントリーデータをassignメソッドを使ってキャッシュに追加していきます。resetメソッドは__storage__とsizeプロパティの初期化を担当します。

resetメソッド

reset() {
  this.__storage__ = [];
  this.size = 0;
}

キャッシュをクリアする処理で、ストレージを空の配列に設定し、要素数を0にリセットします。

existsメソッド

exists(key) {
  return findEntryIndex(this.__storage__, key) > -1;
}

指定されたキーを持つデータがキャッシュ内に存在するかを判定します。findEntryIndex関数は二次元配列内で対応する[key, value]ペアのインデックスを検索し、存在しない場合は-1を返します。

assignメソッド

assign(key, value) {
  const storage = this.__storage__;
  const index = findEntryIndex(storage, key);

  if (index < 0) {
    ++this.size;
    storage.push([key, value]);
  } else {
    storage[index][1] = value;
  }
  return this;
}

キャッシュへの値の追加または更新を行います。既存のキーが見つからない場合はサイズを増加させ新しいペアを追加し、既存の場合は値を上書きします。

retrieveメソッド

retrieve(key) {
  const storage = this.__storage__;
  const index = findEntryIndex(storage, key);
  return index < 0 ? undefined : storage[index][1];
}

キャッシュから指定されたキーに対応する値を取得します。存在しない場合はundefinedを返却します。

removeメソッド

remove(key) {
  const storage = this.__storage__;
  const index = findEntryIndex(storage, key);

  if (index < 0) {
    return false;
  }
  const lastPosition = storage.length - 1;
  if (index === lastPosition) {
    storage.pop();
  } else {
    storage.splice(index, 1);
  }
  --this.size;
  return true;
}

指定されたキーを持つエントリを削除します。削除対象が最終要素の場合にはpopメソッドを使用し、それ以外はspliceメソッドを使用することでパフォーマンスを最適化しています。popメソッドはspliceよりも約17%高速であることが確認されています。

タグ: javascript lodash cache map listcache

7月26日 21:33 投稿