Lua技術備註 8

在 Lua 中實作快速的多重繼承標籤方法

作者 David Jeske

摘要

此備註說明一種基於 Lua 標籤方法的多重繼承樣式類別系統,其效能類似 Python 等語言。

問題

有時需要一個繼承樣式類別系統來組合 Lua 物件。一篇關於使用 Lua 的簡短文章中提出了一個稱為「透過後備繼承」[1] 的預設單一繼承方案。然而,隨著繼承鏈變長,處理此實作所使用的重複查詢走訪父鏈的時間可能會變得不理想。此外,有時除了單一繼承之外,同時擁有多重繼承也很方便。

解決方案

提出一個標籤方法繼承方案,提供單一和多重繼承,其實作透過快取在類似 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,改用某種方法來使 slotcaches 失效(例如維護一個反向 slotcache 相依性清單)。每當基底類別方法或函式變更時,反向相依性鏈的 slotcaches 就會失效。如果將「_slotcache」改為在物件的「類別」層級而非目前的「實例」層級發生,這可能會導致僅有適度的額外資料保留。

缺點

由於 Lua 中的 nil 被覆寫為 false 和不存在資料值的意義,因此加入了一些技巧來正確快取繼承的 nil 值。沒有這些技巧,任何預期為遺失或 false 的實例資料或方法都會導致每次都發生昂貴的 _parents 查詢。由於假設基底類別永遠不會變更,因此這是安全的最佳化。即使 _slotcache 中存在「快取的 NIL」值,將實例成員設定為其他值也會覆寫「快取的 NIL」,因為僅在實例表格中的查詢遺失時才會諮詢 _slotcache

重要的是要了解,由於快取版本將資訊直接儲存在單一扁平表格中,因此如果基底類別方法已在快取表格中,變更這些方法可能會被忽略。在實務上,變更基底類別的方法是不常見的操作。使用這個機制時,應該完全避免這種做法。

由於 Lua(在我看來是錯誤地)將 nil 覆寫為 false 和空資料元素的意義,因此無法使用 nil 值覆寫繼承的物件成員。這是因為設定 self.a = nil 會導致移除「a」成員,進而導致遺失索引標籤方法觸發,而該方法會諮詢 _parents_slotcache 表格以尋找繼承的元素「a」。我尚未找到解決這個問題的解決方法。

結論

這個說明說明了一種使用 Lua 內建標籤方法實作多重繼承的效能最佳化方法。這使得在 Lua 中建立更大、更有用的類別階層成為可能,而不會像「每次都進行父項查詢」的簡化實作那樣造成顯著的效能損失。

包含一些有用的公用程式函式的完整解決方案程式碼,請參閱這裡

參考

[1] R. Ierusalimschy、L. H. de Figueiredo、W. Celes,「Lua - 一種可延伸的延伸語言」軟體:實務與經驗 26 #6 (1996) 635-652。


最後更新時間:2002 年 8 月 12 日星期一下午 3:51:45 美東時間