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 而变得更快,而是使用某种方法使 slotcache 无效(例如维护反向 slotcache 依赖项列表)。每当基类方法或函数发生更改时,反向依赖链的 slotcache 将失效。如果将“_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 日星期一 15:51:45 美国东部时间