perf(lsp): clear table by table.clear() #39222

benchmark: https://gist.github.com/ofseed/6224529d77c016c36f7ab2f977059848

    local rounds = tonumber(arg[1]) or 1000
    local count = tonumber(arg[2]) or 1000

    -- Load the table.clear function.
    local clear = require("table.clear")

    local function fill(t, n)
      for i = 1, n do
        t[i] = i
      end
    end

    local function bench_reassign(n_rounds, n_items)
      local t = {}
      local start = os.clock()

      for _ = 1, n_rounds do
        t = {}
        collectgarbage("collect")
        fill(t, n_items)
      end

      return os.clock() - start
    end

    local function bench_reassign_no_gc(n_rounds, n_items)
      local t = {}
      local start = os.clock()

      for _ = 1, n_rounds do
        t = {}
        fill(t, n_items)
      end

      return os.clock() - start
    end

    local function bench_clear(n_rounds, n_items)
      local t = {}
      local start = os.clock()

      for _ = 1, n_rounds do
        clear(t)
        fill(t, n_items)
      end

      return os.clock() - start
    end

    -- Warm up LuaJIT before the real benchmark.
    do
      local t = {}
      for _ = 1, 2000 do
        clear(t)
        fill(t, count)
      end
    end

    collectgarbage("collect")

    local reassign_time = bench_reassign(rounds, count)
    collectgarbage("collect")

    local reassign_no_gc_time = bench_reassign_no_gc(rounds, count)
    collectgarbage("collect")

    local clear_time = bench_clear(rounds, count)

    print(string.format("rounds=%d count=%d", rounds, count))
    print(string.format("t = {} + GC   : %.6f s", reassign_time))
    print(string.format("t = {}        : %.6f s", reassign_no_gc_time))
    print(string.format("table.clear   : %.6f s", clear_time))
    print(string.format("vs + GC       : %.2fx", reassign_time / clear_time))
    print(string.format("vs no GC      : %.2fx", reassign_no_gc_time / clear_time))

benchmark result:

    rounds=1000 count=1000
    t = {} + GC   : 0.022469 s
    t = {}        : 0.002570 s
    table.clear   : 0.000387 s
    vs + GC       : 58.06x
    vs no GC      : 6.64x

`count` is how many items the table has, and `round` is how many rounds we fill
the table, clear, and then refill it. `table = {}` is clear the table by
resigning a new empty one, because this script does not run persistently like
nvim so GC is not triggered, so I added another extreme control group that
manually triggers GC.
This commit is contained in:
Yi Ming
2026-04-22 23:38:58 +08:00
committed by GitHub
parent 61fb88992d
commit 558204d87b
5 changed files with 53 additions and 23 deletions

View File

@@ -0,0 +1,27 @@
-- Basic shim for LuaJIT's table.new and table.clear.
local has_new, new = pcall(require, 'table.new')
local has_clear, clear = pcall(require, 'table.clear')
local M = {}
if not has_new then
---@diagnostic disable-next-line: unused-local
new = function(narr, nrec)
return {}
end
end
if not has_clear then
clear = function(tab)
---@diagnostic disable-next-line: no-unknown
for k in pairs(tab) do
---@diagnostic disable-next-line: no-unknown
tab[k] = nil
end
end
end
M.new = new
M.clear = clear
return M

View File

@@ -1,5 +1,6 @@
local util = require('vim.lsp.util')
local log = require('vim.lsp.log')
local tableclear = require('vim._core.table').clear
local api = vim.api
---@type table<lsp.FoldingRangeKind, true>
@@ -51,12 +52,13 @@ Capability.all[State.name] = State
--- Re-evaluate the cached foldinfo in the buffer.
function State:evaluate()
---@type table<integer, [integer, ">" | "<"?]?>
local row_level = {}
---@type table<integer, table<lsp.FoldingRangeKind, true?>?>>
local row_kinds = {}
---@type table<integer, string?>
local row_text = {}
local row_level, row_kinds, row_text, row_virt_text =
self.row_level, self.row_kinds, self.row_text, self.row_virt_text
tableclear(row_level)
tableclear(row_kinds)
tableclear(row_text)
tableclear(row_virt_text)
for client_id, ranges in pairs(self.client_state) do
for _, range in ipairs(ranges) do
@@ -88,11 +90,6 @@ function State:evaluate()
end
end
end
self.row_level = row_level
self.row_kinds = row_kinds
self.row_text = row_text
self.row_virt_text = {}
end
--- Force `foldexpr()` to be re-evaluated, without opening folds.
@@ -190,10 +187,10 @@ end
function State:reset()
self.lang = vim.treesitter.language.get_lang(vim.bo[self.bufnr].filetype)
self.row_level = {}
self.row_kinds = {}
self.row_text = {}
self.row_virt_text = {}
tableclear(self.row_level)
tableclear(self.row_kinds)
tableclear(self.row_text)
tableclear(self.row_virt_text)
end
--- Initialize `state` and event hooks, then request folding ranges.
@@ -201,7 +198,11 @@ end
---@return vim.lsp.folding_range.State
function State:new(bufnr)
self = Capability.new(self, bufnr)
self:reset()
self.lang = vim.treesitter.language.get_lang(vim.bo[self.bufnr].filetype)
self.row_level = {}
self.row_kinds = {}
self.row_text = {}
self.row_virt_text = {}
api.nvim_buf_attach(bufnr, false, {
-- Reset `bufstate` and request folding ranges.

View File

@@ -1,5 +1,6 @@
local util = require('vim.lsp.util')
local log = require('vim.lsp.log')
local tableclear = require('vim._core.table').clear
local api = vim.api
local M = {}
@@ -99,10 +100,10 @@ end
function Provider:clear()
self:reset_timer()
self.version = nil
self.row_version = {}
tableclear(self.row_version)
for _, state in pairs(self.client_state) do
state.row_lenses = {}
tableclear(state.row_lenses)
api.nvim_buf_clear_namespace(self.bufnr, state.namespace, 0, -1)
end
@@ -130,8 +131,8 @@ function Provider:handler(err, result, ctx)
return
end
---@type table<integer, lsp.CodeLens[]>
local row_lenses = {}
local row_lenses = state.row_lenses
tableclear(row_lenses)
-- Code lenses should only span a single line.
for _, lens in ipairs(result or {}) do
@@ -140,8 +141,6 @@ function Provider:handler(err, result, ctx)
table.insert(lenses, lens)
row_lenses[row] = lenses
end
state.row_lenses = row_lenses
self.version = ctx.version
api.nvim__redraw({ buf = self.bufnr, valid = true, flush = false })
@@ -516,7 +515,7 @@ function M.on_refresh(err, _, ctx)
if not provider.timer then
provider:request(client_id, function()
if api.nvim_buf_is_valid(bufnr) then
provider.row_version = {}
tableclear(provider.row_version)
vim.api.nvim__redraw({ buf = bufnr, valid = true, flush = false })
end
end)