mirror of
https://github.com/neovim/neovim.git
synced 2024-12-19 18:55:14 -07:00
feat(lua): add noref to deepcopy
Problem: Currently `deepcopy` hashes every single tables it copies so it can be reused. For tables of mostly unique items that are non recursive, this hashing is unnecessarily expensive Solution: Port the `noref` argument from Vimscripts `deepcopy()`. The below benchmark demonstrates the results for two extreme cases of tables of different sizes. One table that uses the same table lots of times and one with all unique tables. | test | `noref=false` (ms) | `noref=true` (ms) | | -------------------- | ------------------ | ----------------- | | unique tables (50) | 6.59 | 2.62 | | shared tables (50) | 3.24 | 6.40 | | unique tables (2000) | 23381.48 | 2884.53 | | shared tables (2000) | 3505.54 | 14038.80 | The results are basically the inverse of each other where `noref` is much more performance on tables with unique fields, and `not noref` is more performant on tables that reuse fields.
This commit is contained in:
parent
a064ed6229
commit
3734519e3b
@ -1905,15 +1905,24 @@ vim.deep_equal({a}, {b}) *vim.deep_equal()*
|
|||||||
Return: ~
|
Return: ~
|
||||||
(boolean) `true` if values are equals, else `false`
|
(boolean) `true` if values are equals, else `false`
|
||||||
|
|
||||||
vim.deepcopy({orig}) *vim.deepcopy()*
|
vim.deepcopy({orig}, {noref}) *vim.deepcopy()*
|
||||||
Returns a deep copy of the given object. Non-table objects are copied as
|
Returns a deep copy of the given object. Non-table objects are copied as
|
||||||
in a typical Lua assignment, whereas table objects are copied recursively.
|
in a typical Lua assignment, whereas table objects are copied recursively.
|
||||||
Functions are naively copied, so functions in the copied table point to
|
Functions are naively copied, so functions in the copied table point to
|
||||||
the same functions as those in the input table. Userdata and threads are
|
the same functions as those in the input table. Userdata and threads are
|
||||||
not copied and will throw an error.
|
not copied and will throw an error.
|
||||||
|
|
||||||
|
Note: `noref=true` is much more performant on tables with unique table
|
||||||
|
fields, while `noref=false` is more performant on tables that reuse table
|
||||||
|
fields.
|
||||||
|
|
||||||
Parameters: ~
|
Parameters: ~
|
||||||
• {orig} (table) Table to copy
|
• {orig} (table) Table to copy
|
||||||
|
• {noref} (boolean|nil) When `false` (default) a contained table is
|
||||||
|
only copied once and all references point to this single
|
||||||
|
copy. When `true` every occurrence of a table results in a
|
||||||
|
new copy. This also means that a cyclic reference can cause
|
||||||
|
`deepcopy()` to fail.
|
||||||
|
|
||||||
Return: ~
|
Return: ~
|
||||||
(table) Table of copied keys and (nested) values.
|
(table) Table of copied keys and (nested) values.
|
||||||
|
@ -282,6 +282,8 @@ The following new APIs and features were added.
|
|||||||
|vim.diagnostic.get()| when only the number of diagnostics is needed, but
|
|vim.diagnostic.get()| when only the number of diagnostics is needed, but
|
||||||
not the diagnostics themselves.
|
not the diagnostics themselves.
|
||||||
|
|
||||||
|
• |vim.deepcopy()| has a `noref` argument to avoid hashing table values.
|
||||||
|
|
||||||
==============================================================================
|
==============================================================================
|
||||||
CHANGED FEATURES *news-changed*
|
CHANGED FEATURES *news-changed*
|
||||||
|
|
||||||
|
@ -134,7 +134,7 @@ local function prefix_source(diagnostics)
|
|||||||
return d
|
return d
|
||||||
end
|
end
|
||||||
|
|
||||||
local t = vim.deepcopy(d)
|
local t = vim.deepcopy(d, true)
|
||||||
t.message = string.format('%s: %s', d.source, d.message)
|
t.message = string.format('%s: %s', d.source, d.message)
|
||||||
return t
|
return t
|
||||||
end, diagnostics)
|
end, diagnostics)
|
||||||
@ -146,7 +146,7 @@ local function reformat_diagnostics(format, diagnostics)
|
|||||||
diagnostics = { diagnostics, 't' },
|
diagnostics = { diagnostics, 't' },
|
||||||
})
|
})
|
||||||
|
|
||||||
local formatted = vim.deepcopy(diagnostics)
|
local formatted = vim.deepcopy(diagnostics, true)
|
||||||
for _, diagnostic in ipairs(formatted) do
|
for _, diagnostic in ipairs(formatted) do
|
||||||
diagnostic.message = format(diagnostic)
|
diagnostic.message = format(diagnostic)
|
||||||
end
|
end
|
||||||
@ -373,7 +373,7 @@ local function get_diagnostics(bufnr, opts, clamp)
|
|||||||
or d.col < 0
|
or d.col < 0
|
||||||
or d.end_col < 0
|
or d.end_col < 0
|
||||||
then
|
then
|
||||||
d = vim.deepcopy(d)
|
d = vim.deepcopy(d, true)
|
||||||
d.lnum = math.max(math.min(d.lnum, line_count), 0)
|
d.lnum = math.max(math.min(d.lnum, line_count), 0)
|
||||||
d.end_lnum = math.max(math.min(d.end_lnum, line_count), 0)
|
d.end_lnum = math.max(math.min(d.end_lnum, line_count), 0)
|
||||||
d.col = math.max(d.col, 0)
|
d.col = math.max(d.col, 0)
|
||||||
@ -636,7 +636,7 @@ function M.config(opts, namespace)
|
|||||||
|
|
||||||
if not opts then
|
if not opts then
|
||||||
-- Return current config
|
-- Return current config
|
||||||
return vim.deepcopy(t)
|
return vim.deepcopy(t, true)
|
||||||
end
|
end
|
||||||
|
|
||||||
for k, v in pairs(opts) do
|
for k, v in pairs(opts) do
|
||||||
@ -723,7 +723,7 @@ end
|
|||||||
---
|
---
|
||||||
---@return table A list of active diagnostic namespaces |vim.diagnostic|.
|
---@return table A list of active diagnostic namespaces |vim.diagnostic|.
|
||||||
function M.get_namespaces()
|
function M.get_namespaces()
|
||||||
return vim.deepcopy(all_namespaces)
|
return vim.deepcopy(all_namespaces, true)
|
||||||
end
|
end
|
||||||
|
|
||||||
---@class Diagnostic
|
---@class Diagnostic
|
||||||
@ -756,7 +756,7 @@ function M.get(bufnr, opts)
|
|||||||
opts = { opts, 't', true },
|
opts = { opts, 't', true },
|
||||||
})
|
})
|
||||||
|
|
||||||
return vim.deepcopy(get_diagnostics(bufnr, opts, false))
|
return vim.deepcopy(get_diagnostics(bufnr, opts, false), true)
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Get current diagnostics count.
|
--- Get current diagnostics count.
|
||||||
|
@ -44,7 +44,7 @@ function keymap.set(mode, lhs, rhs, opts)
|
|||||||
opts = { opts, 't', true },
|
opts = { opts, 't', true },
|
||||||
})
|
})
|
||||||
|
|
||||||
opts = vim.deepcopy(opts or {})
|
opts = vim.deepcopy(opts or {}, true)
|
||||||
|
|
||||||
---@cast mode string[]
|
---@cast mode string[]
|
||||||
mode = type(mode) == 'string' and { mode } or mode
|
mode = type(mode) == 'string' and { mode } or mode
|
||||||
|
@ -1353,7 +1353,7 @@ function lsp.start_client(config)
|
|||||||
---@param context? {bufnr: integer}
|
---@param context? {bufnr: integer}
|
||||||
---@param handler? lsp.Handler only called if a server command
|
---@param handler? lsp.Handler only called if a server command
|
||||||
function client._exec_cmd(command, context, handler)
|
function client._exec_cmd(command, context, handler)
|
||||||
context = vim.deepcopy(context or {}) --[[@as lsp.HandlerContext]]
|
context = vim.deepcopy(context or {}, true) --[[@as lsp.HandlerContext]]
|
||||||
context.bufnr = context.bufnr or api.nvim_get_current_buf()
|
context.bufnr = context.bufnr or api.nvim_get_current_buf()
|
||||||
context.client_id = client.id
|
context.client_id = client.id
|
||||||
local cmdname = command.command
|
local cmdname = command.command
|
||||||
|
@ -314,7 +314,7 @@ local constants = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for k, v in pairs(constants) do
|
for k, v in pairs(constants) do
|
||||||
local tbl = vim.deepcopy(v)
|
local tbl = vim.deepcopy(v, true)
|
||||||
vim.tbl_add_reverse_lookup(tbl)
|
vim.tbl_add_reverse_lookup(tbl)
|
||||||
protocol[k] = tbl
|
protocol[k] = tbl
|
||||||
end
|
end
|
||||||
|
@ -9,43 +9,36 @@
|
|||||||
---@diagnostic disable-next-line: lowercase-global
|
---@diagnostic disable-next-line: lowercase-global
|
||||||
vim = vim or {}
|
vim = vim or {}
|
||||||
|
|
||||||
local function _id(v)
|
---@generic T
|
||||||
return v
|
---@param orig T
|
||||||
end
|
---@param cache? table<any,any>
|
||||||
|
---@return T
|
||||||
local deepcopy
|
local function deepcopy(orig, cache)
|
||||||
|
if orig == vim.NIL then
|
||||||
local deepcopy_funcs = {
|
return vim.NIL
|
||||||
table = function(orig, cache)
|
elseif type(orig) == 'userdata' or type(orig) == 'thread' then
|
||||||
if cache[orig] then
|
|
||||||
return cache[orig]
|
|
||||||
end
|
|
||||||
local copy = {}
|
|
||||||
|
|
||||||
cache[orig] = copy
|
|
||||||
local mt = getmetatable(orig)
|
|
||||||
for k, v in pairs(orig) do
|
|
||||||
copy[deepcopy(k, cache)] = deepcopy(v, cache)
|
|
||||||
end
|
|
||||||
return setmetatable(copy, mt)
|
|
||||||
end,
|
|
||||||
number = _id,
|
|
||||||
string = _id,
|
|
||||||
['nil'] = _id,
|
|
||||||
boolean = _id,
|
|
||||||
['function'] = _id,
|
|
||||||
}
|
|
||||||
|
|
||||||
deepcopy = function(orig, _cache)
|
|
||||||
local f = deepcopy_funcs[type(orig)]
|
|
||||||
if f then
|
|
||||||
return f(orig, _cache or {})
|
|
||||||
else
|
|
||||||
if type(orig) == 'userdata' and orig == vim.NIL then
|
|
||||||
return vim.NIL
|
|
||||||
end
|
|
||||||
error('Cannot deepcopy object of type ' .. type(orig))
|
error('Cannot deepcopy object of type ' .. type(orig))
|
||||||
|
elseif type(orig) ~= 'table' then
|
||||||
|
return orig
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- @cast orig table<any,any>
|
||||||
|
|
||||||
|
if cache and cache[orig] then
|
||||||
|
return cache[orig]
|
||||||
|
end
|
||||||
|
|
||||||
|
local copy = {} --- @type table<any,any>
|
||||||
|
|
||||||
|
if cache then
|
||||||
|
cache[orig] = copy
|
||||||
|
end
|
||||||
|
|
||||||
|
for k, v in pairs(orig) do
|
||||||
|
copy[deepcopy(k, cache)] = deepcopy(v, cache)
|
||||||
|
end
|
||||||
|
|
||||||
|
return setmetatable(copy, getmetatable(orig))
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Returns a deep copy of the given object. Non-table objects are copied as
|
--- Returns a deep copy of the given object. Non-table objects are copied as
|
||||||
@ -54,11 +47,20 @@ end
|
|||||||
--- same functions as those in the input table. Userdata and threads are not
|
--- same functions as those in the input table. Userdata and threads are not
|
||||||
--- copied and will throw an error.
|
--- copied and will throw an error.
|
||||||
---
|
---
|
||||||
|
--- Note: `noref=true` is much more performant on tables with unique table
|
||||||
|
--- fields, while `noref=false` is more performant on tables that reuse table
|
||||||
|
--- fields.
|
||||||
|
---
|
||||||
---@generic T: table
|
---@generic T: table
|
||||||
---@param orig T Table to copy
|
---@param orig T Table to copy
|
||||||
|
---@param noref? boolean
|
||||||
|
--- When `false` (default) a contained table is only copied once and all
|
||||||
|
--- references point to this single copy. When `true` every occurrence of a
|
||||||
|
--- table results in a new copy. This also means that a cyclic reference can
|
||||||
|
--- cause `deepcopy()` to fail.
|
||||||
---@return T Table of copied keys and (nested) values.
|
---@return T Table of copied keys and (nested) values.
|
||||||
function vim.deepcopy(orig)
|
function vim.deepcopy(orig, noref)
|
||||||
return deepcopy(orig)
|
return deepcopy(orig, not noref and {} or nil)
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Gets an |iterator| that splits a string at each instance of a separator, in "lazy" fashion
|
--- Gets an |iterator| that splits a string at each instance of a separator, in "lazy" fashion
|
||||||
|
@ -158,7 +158,7 @@ end
|
|||||||
function M._version(version, strict) -- Adapted from https://github.com/folke/lazy.nvim
|
function M._version(version, strict) -- Adapted from https://github.com/folke/lazy.nvim
|
||||||
if type(version) == 'table' then
|
if type(version) == 'table' then
|
||||||
if version.major then
|
if version.major then
|
||||||
return setmetatable(vim.deepcopy(version), Version)
|
return setmetatable(vim.deepcopy(version, true), Version)
|
||||||
end
|
end
|
||||||
return setmetatable({
|
return setmetatable({
|
||||||
major = version[1] or 0,
|
major = version[1] or 0,
|
||||||
@ -228,7 +228,7 @@ function VersionRange:has(version)
|
|||||||
version = M.parse(version)
|
version = M.parse(version)
|
||||||
elseif getmetatable(version) ~= Version then
|
elseif getmetatable(version) ~= Version then
|
||||||
-- Need metatable to compare versions.
|
-- Need metatable to compare versions.
|
||||||
version = setmetatable(vim.deepcopy(version), Version)
|
version = setmetatable(vim.deepcopy(version, true), Version)
|
||||||
end
|
end
|
||||||
if version then
|
if version then
|
||||||
if version.prerelease ~= self.from.prerelease then
|
if version.prerelease ~= self.from.prerelease then
|
||||||
@ -298,7 +298,7 @@ function M.range(spec) -- Adapted from https://github.com/folke/lazy.nvim
|
|||||||
local semver = M.parse(version)
|
local semver = M.parse(version)
|
||||||
if semver then
|
if semver then
|
||||||
local from = semver
|
local from = semver
|
||||||
local to = vim.deepcopy(semver)
|
local to = vim.deepcopy(semver, true)
|
||||||
if mods == '' or mods == '=' then
|
if mods == '' or mods == '=' then
|
||||||
to.patch = to.patch + 1
|
to.patch = to.patch + 1
|
||||||
elseif mods == '<' then
|
elseif mods == '<' then
|
||||||
|
58
test/benchmark/deepcopy_spec.lua
Normal file
58
test/benchmark/deepcopy_spec.lua
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
local N = 20
|
||||||
|
|
||||||
|
local function tcall(f, ...)
|
||||||
|
local ts = vim.uv.hrtime()
|
||||||
|
for _ = 1, N do
|
||||||
|
f(...)
|
||||||
|
end
|
||||||
|
return ((vim.uv.hrtime() - ts) / 1000000) / N
|
||||||
|
end
|
||||||
|
|
||||||
|
local function build_shared(n)
|
||||||
|
local t = {}
|
||||||
|
local a = {}
|
||||||
|
local b = {}
|
||||||
|
local c = {}
|
||||||
|
for _ = 1, n do
|
||||||
|
t[#t + 1] = {}
|
||||||
|
local tl = t[#t]
|
||||||
|
for _ = 1, n do
|
||||||
|
tl[#tl + 1] = a
|
||||||
|
tl[#tl + 1] = b
|
||||||
|
tl[#tl + 1] = c
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return t
|
||||||
|
end
|
||||||
|
|
||||||
|
local function build_unique(n)
|
||||||
|
local t = {}
|
||||||
|
for _ = 1, n do
|
||||||
|
t[#t + 1] = {}
|
||||||
|
local tl = t[#t]
|
||||||
|
for _ = 1, n do
|
||||||
|
tl[#tl + 1] = {}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return t
|
||||||
|
end
|
||||||
|
|
||||||
|
describe('vim.deepcopy()', function()
|
||||||
|
local function run(name, n, noref)
|
||||||
|
it(string.format('%s entries=%d noref=%s', name, n, noref), function()
|
||||||
|
local t = name == 'shared' and build_shared(n) or build_unique(n)
|
||||||
|
local d = tcall(vim.deepcopy, t, noref)
|
||||||
|
print(string.format('%.2f ms', d))
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
run('unique', 50, false)
|
||||||
|
run('unique', 50, true)
|
||||||
|
run('unique', 2000, false)
|
||||||
|
run('unique', 2000, true)
|
||||||
|
|
||||||
|
run('shared', 50, false)
|
||||||
|
run('shared', 50, true)
|
||||||
|
run('shared', 2000, false)
|
||||||
|
run('shared', 2000, true)
|
||||||
|
end)
|
Loading…
Reference in New Issue
Block a user