Are variables automatically set to nil at the end of a script?

Please note: Modules were used in the example scripts for demonstrative purposes only, I’m well aware that modules are cached upon being required :slight_smile::+1:

As an example, let’s say I need to require some modules which are stored in a Folder named Modules, which is inside of ReplicatedStorage:

local ReplicatedStorage = game:GetService("ReplicatedStorage")

local modules = ReplicatedStorage.Modules
local module1 = require(modules.Module1)
local module2 = require(modules.Module2)

-- And then the rest of the code

Both the variables named ReplicatedStorage and modules are never used again within this script, so once they’ve served their purpose, they can be safely removed from memory

Now, I’m aware that local variables are automatically set to nil once the script exits the scope they were created in (unless a strong reference is held to the variable, like it being used within an active connection), but my question is: Since in this case the variables were declared within what essentially is the script’s global scope, is it still the case that they’re automatically set to nil if the conditions allow, or do we need to manually set them to nil as the example below demonstrates?

local module1, module2

do
	local ReplicatedStorage = game:GetService("ReplicatedStorage")

	local modules = ReplicatedStorage.Modules

	module1 = require(modules.Module1)
	module2 = require(modules.Module2)
end

-- And then, once again, the rest of the code

Or a more simple alternative to the above:

local ReplicatedStorage = game:GetService("ReplicatedStorage")

local modules = ReplicatedStorage.Modules
local module1 = require(modules.Module1)
local module2 = require(modules.Module2)

ReplicatedStorage = nil
modules = nil

-- And then, for the final time, the rest of the code
1 Like

You’re thinking about it wrong. The variables themselves aren’t set to nil, but the script is GC’d, which clears the memory used by the variables. You do not need to set variables to nil yourself.

Edit, further note: within a Lua VM (so an Actor), modules will stay around in memory. This is because when you require a module e.g. 3 times, it’s only evaulated once, and cached, and the following 2 requires just grab the cached value. So when you require a module, you can’t free that memory, unless you remove the actor that holds it (or shutdown the game for the global VM)

Wouldn’t that require the script to be destroyed first before the garbage collector can free it from memory? Instances, which scripts are, need to be destroyed and removed from any strong references first before the garbage collector can truly remove them from memory

I’ll go run a benchmark. Be right back with the results in ~5 min.

1 Like

Okay, back with some basic data. (to mods - separate post instead of edit for notification purposes <3)

Naturally, the usage is always lower after destroying. There is no difference between setting the variables to nil and not setting them (which supports my point, script is GC’d at its end even before it’s destroyed).

This makes sense: there isn’t much reason for a script to stay around in memory after it finishes. The GC picks up on this and poofs it out of existence.

Benchmark: a script starts another script. The latter script uses up some discernible amount of memory (I avoided using instances to ensure that we don’t get the engine secretly tracking stuff in the way). It measures the memory usage for scripts before starting the 2nd script, right before the data is created, and right after the second script finishes excution. For the “set vars to nil: true” run, 5 seconds are added to ensure that the GC could find time to run and clear up the variables.

Place with benchmark (use F8 to run without player), has an added delay of 5 sec before any benchmark starts: benchmark.rbxl (43,3 KB)

1 Like

Your benchmark helped me think of another way I can test the problem, I’ll work on it and report back with my results :slight_smile::+1:


@Spacerator Here’s how I ran my benchmark:

  1. Created a new empty baseplate (I used the classic one since it doesn’t have the lighting Instances and spawn location) and deleted its baseplate part

  2. I set Players service’s CharacterAutoLoads property to false

  3. Created a server Script named Test inside of ServerScriptService, which has Enabled set to false

debug.setmemorycategory("TEST")

local a = table.create(999_999, 2^64-1)
  1. Created a server Script (left the name default), which is also inside of ServerScriptService. This script was left enabled
debug.setmemorycategory("TEST")

local ServerScriptService = script.Parent

local test = ServerScriptService.Test

task.wait(30)
print("Start")

for _ = 1, 9999 do
	local test = Instance.fromExisting(test)
	test.Enabled = true
	test.Parent = ServerScriptService

	task.wait()
end

print("Stop")
  1. I ran the benchmark using F5, not F8 (which is why I disabled CharacterAutoLoads). This was an intentional decision in-order to be able to view the dev console before the benchmark starts

  2. I re-ran the benchmark, but this time I added script:Destroy() at the end of the script named Test

The memory category showed 0MB of memory at the start of both runs, and after the first benchmark concluded, 0.305MB was left in memory, while after the second benchmark concluded where Test was destroyed, memory returned to 0MB

1 Like

