Optimizing the code as much as it is possible (A challenge for you)

Hello devs! Yesterday i was testing how much parts can be spawned without using any form of wait inside a loop. Turns out, its A LOT, after playing around ive decided to try to make code that creates 30000 parts run as fast as possible.

This post is about my journey on how i optimized the script to create 30000 parts more than 2.5 times faster!! And trust me, even though i am not ultra advanced programmer, some of these optimizations that i found i believe might shock some great devs

This is the first code:

task.wait(3.5)

local startTime = os.clock()

for i = 1,30000 do
	local x = Instance.new("Part", workspace)
	x.Position = Vector3.new(math.random(10,35), math.random(2,20),math.random(10,35))
	x.Size = Vector3.new(1,1,1)
	x.Anchored = true
end

print(os.clock()-startTime)

There are 2 rules:

  1. code has to spawn 30000 parts, no more, no less
  2. These 4 properties of the part have to stay the same, meaning you can optimize everything but you can’t change its position to Vector3.new(5,3,1) for example, Anchored has to stay the same, position also and its Parent has to be in the workspace

os.clock() is being used to measure time between start and end of the script so we could have as precise measure as possible.

The code right now runs in ~0.665s
image

  1. Setting Parent last!!!
    The first and best thing we can do is setting Part.Parent last.
    Insted of
local x = Instance.new("Part", workspace) ----

we do this

local x = Instance.new("Part")
x.Position = Vector3.new(math.random(10,35), math.random(2,20),math.random(10,35))
x.Size = Vector3.new(1,1,1)
x.Anchored = true
x.Parent = workspace ----

This alone decreases our time to about 0.41s
image
0.2 sec differece just by changing one line one code! But why is that?
How it works is part properties are being set before going to the workspace, Think of this like this…
you are in bed and you want to do eggs for breakfast. When setting parent first, you instantly stand up and the time starts, you go to the kitchen, then you are searching for the pan, then you are searching for the eggs, for seasonings etc etc

When you set Parent last, you think where everything is before leaving the bed, now that you know where everything is, you stand up, time starts, you go to the kitchen and you get everything much faster because you dont have to think and search where the stuff are.

  1. table.insert()
    Second optimization we can do is use table and table.insert()
task.wait(3.5)

local startTime = os.clock()

local parts = {}

for i = 1,30000 do
	local x = Instance.new("Part")
	x.Position = Vector3.new(math.random(10,35), math.random(2,20),math.random(10,35))
	x.Size = Vector3.new(1,1,1)
	x.Anchored = true
	
	table.insert(parts, x)
end

for i,v in parts do
	v.Parent = workspace
end

print(os.clock()-startTime)

went from ~0.41 to ~0.34
image

How this optimization works is it creates a parts and sets its properties, then it inserts it into a table that we created, then with the for i,v loop it sets all of the part parents to workspace

You might be thinking, why not put every property into the for i,v loop then? Well, for some reason this makes the code run faster by ~0.03s (it will be ~0.3s total) but later it will make the code slower.

code already runs almost 2 times faster, but were not stopping there!

  1. Creating folder for parts

Instead of putting all these parts into the workspace, we can store them into a folder that will be inside a workspace


The only thing in the code that we have to change is

for i,v in parts do
	v.Parent = workspace
end

to

for i,v in parts do
	v.Parent = workspace.Folder
end

image
We are almost reaching measures below 0.3 seconds!!

Second thing we can do is changing workspace.Folder to a variable

local folder = workspace.Folder

for i,v in parts do
	v.Parent = folder
end

With this, code got another minor optimisation and we did reach measures below 0.3 and even below 0.29!
image
We can change Vector3.new to Vector3.one which should run a little bit faster
current code:

task.wait(3.5)

local startTime = os.clock()

local parts = {}

local min = 10
local max = 35

local folder = workspace.Folder

for i = 1,30000 do
	local x = Instance.new("Part")
	x.Anchored = true
	x.Size = Vector3.one
	x.Position = Vector3.new(math.random(min, max), math.random(2,20),math.random(min,max))
	
	table.insert(parts, x)
end

for i,v in parts do
	v.Parent = folder
end


print(os.clock()-startTime)
  1. Shocking discoveries

Now, how code works is, it creates 30000 parts and it inserts them into a table. Then, that table is being looped to set all of 30000 parts parents into a folder that is inside a workspace.

We can make it even faster.

Right now it sets 30000 parts Parent inside a workspace.Folder, which means we are going back to first point of this post again but a little bit different, it sets parent to workspace.Folder 30000 times. Folder is inside a workspace, that is the problem.

We can create a Folder in a different service and then set Folder.Parent to workspace, but where do we put the folder?

I was testing putting folder into every service and then changing that folder parent to workspace and checking how fast it performs, ReplicatedFirst won.

These are the full results


These differences are too big to say its just statistical error so where we place the folder really matters

image
These are the results that we can get now, but we can still go quicker

I believe game:GetService() is slower than game.

When i changed it, the results were a little bit better but it might be just statistical error so take that with the grain of salt

image

When we create 2 new prints

