Lua table 如何實現最快的 insert?

  • 2020 年 3 月 18 日
  • 筆記

前兩天群里有個朋友貼出一段代碼:

function _M.table_keys(self, tb)    if type(tb) ~= "table" then        ngx.log(ngx.WARN, type(tb) .. "was given to table_keys")        return tb    end      local t = {}    for key,_ in pairs(tb) do        table.insert(t, key)    end      return tend

用來拷貝 HTTP Header,是這樣來調用的:

request.REQUEST_HEADERS_NAMES = twaf_func:table_keys(request_headers)

但是他發現,這個操作很耗性能。加了這句,就少了 "5000" 並發。

且不管他這 "5000" 並發是怎麼計算出來的。今天,我們就來探討下 table insert 最快的方法。

CASE 1

題外話:根據 Lua Wiki 上的優化建議,local 化的變量會更快。但是這在 LuaJIT 上幾乎已經沒有了優勢。

local t = {}local table_insert = table.insert  for i=1,1e7 do    table_insert(t, i)end

最經典的寫法,LuaJIT 2.1 耗時:1838ms

CASE 2

根據 Lua Wiki 上的優化建議 Lua 5.1 Optimization Notes:

Short inline expressions can be faster than function calls. t[#t+1] = 0 is faster than table.insert(t, 0).

local t = {}  for i=1,1e7 do    t[#t+1] = iend

優化後,LuaJIT 2.1 耗時:1836ms

似乎和 CASE-1 並沒有明顯差距,lua-resty-waf 的作者指出了其中的問題:

通過對比二者的 trace log,可以發現它們幾乎沒有明顯區別,但是都調用了 lj_tab_len 來獲取 t 的長度,這個操作的時間複雜度為 O(log n),那麼完成整個 insert 動作的時間複雜度就是 O(n log n),是影響二者耗時的主要原因。

CASE 3

我們嘗試將 lj_tab_len 幹掉,自己來計算 t 的長度。那麼理論上完成整個 insert 動作的時間複雜度就簡化為了 O(n)

local t = {}local c = 1  for i=1,1e7 do    t[c] = i    c = c + 1end

結果 LuaJIT 2.1 耗時:57ms

一個簡單的優化,性能就提升了驚人的 32 倍!

CASE 4

CASE-3 的性能已經非常好了,但還是漏了一個優化的點:table 的擴容。

table 的擴容用的是 hashpow2,它是不小於 table hash or array 區域數量的 2^n^ 形式的整數

local table_new = require "table.new"  local t = table_new(1e7, 0)local c = 1  for i=1,1e7 do    t[c] = i    c = c + 1end

結果 LuaJIT 2.1 耗時:30ms

性能進一步提升到 64 倍!!!還可以更快嗎?

參考文獻

•Optimisation Coding Tips•Fast(er)(est) Table Inserts in LuaJIT•Performance of array creation in Lua