Table or Individual?

I’m working on a somewhat Parallax / InteriorMapping module and currently in the process of improving performance -
Just a general query but would it be better to Filter through a table using a single Connection i.e.

Camera:GetPropertyChangedSignal("CFrame"):Connect(function()
     for i,v in (table) do
     -- Handle individual changes.
     end
end

)

Or simply create a Connection for each individual ViewportFrame that’ll rely on this function?

The difference is negligible and should not matter (with multiple connections performing only slightly better as I expected), and connections should be very cheap on memory in the first place. Do note that running these tests by themselves without getting the average speed will kind of result in the first test (whether it is the single or multiple) almost always technically being slightly faster due to the creation of threads.


--!strict

local function single(accumulate: (seconds: number) -> ()): ()
	local mainThread: thread = coroutine.running()
	local singleBindableEvent: BindableEvent = Instance.new("BindableEvent")
	local callbacks: {(...any) -> ...any} = {}

	for i: number = 1000, 1, -1 do
		table.insert(callbacks, function(timeFired: number): ()
			local timeSince: number = os.clock() - timeFired
			accumulate(timeSince)
			
			print(`Callback #{i} fired. Took {string.format(`%.6f`, timeSince)} seconds.`)
			
			if i == 1 then
				coroutine.resume(mainThread)
			end
		end)
	end

	singleBindableEvent.Event:Connect(function(timeFired: number): ()
		for _: number, callback: (...any) -> ...any in callbacks do
			task.spawn(callback, timeFired)
		end
	end)

	singleBindableEvent:Fire(os.clock())
	
	coroutine.yield()
end

local function multiple(accumulate: (seconds: number) -> ()): ()
	local mainThread: thread = coroutine.running()
	local multipleBindableEvent: BindableEvent = Instance.new("BindableEvent")

	for i: number = 1, 1000 do
		local index: number = i
		
		multipleBindableEvent.Event:Connect(function(timeFired: number): ()
			local timeSince: number = os.clock() - timeFired
			accumulate(timeSince)
			
			print(`Connection #{i} fired. Took {string.format(`%.6f`, timeSince)} seconds.`)
			
			if i == 1 then
				coroutine.resume(mainThread)
			end
		end)
	end

	multipleBindableEvent:Fire(os.clock())
	
	coroutine.yield()
end

local function compare<A..., R...>(func1: (accumulate: (seconds: number) -> (), A...) -> R..., func2: (accumulate: (seconds: number) -> (), A...) -> R..., ...: A...): ()
	local func1AccumulatedTime: number = 0
	local func2AccumulatedTime: number = 0
	
	local function accumulate1(seconds: number): ()
		func1AccumulatedTime += seconds
	end
	
	local function accumulate2(seconds: number): ()
		func2AccumulatedTime += seconds
	end
	
	func1(accumulate1, ...)
	func2(accumulate2, ...)
	
	print(`Function #1 ran {string.format(`%.6f`, func1AccumulatedTime / func2AccumulatedTime)}x the speed of function #2`)
end

compare(single, multiple)

Running 1,000 callbacks with the array based (single) function ran on average ~0.96x-1.001x the speed of connection based (multiple) function.

Running 1,000 callbacks with the connection based (multiple) function ran on average ~0.99x-1.06x the speed of array based (single) function.

In conclusion: whichever one works better for you. The difference is so small that even with the extreme 1,000 simultaneous threads (a total of 2,000 threads for both tests), it doesn’t make that much of a speed difference.


I’m not too familiar on the specifics on how this works on the C++ side, but I assume Connect and Fire is the same thing as doing a manual callback table, except slightly faster due to being low-level.