we can see which part of code runs how fast
image
we can see that setting folder to workspace is the longest, second is 30000 parts loop and the lightest is setting them to folder.

This is where everything stands now, i dont know what else i can do, so here’s a challenge for you.

Make this run even faster than 0.244s and publish your code in the comments if you believe it is faster. I will post an update if i find a faster way. Thank you to anyone who read all of this

5 Likes

This was a very interesting experiment and topic. The best I got after 20-ish attempts (times 10 iterations) is 0.213… .

task.wait(2)


local sum = 0
local count = 0
local shortest = 10
local vOne = Vector3.one
local v3 = Vector3.new
local WS = game:GetService("Workspace")

local rand = math.random

for j = 1,10 do
	print(j)
	count += 1
	local parts = {}
	local folder = Instance.new("Folder")
	local min, max = 10,35
	local yMin, yMax = 2,20
	

	local start = os.clock()
	for _ = 1,30000 do
		local n = Instance.new("Part")
		n.Anchored = true
		n.Size = vOne
		n.Position = v3(rand(min, max), rand(yMin,yMax), rand(min, max))

		table.insert(parts, n)
	end

	print(os.clock()-start)

	for _,v in parts do
		v.Parent = folder
	end

	print(os.clock()-start)

	folder.Parent = WS
	local delta = os.clock()-start
	print(delta)
	sum += delta
	if delta < shortest then
		shortest = delta
	end
	folder:Destroy()
	task.wait(0.2)
end

print("-----------")
print("average:", sum/count)
print("best:",shortest)

Improvements:

  • Replaced the for loops’ index variables with underscores as they are unnecessary in our situation
  • Variables for Workspace and Vector3.new, Vector3.one and math.random

Discoveries:

  • Using Random.new() is a bit slower than math.random
local rand = Random.new()
...
rand:NextInteger(min, max)
  • Right after opening Studio, I’d clock 0.3 seconds or more, but the more I ran it the quicker it became until it eventually settled at the best result
  • As you’ve probably stated yourself, parenting the folder right after everything’s done to workspace is faster than doing it beforehand, which I find very surprising
  • Not destroying the folder at the end causes the next attempts to be a lot slower
3 Likes

Ive tried that also but i wasn’t sure if better results weren’t just lucky spawns

Yepp!! That’s because Random supports 64 bit integers (whole numbers up to 2^63)
While math.random supports up to 32 bit so whole numbers up to to 2^31

Yeah that shocked me too, but thats why i made this post, so people could learn

logical but i didn’t notice that because i was running only 1 attempt and leaving the game lol

Ive borrowed your code and changed some stuff
changed:

local WS = game:GetService("Workspace") to local WS = workspace

n.Position = v3(rand(min, max), rand(yMin,yMax), rand(min, max))--------
to

local rand = math.random

local min, max = 10,35
local yMin, yMax = 2,20

local randY = rand(yMin, yMax)
local randXZ = rand(min,max)


n.Position = v3(randXZ, randY, randXZ)--------

there is a little difference but not worth the notice really, ill try to otpimize it more

1 Like

Update:

I just crashed roblox and my progress is gone

1 Like

S**t happens :joy: perhaps check your autosaves folder, you never know

Update
I did some changes to the code for the past couple of days, been working hard to decrease the code by even a milisecond. What i was working on is adding coroutines and changing loops to make it create chunks of code instead of creating everything fully instantly, chunks didnt workbut coroutines did.

task.wait(5)

local WS = workspace

local min, max = 10,35
local yMin, yMax = 4,35

local rand = math.random

local v3 = Vector3.new
local vOne = Vector3.one

local sum = 0
local count = 40
local add = 0

local insert = table.insert
local newInstance = Instance.new

for _ = 1,count do
	local startTime = os.clock()
	
	local new = {}
	local folder = newInstance("Folder")
	
	local xLoop = coroutine.create(function()
		for _ = 1,30000 do
			local x = newInstance("Part")
			x.Anchored = true
			x.Position = v3(rand(min,max), rand(yMin,yMax), rand(min,max))
			x.Size = vOne
			
			table.insert(new,x)
		end
	end)
	coroutine.resume(xLoop)
	
	for _,v in new do
		v.Parent = folder
	end
	
	folder.Parent = WS
	
	local finish = os.clock()-startTime
	print(finish)
	print("done")
	sum += finish
	folder:Destroy()
	table.clear(new)
	task.wait(0.4)
end

print("whole average: ", sum/count)

It on average finishes the code in 0.222, earlier i had averages like 0.218, 0.219 but couldn’t recreate this.
image
Tho, this means that the difference between this code and the previous one made by @doomguy2164, is equal almost zero. Thats actually not the case.

image
This were my results with your latest script, i am not getting close to the average that i am getting with mine (x40 iterations), this script on average plays 10 miliseconds longer. It would be great if @ doomguy2164 you could check this code on your pc.

DIscoveries:

  • for i,v loop is faster than for i = loop

  • coroutine.resume is faster than coroutine.wrap

here are the averages that i got for combinations of coroutines and different loops, each average was ran once by 40 iterations so its not that precise

