FilterDescendantsInstances clones itself every time it's used

I was writing some raycasting code, and I realized that FilterDescentantsInstances shows interesting behavior when it’s assigned to something.

Here is the relevant part of the code:

local rcparams = RaycastParams.new()
rcparams.FilterType = Enum.RaycastFilterType.Blacklist

local chari: number

plrs.LocalPlayer.CharacterAdded:Connect(function(char)
	local t = rcparams.FilterDescendantsInstances
	local t2 = rcparams.FilterDescendantsInstances
	chari = #t + 1
	print(t, tostring(t), tostring(t2), tostring(rcparams.FilterDescendantsInstances)) --prints 3 different addresses!
	t[chari] = char
	print(t, t[chari])
	rcparams.FilterDescendantsInstances[chari] = t
	print(rcparams, rcparams.FilterDescendantsInstances)
end)

I printed 3 tables’ addresses using tostring. They’re all supposed to point to the same address. Interestingly, I got three different addresses!

Similar behavior can be observed when assigning to an index.

Is this a bug, or is it intentional?

I don’t want to call table.clone every time I need to modify FilterDescendantsInstances. Is there a more efficient way to do index assignment on it?

Any help is appreciated.

Searched it with different wording and found this:

Not sure if I should flag this, the questions aren’t exactly the same but pretty similar.

I don’t want to call table.clone every time I need to modify FilterDescendantsInstances . Is there a more efficient way to do index assignment on it?

I believe my reply at the bottom of the linked thread should resolve this, i.e; perform all of the assignments in a single expression.

RaycastParameters.FilterDescendantsInstances = {table.unpack(RaycastParameters.FilterDescendantsInstances), Instance1, Instance2, Instance3}
1 Like

Thanks for the answer, though now that I realized it, FilterDescendantsInstances seems to be cloned whenever it’s obtained, so using multiple statements taking advantage that behavior should be faster.

Why would cloning it more times than necessary be faster?

What I meant was something like this:

local t = rcparams.FilterDescendantsInstances --automatically cloned
table.insert(t, value)
rcparams.FilterDescendantsInstances = t
--cloned just once (?)

It seems to be cloned whenever it’s obtained, you can test it easily like this:

Unpacking and then cloning sounds less efficient to me.

so using multiple statements taking advantage that behavior should be faster.

I interpreted this as if you were performing multiple assignments to the table, in which case unpack would be quicker.

1 Like

I want to add something that is related and interesting.

I said that indexing a RaycastParams with FilterDescendantsInstances returns unrelated tables stored in different memory addresses. Interestingly, it’s possible to get similar behavior with much more common userdata, like events.

Code to test it:

local plrs = game:GetService("Players")

local function print_event(signal: RBXScriptSignal)
	print("\nEvent: " .. tostring(signal) ..
		"\nConnect, twice: " .. tostring(signal.Connect) .. " | " .. tostring(signal.Connect) ..
		"\nConnectParallel, twice: " .. tostring(signal.ConnectParallel) .. tostring(signal.ConnectParallel) ..
		"\nWait, twice: " .. tostring(signal.Wait) .. " | " .. tostring(signal.Wait)
	)
end

print_event(workspace.Destroying)
print_event(plrs.PlayerAdded)
print_event(plrs.PlayerRemoving)
print_event(workspace.DescendantRemoving)

This time it’s even more confusing - is a function being copied?

I decided to do a quick benchmark; because copying is a pretty expensive task, it would be noticeable.

local plrs = game:GetService("Players")
local t = table.create(65536)

local test_connectable = {
	Connect = function()
		--The key doesn't matter, Luau optimizes it anyways
	end,
}
local plradd = plrs.PlayerAdded

local clock = os.clock

local t1 = clock()
for i = 1, 65536 do
	t[i] = test_connectable.Connect
end
local t2 = clock()

warn("Done in " .. t2 - t1 .. " seconds!")

Results (Code executed 3 times for each test, 6 times in total):

RBXScriptSignal:
0.011972700012847781
0.011786399991251528
0.011438199959229678

Empty Function:
0.001651400001719594
0.0017411999870091677
0.0016474999720230699
About the addresses being different

If you aren’t sure about the addresses being different, here is a modified version:

local plrs = game:GetService("Players")
local t = table.create(65536)
local exact = {}
local match = 0

local test_connectable = {
	Connect = function()
		--The key doesn't matter, Luau optimizes it anyways
	end,
}
local plradd = plrs.PlayerAdded

local clock = os.clock

local t1 = clock()
for i = 1, 65536 do
	local x = test_connectable.Connect
	t[i] = x
	if exact[x] then match += 1 end
	exact[x] = true
end
local t2 = clock()

warn("Done in " .. t2 - t1 .. " seconds! Matches: " .. match)

Results:

Empty Function: 65535
RBXScriptSignal Connect: 0

First index will be unique, that’s why it’s not 65536.

The delay could have different explanations. I know that the function size doesn’t matter, I tried initializing Connect in test_connectable to plradd and it was just as fast. To me though, it looks like the function is somehow copied.

I also don’t know about how it’s copied. Perhaps the Connect function is written in C++ and for whatever reason the engine copies the instructions (copying instructions is not easy). Luau is compiled to bytecode and it could be related to bytecode. I don’t have a lot of knowledge about either of these. Again, I’ve got no idea about what’s happening / why it’s copied (if it even is).