A question about event functions and memory usage

Currently, I’m unsure whether connecting anonymous functions to events uses less memory than connecting regular functions, so for example does doing this:

local Players = game:GetService("Players")

local function onPlayerAdded()
	
end

Players.PlayerAdded:Connect(onPlayerAdded)

end up using more memory overall than doing this:

local Players = game:GetService("Players")

Players.PlayerAdded:Connect(function()
	
end)

I’m also unsure as to whether the garbage collector cleans up the anonymous function per successful execution per event activation, which would have the benefit of temporarily removing the function from memory in the case that there are long periods of time before each event activation

Edit: This is a test I ran (and forgot to mention :sweat_smile:) to see if there’s a difference before making this topic:

You can test it out yourself now with the new memory debug tools.

Example:

1 Like

I don’t know the ins and outs of memory but these do the exact same thing and I doubt theres a memory difference, however I imagine if there is so then it is so miniscule you don’t need to worry about it whatsoever.

If you’re concerned about memory usage or noticing memory problems in your system then this more than likely this isn’t the problem. Focus on disconnecting unused events and not storing unnecessary variables

2 Likes

I’m unfamiliar with doing tests like this, will it need to be done within a loop for the test to be valid like so:

debug.setmemorycategory("TEST")
game:GetService("RunService").Heartbeat:Connect(function()
	local Player = game:GetService("Players")

	Player.PlayerAdded:Connect(function()
		print(true)
	end)
end)

Also, should I disconnect the PlayerAdded connection afterward or will it affect the result of the test?

Edit: @dthecoolest I ran these tests using the method you provided and the results are quite strange. The first test was:

debug.setmemorycategory("TEST")
game:GetService("RunService").Heartbeat:Connect(function()
	local Players = game:GetService("Players")

	local connection
	connection = Players.PlayerAdded:Connect(function()
		print(true)
	end)

	connection:Disconnect()
	connection = nil
end)

Which resulted in the value MB starting at 0.044 then gradually decreasing by 0.001
The second test was:

debug.setmemorycategory("TEST")
game:GetService("RunService").Heartbeat:Connect(function()
	local Players = game:GetService("Players")

	local function onPlayerAdded()
		print(true)
	end

	local connection
	connection = Players.PlayerAdded:Connect(onPlayerAdded)

	connection:Disconnect()
	connection = nil
end)

Which resulted in the value MB of 0.043 and also decreased gradually by 0.001

Hopefully I have done the tests correctly, and from what it seems the difference does seem to be quite minimal, although for me it was unexpected that the second test resulted in a very slightly smaller value so I’d like you to confirm if possible

@noxiirity I think these new tests would interest you too :slightly_smiling_face:

1 Like

Thankfully this question was done out of curiosity instead of necessity :sweat_smile:

I did as much research as I could before I made this topic, but for me at least the information I found were a bit inconclusive

1 Like

@dthecoolest @noxiirity
Something I forgot to mention is that I did a test using gcinfo() before I made this topic both with and without using a task.wait() before printing the result and in all cases the result was equal to 83, although I’m unsure as to the accuracy of the result of this test

Here’s how I did the test:

local Player = game:GetService("Players")

Player.PlayerAdded:Connect(function()
	print(true)
end)

print(gcinfo())
local Player = game:GetService("Players")

local function onPlayerAdded()
	print(true)
end

Player.PlayerAdded:Connect(onPlayerAdded)

print(gcinfo())
local Player = game:GetService("Players")

Player.PlayerAdded:Connect(function()
	print(true)
end)

task.wait()
print(gcinfo())
local Player = game:GetService("Players")

local function onPlayerAdded()
	print(true)
end

Player.PlayerAdded:Connect(onPlayerAdded)

task.wait()
print(gcinfo())

I’ll add a link to this comment on the topic as well

In the example provided I can’t imagine there is any difference.

Whether it is an anonymous function or one with a local reference it would still take up the same memory allocation.
The local reference simply allows you to refer to the same function from elsewhere in your code.

If you were to copy and paste the same anonymous function multiple times to different connections, each one would be considered separate and take more space.
But create multiple connections to the same local function and it wouldn’t take up any more than just creating one, as each connection would point to the same memory address.

(Sorry I don’t have any data, just working from theory!!)

1 Like

Although I’m unsure as to what happens to anonymous functions connected to events after they run successfully, are they temporarily removed from memory until the event is fired again? If yes, then there would be an overall slight benefit to using anonymous functions rather than regular function when dealing with infrequently activated events since a function isn’t kept in memory until needed

This is the question I’m personally having trouble testing the most :slightly_frowning_face:

Aha, true, I neglected that bit…

The GC won’t collect stuff if a reference is held, and the event holds the reference, so the function won’t be gc’d until the event is disconnected or otherwise destroyed.

(This is one of the common causes of memory leaks, and there are quite a few topics about those)

2 Likes

Short answer: no.

Thought experiment: How do you expect it to be able to call a function that’s not in memory?
The anonymous function is never garbage collected because it’s still connected, i.e. has a reference to it.

1 Like

I see, so if I’m understanding correctly a function that’s a parameter of a connect method such as :Connect or :Once or :ConnectParallel is considered a strong reference until the connection is disconnected?

That is my understanding, yes, except for perhaps :once, as I think that disconnects itself after use.
Edit: more accurately, :Once would also be a strong reference, but disconnects itself allowing it to be gc’d.

3 Likes

Adding to what others have said, here is a very simple method to prove that the connected function in fact remains as one function with the same memory address.

local be = Instance.new("BindableEvent")
be.Event:Connect(function()
	print(debug.info(1,"f"))
end)

be:Fire(); be:Fire(); be:Fire() -- same memory address :)

Why?

Functions in lua are passed by reference (just like tables). When we declare a new function, it gets a place in memory and keeps it so long as an adequately strong reference exists, be it a variable or something else.

We tell methods like :Connect to call the function we provide as an argument. Passing functions around in our code leads to a relatively similar situation (except if the compiler in some cases inlines the function as an optimisation tactic).

local function Post(messenger: (string) -> ()): ()
	messenger("Hello")
end
Post(function(message)
	print(message)
end)

That is an interesting take, but the engine and LuaVM have to retain the information about what the function does and possibly how it accesses the environment outside the function. Creating new functions for each call would spend a lot more resources, even if the callback was called seldom if ever.

3 Likes

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.