I think that will be it for now, i believe sub 0.2 might be possible but i dont know how yet, theres still a lot i didnt try with table. functions, maybe some combination of table.create, table.insert and table.copy could work, or seperation of loops into smaller pieces, ive tried that before but maybe someone could find something that i didn’t.

Whole code can be explained in 3 steps:

  1. creating 30000 parts and inserting them into a table
  2. setting table values parent to the folder (folder has to be Instance.new too to make it faster)
  3. setting folder parent to the workspace

ive tried connecting the steps by inserting step 2 into the step 1 loop, but it was as fast as before or maybe even slower,

these 3 steps made the code run 3 times faster than its original (from ~0.66s to ~0.22s). Overall this was fun challange, and propably everyone reading this learned something about optimization, including me. Use this if you are creating a bunch of instances, it might come in handy :slight_smile:

--!native
--!strict

local min: number, max: number = 10, 35
local yMin: number, yMax: number = 4, 35

local rand = math.random

local v3 = Vector3.new
local vOne = Vector3.one

local count: number = 40
local add: number = 0

local insert = table.insert
local newInstance = Instance.new

local coroutineWrap = coroutine.wrap

local clock = os.clock

local new: {Part} = {}
local folder: Folder = newInstance("Folder")
local startTime: number = clock()

coroutineWrap(function()
	for _ = 1, 30000 do
		local x: Part = newInstance("Part")
		x.Anchored = true
		x.Position = v3(rand(min, max), rand(yMin, yMax), rand(min, max))
		x.Size = vOne
		insert(new, x)
	end
end)()

for _, v in new do
	v.Parent = folder
end

folder.Parent = workspace

local endTime: number = clock() - startTime
print(endTime)

Can you try this one and let me know? I wonder if this will make it faster.

With the loop:

--!native
--!strict

local min: number, max: number = 10, 35
local yMin: number, yMax: number = 4, 35

local rand = math.random

local v3 = Vector3.new
local vOne = Vector3.one

local sum: number = 0
local count: number = 40
local add: number = 0

local insert = table.insert
local newInstance = Instance.new

local coroutineWrap = coroutine.wrap

local clock = os.clock

local new: {Part} = {}

for _ = 1, count do
	local folder: Folder = newInstance("Folder")
	local startTime: number = clock()
	
	coroutineWrap(function()
		for _ = 1, 15000 do
			local x: Part = newInstance("Part")
			x.Anchored = true
			x.Position = v3(rand(min, max), rand(yMin, yMax), rand(min, max))
			x.Size = vOne
			insert(new, x)
		end
	end)()
	coroutineWrap(function()
		for _ = 1, 15000 do
			local x: Part = newInstance("Part")
			x.Anchored = true
			x.Position = v3(rand(min, max), rand(yMin, yMax), rand(min, max))
			x.Size = vOne
			insert(new, x)
		end
	end)()

	for _, v in new do
		v.Parent = folder
	end
	
	folder.Parent = workspace
	
	local endTime: number = clock() - startTime
	sum += endTime
	
	folder:Destroy()
	table.clear(new)
	task.wait()
end

print(sum / count)

test this

--!native
--!optimize 2

local vec1 = vector.one

local obje = Instance.new
local vec3 = vector.create
local ran = math.random
local def = task.defer

local fd = obje('Folder')
fd.Parent = workspace

task.wait(3)

local startTime = os.clock()

def(function()
	for i = 1, 30000 do
		def(function()
			local v = obje('Part') v.Anchored, v.Size, v.Position = true, vec1, vec3(ran(-35, 35), ran(-20,20), ran(-35,35)) v.Parent = fd
		end)
	end
	print('real time spawn', os.clock()-startTime)
	
end)

print('fast time spawn', os.clock()-startTime)

Is this break the rules?
fast time spawn - 0.000027300000510876998
real time spawn - 0.060148599999847647
? lol

Why don’t you guys use the microprofiler to see exactly how many milliseconds every line of code takes up?

1 Like

after restarting pc and tested my script again, these are the results

been using 2 more os.clock() prints to measure time of each step and that was enough that i needed honestly

It didnt work. It makes the print run ultra fast but the actual time is ~0.45s that can be seen by simply wathcing the lag, its much longer than 0.06
image

Also, part position numbers should be the same as in my example

average ~0.231 with the loop version
and the best time i had with the non loop version was 0.23694089999997914

Best I got was 0.22, and averaged 0.23, very similar to yours.
Making Instance.new and table.insert variables completely flew over my head, so I doubled down on this and also made a variable for os.clock. I applied these changes to my initial script and peaked at 0.235, proving your method is an improvement. The explanation behind not reaching below 0.22 might be that now I have one browser tab opened and music in the background, I also noticed that tabbing off Studio drastically increases the times, which partially proves this (partially) relies on your PC.

1 Like


This was my post after restarting the pc, it does depends on pc and how many times the studio was played because later average went up to 0.22 and later up to 0.23+. These are the limits for me, idk what else i can do. I think thats gonna be it

1 Like

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