PSA: Don't use Instance.new() with parent argument

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:

  1. local obj = Instance.new(‘type’); fill obj fields
  2. local obj = Instance.new(‘type’, parent); fill obj fields
  3. 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:

  1. You create a Part with the CFrame of 0,0,0 and a default Size
  2. You assign Size and CFrame fields and then parent the part to workspace
  3. ROBLOX sets up property change listeners for replication, rendering, etc.
  4. ROBLOX queues a replication of a new instance with Size 10,10,1 and CFrame x,y,z
  5. 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:

  1. You create a Part with the CFrame of 0,0,0 and a default Size, and parent it to workspace.
  2. ROBLOX sets up property change listeners for replication, rendering, etc.
  3. ROBLOX queues a replication of a new instance with default values for the Size/CFrame
  4. ROBLOX updates physics contacts between this part and whatever is at the origin
  5. You set the part Size to 10,10,1
  6. ROBLOX queues a replication change for the Size
  7. ROBLOX updates physics contacts between this part and whatever is at the origin and overlaps with the part given the new dimensions
  8. You set the part CFrame to x,y,z
  9. ROBLOX queues a replication change for the CFrame
  10. 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:

  1. 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.
  2. You redundantly create temporary hash tables which causes more work for the garbage collector
  3. You redundantly fill the temporary hash tables with key/value pairs which incurs a CPU cost
  4. 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.

586 Likes

P.S. I am considering a script analysis warning that would warn against using Instance.new with a parent argument - it’s tantalizing but so very wrong to use it.

84 Likes

I was thinking “It can’t be that much of a difference can it?”

Oh my. Time to go change some things.

236 Likes

Good to know! By a stroke of luck, I frequently use my own implementation of util.Create, but it already does parent assignment last so properties are what they should be when ChildAdded/etc fire.

12 Likes

Oh.

Oh no.

EDIT: Never mind, as far as I can tell I had been taking this into account the whole time. whew

18 Likes

I’ve always done this. Now I’m justified! :smiley:

97 Likes

Time to start CTRL+F-ing

51 Likes

Oh dear, I was under the assumption that none of this would happen until the next frame. Better get round to some optimization.

9 Likes

Not to sound grumpy, but there’s a reason not to look into how ROBLOX gears works.
From a selected few gears, not only is there this sort of code, but also issues with people getting stuck with it, it’s not cross-platform compatible, and even invites to collision-related exploits for people to teleport outside of game barriers. Bites lip in disgust - that is, recode it all

:confounded:

23 Likes

A lot of CoreGui uses this, should probably look into cleaning those up.

24 Likes

You just gave me flashbacks on all the times I used the second/third method

27 Likes

Rip me, I used this at every chance I could. Guess now would be a good time to stop

19 Likes

I almost feel like I need to find something worse just to upset zeuxcg further.

Perhaps

Instance.new("Object")
  .Transparency(0)
  .Size(Vector3.new(2, 2, 2))
  .Anchored(true)
  .CFrame(CFrame.new(5, 5, 5))
  .BrickColor(BrickColor.new("Bright red"))
.Parent(workspace)

Through the use of a metatable.

Enjoy suffering some more @zeuxcg

do
	local create = Instance.new
	local object, parentFunction, setFunction, tab
	tab = setmetatable({}, {
		__index = function (_, index)
			if index == "Parent" then
				return parentFunction
			else
				return function (value)
					return setFunction(index, value)
				end
			end
		end
	})
	parentFunction, setFunction = function (parent)
		object.Parent = parent
		return object
	end, function (index, value)
		object[index] = value
		return tab
	end
	Instance = {
	new = function (class)
		object = create(class)
		return tab
	end
	}
end
20 Likes

NEVER NEVER NEVER read ROBLOX gear code.

Please.

101 Likes


I would, if it didn’t cause… this to appear.

4 Likes

Oh my

Time to go change all my instancing I guess

5 Likes

Tbh, if your game needs this kind of optimization…

@zeuxcg wouldn’t it be possible for the engine to only start doing the queueing for new objects when the thread yields?
E.g.

local obj = Instance.new("Part",workspace) -- state: instantiation
-- set properties
-- state: still instantiation
coroutine.yield() -- or wait()
-- thread yielded, obj's state becomes "initial setup" or "doing the queueing"
12 Likes

Oh my. I had observed this but I hadn’t connected it to setting properties after setting Parent. Good to know.

I’ve added a warning to the wiki documentation for Instance.new.

6 Likes

Reading ROBLOX Gear code is how I got started with Lua in the first place.

32 Likes

Keep in mind in his example the first 100 parts have already been created when the second for loop runs, it would be better to test fresh both times.

I just tried it (restarting each time) and got 0.0027530193328857 and 0.10685229301453 (40x difference) over 100 iterations, which is still a lot.

9 Likes