Why does removing whitespace speed up your code?

Lua is an interpreted language, so the differences are literally the amount of time it takes to ignore stuff as it interprets on the fly. It’s so minuscule of a difference that it would be foolish to minify your code for performance reasons. Interesting observation though.

I’m not an expert on the Lua interpreter, so anyone feel free to correct me on that.

Code on the internet is minified for the sake of download size, not performance: Smaller files mean that clients will load scripts faster.

5 Likes

;o According to Lua (programming language) - Wikipedia

1 Like

In regular Lua it’s exactly the same bytecode, as it’s supposed to be.

@Crazyman32 since Lua gets parsed before being executed, it shouldn’t matter for that loop.
If you’re measuring time from inside the code, the code is already executing, so you can’t measure parsing difference.

Dumping the bytecode in a second

1 Like

Oh, maybe I’m getting confused with the LuaJIT compiler. Or I’m just wrong both ways. Anyway, that makes this much more interesting. I would be interested to see the dumped bytecode.

I think that while in studio, code is compiled into bytecode as it runs which is why the condensed version is faster, but I just tested in a live game on both server and client, and both versions ran at about the same speed on both client and server, so maybe Roblox precompiles like the other option Wikipedia says (same source) (which would also be more secure so exploiters can’t steal the lua text code directly):

Or that might be completely wrong because the running might be slightly delayed by other scripts while in the mean time the code is still compiled in parallel?

	local tick = tick
	local count = 1000000

	do
		local start=tick()
		for i=1,count do local a=i end
		print(tick()-start)
	end
	
	do
		local start = tick()
		for i = 1,count do
			local a = i
		end
		print(tick() - start)
	end
-- globals
0032  05000000           [01] getglobal  0   0        ; tick	
0036  41400000           [02] loadk      1   1        ; 1000000	
-- first block
003A  80000000           [03] move       2   0      	
003E  9C808000           [04] call       2   1   2  	
0042  C1800000           [05] loadk      3   2        ; 1	
0046  00018000           [06] move       4   1      	
004A  41810000           [07] loadk      5   2        ; 1	
004E  E0000080           [08] forprep    3   1        ; to [10]	
0052  C0010003           [09] move       7   6      	
0056  DF40FF7F           [10] forloop    3   -2       ; to [9] if loop	
005A  C5C00000           [11] getglobal  3   3        ; print	
005E  00010000           [12] move       4   0      	
0062  1C818000           [13] call       4   1   2  	
0066  0D810002           [14] sub        4   4   2  	
006A  DC400001           [15] call       3   2   1  	
-- second block
006E  80000000           [16] move       2   0      	
0072  9C808000           [17] call       2   1   2  	
0076  C1800000           [18] loadk      3   2        ; 1	
007A  00018000           [19] move       4   1      	
007E  41810000           [20] loadk      5   2        ; 1	
0082  E0000080           [21] forprep    3   1        ; to [23]	
0086  C0010003           [22] move       7   6      	
008A  DF40FF7F           [23] forloop    3   -2       ; to [22] if loop	
008E  C5C00000           [24] getglobal  3   3        ; print	
0092  00010000           [25] move       4   0      	
0096  1C818000           [26] call       4   1   2  	
009A  0D810002           [27] sub        4   4   2  	
009E  DC400001           [28] call       3   2   1  	
-- this is just at the end of every function
00A2  1E008000           [29] return     0   1 

Seems the same, although I just used string.dump in studio which might output something different than what’s actually used, or what happens isn’t visible in the bytecode.

I doubt it’s the bytecode that’s doing something, seems more like it’s another VM thing. It seems like roblox is internally doing something at the end of every line (not instruction/statement), unless the newline is in the middle of a statement or so:

do
	local start=tick()
	for i=1,1000000 do local a = i local b = i end
	print(tick()-start)
end

do
	local start = tick()
	for i = 1,1000000 do
		local a = i local b = i
	end
	print(tick() - start)
end

do
	local start = tick()
	for i = 1,1000000 do
		local a = i
		local b = i
	end
	print(tick() - start)
end

do
	local start = tick()
	for i = 1,1000000 do
		local a = i
		local b
		=
		i
	end
	print(tick() - start)
end
0.092844247817993
0.16371250152588
0.21370482444763
0.21733283996582

Roblox has done a lot to their VM, so good luck figuring out what’s really going on.

The time difference doesn’t happen on the commandline, so it’s probably something similar to Code speed inconsistency

4 Likes

Er, this seems completely random. But like @einsteinK did mention, Lua is compiled.

However, to clear some things up:
Lua is not “compiled on the go”, it’s made into a proto, and that’s what’s executed. Or in the case of server->client, proto to bytecode back to proto. There is no difference in the bytecode of each besides the lineinfo debug data, which could very well be the issue when being checked, but I doubt it.

Anyhow, here are my theories as to why this happens;

  • An inconsistency in the Roblox VM
  • Different usage of registers
  • lineinfo acting weird

I doubt Studio does this, but I know they also encrypt their instructions to an extent, which may slow things down.

TL;DR whitespace does not speed up code, and you shouldn’t abuse the lack of it

1 Like

Actually this thing and the “script __index” Code speed inconsistency are most likely one and the same. We assumed the latter happens every instruction, but it might be every line (that isn’t in the middle of a statement), which is quite easy to do with debug.sethook, but the internal C-sided version:


debug.sethook(print,'l')

local a=1

local b
=
1
line 4
line 8

Why would lua be checking every line / instruction? Isn’t everything that you run in a script going to be at the same security level?

I’m almost certain it’s the debugger having to interpret newlines, which is why it doesn’t do it in the command line

You can (although in a limited way) jump between levels/identities, aka let your function run indirectly at another identity. Not really the point here.

Like @zeuxcg once mentioned in the linked thread, “it might be the debugger getting the script variable”, along with the line number (an argument passed by sethook when using 'l') for e.g. line breakpoints.

EDIT: A bit like @Dekkonot mentioned (ninja’d), except it isn’t because it interprets newlines (after compiling at least)

1 Like

I’m not in a position to test it, mind you, but I’m willing to bet that if you turn off the debugger in studio or test it in an active game environment it will probably be a much smaller difference.

1 Like

Aaaaaand got it

I just disabled the Lua debugger (option in studio settings > Studio > Lua), restarted studio and ran my code again:

0.024985313415527
0.025152444839478
0.02470850944519
0.024580717086792

Still don’t know 100% of the details, but it seems to be the debugger doing something with the line sethook, most likely also requesting the script variable.

8 Likes

Stop trying to micro optimize your code by trying to make it easier for the parser. Whitespace has literally no bearing on speed.

3 Likes

While this is a bit of a tangent, yes, you shouldn’t optimize code to the point of unreadablility.

Roblox doesn’t compile code. That would take way to long and use more resources on roblox’s end.

Roblox more than certainly compiles code, otherwise string.dump wouldn’t be outputting Lua chunks.

1 Like

I don’t know how to respond to that except: yes, they definitely do compile code.

1 Like

No? If they used a compiler you wouldn’t be able to get live errors, which is something unique to an interpreter and not a compiler.

That’s… not how any of this works, at all. Unless you mean actual asm compiling vs parse compiling. But no, Roblox compiles. I suggest you take a look at the Lua compiler, which is a thing.