Lua技術ノート 8

Luaにおける高速な多重継承タグメソッド実装

by David Jeske

概要

このノートでは、Luaのタグメソッドに基づいた多重継承スタイルのクラスシステムについて説明します。このシステムは、Pythonなどの言語と同等の性能を提供します。

問題点

Luaオブジェクトを合成するために、継承スタイルのクラスシステムが必要になる場合があります。「フォールバックによる継承」[1] と呼ばれるデフォルトの単一継承スキームは、Luaの使用に関する短い記事で提案されています。しかし、継承チェーンが長くなると、この実装で使用されている親チェーンを繰り返し検索する処理時間が問題となる可能性があります。さらに、単一継承に加えて多重継承を使用することも便利です。

解決策

単一継承と多重継承の両方を提供するタグメソッド継承スキームを提案します。この実装では、Pythonなどの言語が継承を単一のテーブルにフラット化する方法と同様に、フラットなハッシュテーブルにキャッシュすることで、継承されたデータと関数のアクセス速度を大幅に向上させます。この最適化されたバージョンでは、実行時に基底クラスのメソッドを変更しないことを前提としています。

キャッシュを使用しない実装は非常にシンプルです。_parents = {}というメンバーを使用してテーブルに継承チェーンを作成します。

-- ********************************************************
-- index tag method AdvProtoIndex(t,f)
--
-- This is a 'simple' version of the multiple inheritance
-- tag method. It does not cache and thus it is quite slow.
-- However, if you think something strange is happening, you
-- can fall back to this version and see if the strangeness
-- goes away.

function AdvProtoIndex (t,f)
  
  if f == '_parents' then -- to avoid loop
    if (OldIndex) then
	    return OldIndex(t,f)
	else
		return nil;
	end
  end

  local p = t["_parents"];

  if (type(p) == 'table') then
     local cur_index = 1; -- start at 1
	 local cur_data;

	 repeat
	   cur_data = p[cur_index];
	   if (type(cur_data) == 'table') then
	       local result = cur_data[f];
	       if (result ~= nil) then
		       return result;        -- we found a match
		   end
	   else
	       if (OldIndex) then
		      return OldIndex(t,f);
		   else
		      return nil;
		   end
	   end
	   cur_index = cur_index + 1; -- do next parent
	 until (cur_data == nil);

	 return nil; -- we didn't find a match
  else 
     return nil;
  end
end
通常、すべてのテーブルに対してこのフォールバックタグメソッドを設定します。つまり、継承の作成は、適切なメンバーを持つテーブルを作成するだけという簡単な作業になります。
a_base = {
  a_number = 1
}

b_base = {
  b_number = 2
}

ab_class = {
  _parents = { a_base, b_base }
}

print(ab_class.a_number); -- yields "1"
print(ab_class.b_number); -- yields "2"
上記のような単純な実装を使用すると、継承チェーンを作成しやすくなりますが、実行時のパフォーマンスに深刻な影響を与える可能性があります。なぜなら、継承されたメソッド呼び出しやインスタンスデータへのアクセスにより、2n回のルックアップが発生する可能性があるためです。ここで、nは、チェーン全体で継承された基底クラスの数です。

ほとんど同じ機能を持ちながら、大幅に高速なパフォーマンス最適化実装を提供します。

----------------------------------------------------------
-- AdvProtoIndexWithCache
--
-- This inheritance tag method handles multiple inheritance via a
-- "_parent = {}" table. It caches information in the top-level table
-- to make lookups fast.
--
-- Example:
--
-- This tag method is applied to all tables, so all you have to do to
-- get a magic inheritance table is to do this:
--
-- BaseObj1 = { a_value = "a" }
-- BaseObj2 = { b_value = "b" }
-- MyClass  = { _parents = { BaseObj2, BaseObj1 } }
-- MyInstance = { _parents = { MyClass }
--

function setupMultipleInheritenceForTag(a_tag) 
    -- I like to setup my tag methods within a function because
    -- then stuff like these private declarations can be
    -- referenced with upvalues and disappear. :)

    local NIL_OBJECT = { magic_nil_object = 1}
    local SLOT_REF_TAG = newtag()
    local OldIndex = gettagmethod(tag({}),"index")
    local debug_mode = nil

    AdvProtoIndexWithCache = function (t,f, instance, depth)
      if (f == '_parents') or (f == '_slotcache') then -- to avoid loop
	if (%OldIndex) then
		return %OldIndex(t,f)
	    else
		return nil;
	    end
      end


      if instance == nil then
	-- we are the instance!
	instance = t 
      end
      if depth == nil then
	depth = 0
      end

      -- get out the parent table
      local p = rawgettable(t,"_parents")

      local cache = rawgettable(instance,"_slotcache");
      if cache then
	 local item = rawgettable(cache,f)
	 if item then
	   if item == %NIL_OBJECT then
	     return nil
	   elseif tag(item) == %SLOT_REF_TAG then
	     return item.obj[f]
	   else
	     return item
	   end
	 end
      else
	 -- if we are the instance AND we had a _parents
	 -- slot, then create the slot cache!

	 if (instance == t) and (p) then
	   cache = {}
	   rawsettable(t,"_slotcache",cache); -- make the slot cache!
	 end
      end

      if (type(p) == 'table') then
	 local cur_index = 1; -- start at 1
	     local cur_data;


	     repeat
	       cur_data = p[cur_index];

	       if (%debug_mode) then
		 print("---------", cur_index, depth)
	       end
	       if (type(cur_data) == 'table') then
		   if (%debug_mode) then
		     printTables(cur_data)
		   end

		 -- local result = cur_data[f];
		   local result = rawgettable(cur_data,f);

		   if (%debug_mode and (result ~= nil)) then
		     print("value: ", result)
		   end

		   -- if we found the slot in us, then we need
		   -- to do the caching, because after we return
		   -- it's not possible to make a SLOT_REF
		   if ((result ~= nil) and (cache)) then
                     rawsettable(instance,f,result);
		   end

		   if (result == nil) then
		     result = AdvProtoIndexWithCache(cur_data,f,instance, depth + 1)
		   end

		   if (result ~= nil) then
		     if (%debug_mode and (result ~= nil)) then
		       print("value: ", result) 
		     end

		     return result;        -- we found a match
		   end
	       else
		   local result 

		   if (%OldIndex) then
		     result = %OldIndex(t,f);
		   else
		     result = nil;
		   end

			   if cache then
			     if (type(result) == "function") then
			       rawsettable(cache,f,result);
			     elseif result == nil then 
			       rawsettable(cache,f,%NIL_OBJECT)
			     else
			       local slot_ref = { obj = cur_data }
			       settag(slot_ref,%SLOT_REF_TAG);
			       rawsettable(cache,f,slot_ref);
			     end
			   end
			   return result;        -- we found a match


	       end
	       cur_index = cur_index + 1; -- do next parent
	     until (cur_data == nil);

	     if cache then
	       rawsettable(cache,f,%NIL_OBJECT)
	     end

	     return nil; -- we didn't find a match
      else 
	 return nil;
      end
    end


    settagmethod(a_tag,"index",AdvProtoIndexWithCache);  -- Lua 3.1 method
