math.Clamp inefficient!

math.Clamp is super inefficient and should be replaced.
Science Ho~! :eng101:

Suggested Replacement:

[lua]
function math.Clamp( _in, low, high)
return math.min( math.max( _in, low ), high ) )
end
[/lua]

Case Method:
Replace line 20 (local r=) with the function to be benchmarked.
Multiple versions of both the original function and the replacement function are used to make the science a bit more thorough.

r = x+1 – Control Case

r = sClamp(x,50,500) – a local of the original math.Clamp
r = xClamp(x,50,500) – a manual copy of math.Clamp
r = yClamp(x,50,500) – math.Clamp written in shorthand.
r = zClamp(x,50,500) – math.Clamp written in full, but using if() elseif() end

r = gClamp(x,50,500) – proposed replacement without locals
r = fClamp(x,50,500) – proposed replacement with locals

r = math.Clamp(x,50,500) – Special Case – The proposed replacement over math.Clamp as it would be seen and used in the math library.

Benchmarking Function:
[lua]
local sClamp = math.Clamp
local xClamp = function( _in, low, high ) if ( _in < low ) then return low end if ( _in > high ) then return high end return _in end
local yClamp = function(n,m,x) return (n<m&&m||(n>x&&x||n)) end
local zClamp = function(n,m,x) if(n<m) then return m elseif(n>x) then return x end return n end
local gClamp = function(_in,low,high) return math.min(math.max(_in,low),high) end
local mmin,mmax = math.min,math.max
local fClamp = function(_in,low,high) return mmin(mmax(_in,low),high) end
// function math.Clamp(_in,low,high) return math.min(math.max(_in,low,high)) end

for z=1,10 do
timer.Simple(z*30,function()
local bmtn,bmtr,bavg,bvn
local bnum=100000
local breps=50000
bavg,bvn,bmtr = {},0,SysTime()
for i=1,breps do
bmtn = SysTime()
for x=1,bnum do
local r = math.Clamp(x,50,500)
end
table.insert( bavg, SysTime()-bmtn )
end
bmtr=SysTime()-bmtr
for k,v in pairs(bavg) do bvn=bvn+v end bvn=bvn/#bavg
print( “test#”…z, "took: "…bmtr,“avg: “…string.format(”%0.19f”,bvn) )
end )
end
[/lua]

Case Results:

Findings:
The suggested replacement is faster than the current method by around 10x (or 1000%) and is almost as fast as the control case which is rather surprising. :science101:

pull request it

You should localise all functions while benchmarking. You should also test with JIT on and off to see if it’s dependent on it. The results without JIT:


Comparisons:	1.5353685200549
math.*:	3.7996769867249

Code:


local SysTime = SysTime
local print = print

local f1 = function(num, low, high)
	if (num < low) then
		return low
	end
	
	if (num > high) then
		return high
	end
	
	return num
end

local math_min = math.min
local math_max = math.max
local f2 = function(num, low, high)
	return math_min(math_max(num, low), high)
end

jit.off()

local flStart, flEnd
flStart = SysTime()

for i = 0, 99999999 do
	f1(0, 1, 10)
end

flEnd = SysTime()
print("Comparisons:", flEnd - flStart)

flStart = SysTime()

for i = 0, 99999999 do
	f2(0, 1, 10)
end

flEnd = SysTime()
print("math.*:", flEnd - flStart)

jit.on()

And with JIT:


Comparisons:	10.002317002023
math.*:	9.3324100481755

Code:


local SysTime = SysTime
local print = print

local f1 = function(num, low, high)
	if (num < low) then
		return low
	end
	
	if (num > high) then
		return high
	end
	
	return num
end

local math_min = math.min
local math_max = math.max
local f2 = function(num, low, high)
	return math_min(math_max(num, low), high)
end

local flStart, flEnd, n
flStart = SysTime()

for i = 0, 999999999 do
	n = f1(i % 5 - 1, 0, 2)
end

flEnd = SysTime()
print("Comparisons:", flEnd - flStart)

local n
flStart = SysTime()

for i = 0, 999999999 do
	n = f2(i % 5 - 1, 0, 2)
end

flEnd = SysTime()
print("math.*:", flEnd - flStart)

LuaJIT’s tracer will turn emit the equivalent of


function(num, low, high) return low end

for f1 because you always call it with the same arguments and the same branches are always taken.

Yep, edited with code that evens out the branch taken.