インデックスアクセスと rawget/rawset は、どちらが高速か?

以下のスクリプトを書いてテストしてみた。

-- rawget と ls[k]     および
-- rawset と ls[k] = v は、どっちが高速か?

-- 関数 f を n 回実行するのにかかった時間を測定する
do
  local clock = os.clock
  
  function time( n, f, ... )
    local oldtime = clock()
    
    for i = 1, n do
      f(...)
    end
    
    return clock() - oldtime
  end
end

-- ある関数 f を 1 秒間で何回実行出来るかを測定する
do
  local time = time
  function benchmark( f, ... )
    local n = 1000 -- 最低でも実行する回数
    local t = 0
    
    local total = 0
    repeat
      t = t + time( n, f, ... )
      total = total + n
      n = n * 2
    until t >= 1.0
    
    return total / t
  end
end

-- ベンチマークを表示する
do
  local print = print
  local floor = math.floor
  
  function print_benchmark( message, f, ... )
    print( message, floor( benchmark( f, ... ) + 0.5 ) )
  end
end

-- テストと実行速度の安定を兼ねて、関数呼び出しのみの場合を計測
function id(...)
  return ...
end
print_benchmark( "id", id )


-- テーブルアクセス
do
  local n = 100
  local t = {}
  
  for i = 1, n do
    t[i] = i
  end
  
  -- 存在するキーに対して get
  function getA()
    local tmp
    for i = 1, n do
      tmp = t[i]
    end
  end
  -- 存在しないキーに対して get
  function getB()
    local tmp
    for i = n + 1, n + n do
      tmp = t[i]
    end
  end
  
  local rawget = rawget
  -- 存在するキーに対して rawget
  function rawgetA()
    local tmp
    for i = 1, n do
      tmp = rawget( t, i )
    end
  end
  -- 存在しないキーに対して rawget
  function rawgetB()
    local tmp
    for i = n + 1, n + n do
      tmp = rawget( t, i )
    end
  end
  
  -- 実装補助
  function set_( t )
    for i = 1, n do
      t[i] = i
    end
  end
  local rawset = rawset
  function rawset_( t )
    for i = 1, n do
      rawset( t, i, i )
    end
  end
  
  -- 存在するキーに対して set
  function setA()
    set_(t)
  end
  -- 存在しないキーに対して set
  function setB()
    set_({})
  end
  -- 存在するキーに対して rawset
  function rawsetA()
    rawset_(t)
  end
  function rawsetB()
    rawset_({})
  end
  
end

print_benchmark( "getA", getA )
print_benchmark( "getB", getB )
print_benchmark( "rawgetA", rawgetA )
print_benchmark( "rawgetB", rawgetB )
print_benchmark( "setA", setA )
print_benchmark( "setB", setB )
print_benchmark( "rawsetA", rawsetA )
print_benchmark( "rawsetB", rawsetB )

結果(例):

id	11032323
getA	206645
getB	147059
rawgetA	80482
rawgetB	70673
setA	175499
setB	77392
rawsetA	58442
rawsetB	42000


既に存在するキーに対するアクセスの場合も、そうでない場合も、
基本的に普通のテーブルアクセスの方が高速のようです。
恐らくこれは、関数呼び出しをしている分のコストなのだと思います。
普通にテーブルアクセスをする場合は、専用の VM コードにコンパイルされるでしょうから。


また、今回はメタテーブルのない場合でテストしましたが、
メタテーブルを使った場合でも、既に存在するキーに対しては全く処理内容は変わらないですし、
そうでない場合には rawget/rawset とインデックスアクセスで処理内容変わりますから、
結局、通常のインデックスアクセスの方が、 rawset/rawget より常に速いと言えそうです。