end

説明

最終的な実装では、メソッド呼び出しのターゲットに "_slotcache" テーブルを作成します。_parentsによる多重継承機構によるメソッドルックアップが成功するたびに、その結果は元のターゲットの "_slotcache" に保存されます。

キャッシュ内では、関数は直接指し示され、その他の項目は「SLOT_REF」と呼ばれるものを使用して指し示されます。SLOT_REFは、値ではなく参照によるキャッシュである特殊な種類のテーブルです。元のテーブルと元の値のインデックスへの参照が含まれているため、元のデータ値が変更された場合でも、このSLOT_REFは新しいデータ値を正しく指し示します。これはメソッドには行われません。メソッドルックアップのパフォーマンスが、基底クラスのメソッドを変更する機能よりも重要であると判断されたためです。

SLOT_REFを廃止し、代わりにslotcacheの無効化を行う方法(逆slotcache依存リストを維持するなど)を使用することで、この機構の別の実装をさらに高速化できます。基底クラスのメソッドまたは関数が変更されるたびに、逆依存関係チェーンのslotcacheが無効になります。"_slotcache"を現在の「インスタンス」レベルではなく「クラス」レベルで発生させるように変更した場合、これはおそらく適度な追加データ保持に繋がるでしょう。

弱点

Luaでは`nil`が偽と存在しないデータ値の両方の意味としてオーバーライドされているため、継承された`nil`値を正しくキャッシュするために、いくつかの工夫が加えられています。これがないと、欠落しているか偽であることを意図したインスタンスデータまたはメソッドは、毎回高価な_parentsルックアップが発生します。基底クラスは変更されないという前提が立てられているため、これは安全な最適化です。`cached NIL`値が_slotcacheに存在する場合でも、インスタンスメンバーを他の値に設定すると`cached NIL`は上書きされます。なぜなら、_slotcacheは、インスタンステーブルでのルックアップに失敗した場合にのみ参照されるためです。

キャッシュバージョンは情報を単一のフラット化されたテーブルに直接保存するため、基底クラスのメソッドの変更は、既にキャッシュされたテーブルにある場合は無視される可能性があることに注意することが重要です。実際には、基底クラスのメソッドを変更することはまれな操作です。この機構を使用する場合は、この慣習を完全に避けるべきです。

Luaは(私の意見では誤って)`nil`を偽と空のデータ要素の両方の意味としてオーバーライドしているため、継承されたオブジェクトメンバーを`nil`値で上書きする方法がありません。これは、self.a = nilを設定すると、「a」メンバーが削除され、欠落インデックスタグメソッドが起動し、_parentsまたは_slotcacheテーブルを参照して継承された要素「a」を検索するためです。この問題に対する回避策はまだ見つかっていません。

結論

このノートでは、Luaの組み込みタグメソッドを使用して多重継承を実装するパフォーマンス最適化された方法について説明します。これにより、単純な「毎回親を検索する」実装による大きなパフォーマンス低下を招くことなく、Luaでより大きく、より有用なクラス階層を作成することが可能になります。

いくつかの便利なユーティリティ関数を含む、ソリューションの完全なコードはこちらで提供されています。

参考文献

[1] R. Ierusalimschy, L. H. de Figueiredo, W. Celes, "Lua-an extensible extension language", Software: Practice & Experience 26 #6 (1996) 635-652。


最終更新日: 2002年8月12日(月) 15:51:45 EST