@Spacerator is correct in his first reply.

The script is Garbage collected before you would even know it’s done.
The end of a script, if it has no loops or otherwise running code, will kill the current thread in the VM.

No, variables are not set to nil; they simply no longer exist, at all.

They’re gone until that script runs again in the next instance of the game (or when it is cloned again, an example of cloning being for A Chassis drive/GUI scripts being cloned into the Client when you sit in the DriverSeat object.)

Modules are indeed cached by the Luau VM as this saves memory since the Luau VM can just add an instance of that cached data to any script that next requires it to that script’s LuaState (LuauState?)

Furthermore, in the case of your benchmark, you should actually intentionally delay setting Enabled until after you have parented a Script, but only Scripts (both Server/Local) since otherwise your code may (although unlikely) have odd behaviors.

To clarify, by no loops I suspect you mean an infinite loop (while true do) that wasn’t created within a new thread

If a variable no longer exists, its memory address points to NULL, which is essentially nil in Luau

Not once did I doubt that modules are cached when required, I think the main post is being misinterpreted in this case. I only used requiring modules as a random example of a script, rather than to focus on modules specifically

Re-ran both benchmarks, this time enabling the Test clone after parenting it to ServerScriptService, as per your recommendation, and the results were the exact same as before

This isn’t correct from a CS theory perspective. For a variable to not exist, it must mean that:

  1. nothing has access to it;
  2. it doesn’t occupy any space in memory.

If a variable’s contents are 0x00 (NULL), that means the variable still exists in memory. If a variable is set to nil, it still exists. If a variable doesn’t exist, we cannot possibly be talking about NULLs, because it has no value. It’s like trying to imagine nothing - it’s weird, it’s difficult, but the variable simply doesn’t exist anymore.

1 Like

NULL in C is directly equivalent to 0, which is technically a value even though it represents an empty quantity, so you’re correct in that sense

For a variable to truly cease to exist, it must be overwritten with the contents of a new variable, which can only safely happen once it has been freed, which happens either automatically once the script exits the scope the variable was declared in, or using the free function if the variable’s memory was allocated using the malloc function

To remove variable you need to remove or disable the script I guess. Anyway what is the point of this. You not increase/decrease any performance by this.

Just like it’s good practice to clean up after yourself in the real world, it’s also good practice to do so with scripts. A few megabytes there and there may seem insignificant, but when you think about it, freeing them from memory provides the same result as deleting textures or decals

Still arrays or variables takes nothing amount of space in memory. But even if u do there is no guarantee this memory will be freed because roblox do not even cleaning ram after game turn off. So u alawys need start client again. Especially it’s visible on high ram consuming games…

No. Even a RunService connection will still keep a LuaState alive, and prevent source Garbage Collection.
If there is still code that can be ran, or is running, that is reliant upon the script, then the script cannot be Garbage Collected by Lua’s GC.

This is general advice, the point is still mut and nil is an actual datatype that does take up memory within Lua and Luau.

The answer to this thread is: No.

When a script is garbage collected, there is nothing left of it. Variables no longer exist, as they have no reference nor value anymore, therefore there’s not even a chance for them to be “nil” as they cannot be accessed at all by any other code you may write. Even Roblox’s engine wouldn’t be able to recover the script’s memory sector unless it wasn’t freed, which it is.

Edit: And to cover your question:

No. This will actually end up taking up more memory than it will ever save you. You’re adding bloat to the bytecode after compiling, making this a pointless addition.

You’re actually not freeing anything. You have no control over memory management within Lua/Luau.

You can decrease the perceived overall memory by optimizing code and removing bloat from your codebase, but you cannot yourself do anything better than the Lua Garbage Collector does.

This is even further true with the version of Lua that Roblox created and uses, Luau, as they improved it to be more aggressive and thorough with its collection.

I’ve marked you as a solution due to this:

It helped me understand something important: So long as the script Instance exists, the script will always take up some memory, even if you’re careful about setting each variable to nil. This is because for a script to truly no longer exist, it must be destroyed, just like any other Instance. Essentially if you monitor the script, and confirm that no memory leaks are occurring, and keep its code clean and organized by creating only variables that are necessary and removing values from tables when they’re no longer needed, you’re good to go :slight_smile::+1:

https://lua.org/manual/5.1/manual.html#2.10

Thank you for being understanding.

As @Pixeluted provided, the Lua 5.1 Manual goes over a brief outline of the Garbage collector.

Further info is found for Luau here: Performance - Luau
It documents every single improvement the current version has in terms of performance (obviously not all or it gets pedantic with micro-optimizations and some macro-optimizations, but you get the idea.)

1 Like

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