I’ve discovered a pretty bad performance issue in one of top games that has to do with Instance.new and wanted to write about this, since this is not obvious unless you know the system inside out.
Tthere are several ways to create a ROBLOX object in Lua:
- local obj = Instance.new(‘type’); fill obj fields
- local obj = Instance.new(‘type’, parent); fill obj fields
- local obj = util.Create(‘type’, { field1 = value1, … })
If you care at all about performance, please only use the first option - I will explain why.
In ROBLOX, objects start as detached from the game object and are left in this state up until you parent them to something that’s already a descendant of game. When the object is detached (meaning, .Parent = nil or maybe object is parented to some other object that is not parented to anything), changing the state of the object is very cheap - you’re just changing bytes inside some memory block.
Once an object is attached to game, a lot of internal ROBLOX systems start listening to property changes on the object and updating various internal data structures for the change to take effect. These updates can involve queueing changes for replication, updating physics contact state, queueing rendering state changes etc.
It’s thus important that when you create the object, the initial object field assignment is correctly classified as “initial setup” as opposed to “changing state of the existing object” - and the differentiator for this is .Parent, or rather the object being a descendant of game. For this reason, .Parent assignment should go after all property assignments during the object creation. You most likely want to connect all signals after that to make sure they are not dispatched during creation - so the optimal sequence is:
A. Instance.new
B. Assign properties
C. Assign Parent
D. Connect signals
Now let’s go over the options.
Option 1 allows you to be explicit about the order. You should make sure .Parent assignment is the last field that you assign - and in this case all property changes before that are treated as fast updates. You should use this. Example:
local wall = Instance.new("Part")
wall.Size = Vector3.new(10, 10, 1)
wall.CFrame = CFrame.new(x, y, z)
wall.Parent = workspace.Effects
This makes sure Size/CFrame updates are super fast, and when the object is inserted into the game, it’s in its final state - so we do the minimal amount of work necessary to initialize it. Here’s what actually happens:
- You create a Part with the CFrame of 0,0,0 and a default Size
- You assign Size and CFrame fields and then parent the part to workspace
- ROBLOX sets up property change listeners for replication, rendering, etc.
- ROBLOX queues a replication of a new instance with Size 10,10,1 and CFrame x,y,z
- ROBLOX updates physics contacts between this part and whatever is at the position x,y,z given the size 10,10,1
Option 2 explicitly assigns Parent as the first thing, which is the worst pattern you can use. Consider the version of the code above with Instance.new called with parent argument:
local wall = Instance.new("Part", workspace.Effects)
wall.Size = Vector3.new(10, 10, 1)
wall.CFrame = CFrame.new(x, y, z)
You may think that this is faster because you saved one assignment, but it’s actually much slower in this case. Here’s what happens:
- You create a Part with the CFrame of 0,0,0 and a default Size, and parent it to workspace.
- ROBLOX sets up property change listeners for replication, rendering, etc.
- ROBLOX queues a replication of a new instance with default values for the Size/CFrame
- ROBLOX updates physics contacts between this part and whatever is at the origin
- You set the part Size to 10,10,1
- ROBLOX queues a replication change for the Size
- ROBLOX updates physics contacts between this part and whatever is at the origin and overlaps with the part given the new dimensions
- You set the part CFrame to x,y,z
- ROBLOX queues a replication change for the CFrame
- ROBLOX updates physics contacts between this part and whatever is at the position x,y,z given the size 10,10,1
Notice how much more work we’re doing in this case! We are updating physics contacts completely redundantly multiple times - some of these updates may be very expensive - and also queueing useless replication changes which results in inefficient CPU/bandwidth usage.
Option 3 is arguably the worst:
local Create = LoadLibrary("RbxUtility").Create
local wall = Create "Part" {
Size = Vector3.new(10, 10, 1);
CFrame = CFrame.new(x, y, z);
Parent = workspace.Effects
}
It surely looks nice. However:
- You don’t even know which order the updates execute in, unless your Create implementation special cases Parent. This means the code may be fast or slow - you don’t know! All hail hash tables.
- You redundantly create temporary hash tables which causes more work for the garbage collector
- You redundantly fill the temporary hash tables with key/value pairs which incurs a CPU cost
- When filling object with values, you do dynamic property updates by name, which prevents some optimizations we’re planning to introduce
You can make it a tiny bit better by making sure your Create implementation is aware that Parent should be set last - you should at least do that if you really want to use this syntax. (RbxUtility.Create does not do that!) My recommendation though is to avoid extra allocations and extra CPU overhead and just stick to the manual approach - option 1 is the fastest.