Skip to content

Optimize Material() when pngParameters ~= nil#2444

Open
GrayWolf64 wants to merge 2 commits intoFacepunch:masterfrom
GrayWolf64:GrayWolf64-patch-1
Open

Optimize Material() when pngParameters ~= nil#2444
GrayWolf64 wants to merge 2 commits intoFacepunch:masterfrom
GrayWolf64:GrayWolf64-patch-1

Conversation

@GrayWolf64
Copy link

  • Set string.find() no_patterns to true, possibly allowing luajit to compile this more efficiently
  • Eliminate small string creations resulting from ..

* Set `string.find()` no_patterns to true, possibly allowing luajit to compile this more efficiently
* Eliminate small string creations resulting from `..`
@GrayWolf64 GrayWolf64 changed the title Optimize Material() Optimize Material() when pngParameters ~= nil Feb 26, 2026
@callumok2004
Copy link
Contributor

callumok2004 commented Feb 26, 2026

Repitions: 10000

JIT Off
Function 1: 0.010201 seconds (New)
Function 2: 0.009835 seconds (Old)

JIT On
Function 1: 0.005701 seconds (New)
Function 2: 0.003761 seconds (Old)
local SysTime = SysTime
local ipairs = ipairs
local MsgC = MsgC

function GMODBenchmark(repeats, ...)
	local times = {}

	for i=1,5 do print() end

	for i = 1, 2 do
		if i == 1 then
			MsgC(Color(250, 129, 95), "[Benchmark] ", Color(35, 153, 189), "Loop 1: JIT Off\n")
		else
			MsgC(Color(250, 129, 95), "[Benchmark] ", Color(35, 153, 189), "Loop 2: JIT On\n")
		end

		for k, benchmark_function in ipairs({...}) do
			if i == 1 then
				jit.off()
				jit.flush()
			else
				jit.on()
			end

			local start_time = SysTime()
			for i = 1, repeats do
				benchmark_function()
			end

			local end_time = SysTime()
			times[k] = end_time - start_time
			MsgC(Color(250, 129, 95), "[Benchmark] ", Color(35, 153, 189), "Function ", Color(255, 255, 255), k, Color(35, 153, 189), ": ", Color(255, 255, 255), string.format("%.6f", times[k]), " seconds\n")
		end
	end
end

local mat_flags = { 0, 0, 0, 0, 0, 0, 0 }
local C_Material = Material

local function Material( name, words )
	if ( !words ) then return C_Material( name ) end

	mat_flags[ 1 ] = string.find( words, "vertexlitgeneric", 1, true ) and 1 or 0
	mat_flags[ 2 ] = string.find( words, "nocull", 1, true ) and 1 or 0
	mat_flags[ 3 ] = string.find( words, "alphatest", 1, true ) and 1 or 0
	mat_flags[ 4 ] = string.find( words, "mips", 1, true ) and 1 or 0
	mat_flags[ 5 ] = string.find( words, "noclamp", 1, true ) and 1 or 0
	mat_flags[ 6 ] = string.find( words, "smooth", 1, true ) and 1 or 0
	mat_flags[ 7 ] = string.find( words, "ignorez", 1, true ) and 1 or 0

	return C_Material( name, table.concat( mat_flags, "" ) )
end

local function OldMaterial( name, words )

	if ( !words ) then return C_Material( name ) end

	local str = (words:find("vertexlitgeneric") and "1" or "0")
	str = str .. (words:find("nocull") and "1" or "0")
	str = str .. (words:find("alphatest") and "1" or "0")
	str = str .. (words:find("mips") and "1" or "0")
	str = str .. (words:find("noclamp") and "1" or "0")
	str = str .. (words:find("smooth") and "1" or "0")
	str = str .. (words:find("ignorez") and "1" or "0")

	return C_Material( name, str )
end

GMODBenchmark(10000,
function()
	local mat = Material("models/debug/debugwhite", "vertexlitgeneric nocull alphatest mips noclamp smooth ignorez")
end,
function()
	local mat = OldMaterial("models/debug/debugwhite", "vertexlitgeneric nocull alphatest mips noclamp smooth ignorez")
end)

@noaccessl
Copy link
Contributor

You can make inline concat here, it'll be faster than the original separate concat and your table concat. https://gitspartv.github.io/LuaJIT-Benchmarks/#test18.

To directly call string.find is a good idea.

My testing image
Code
-- https://github.com/noaccessl/gluafuncbudget

gluafuncbudget.Configure( {
	usercpu = 'i7-3770 @ 3.40GHz';

	frames = 50;
	iterations_per_frame = 25;
} )

local C_Material = select( 2, debug.getupvalue( Material, 1 ) )

local function g_pre() return 'models/debug/debugwhite', "vertexlitgeneric nocull alphatest mips noclamp smooth ignorez" end

gluafuncbudget.Queue( {
	name = "Material: GMod Variant";

	pre = g_pre;
	main = function( name, words )
		local str = (words:find("vertexlitgeneric") and "1" or "0")
		str = str .. (words:find("nocull") and "1" or "0")
		str = str .. (words:find("alphatest") and "1" or "0")
		str = str .. (words:find("mips") and "1" or "0")
		str = str .. (words:find("noclamp") and "1" or "0")
		str = str .. (words:find("smooth") and "1" or "0")
		str = str .. (words:find("ignorez") and "1" or "0")

		C_Material( name, str )
	end
} )

local mat_flags = { 0, 0, 0, 0, 0, 0, 0 }

gluafuncbudget.Queue( {
	name = "Material: GrayWolf64's";

	pre = g_pre;
	main = function( name, words )
		mat_flags[ 1 ] = string.find( words, "vertexlitgeneric", 1, true ) and 1 or 0
		mat_flags[ 2 ] = string.find( words, "nocull", 1, true ) and 1 or 0
		mat_flags[ 3 ] = string.find( words, "alphatest", 1, true ) and 1 or 0
		mat_flags[ 4 ] = string.find( words, "mips", 1, true ) and 1 or 0
		mat_flags[ 5 ] = string.find( words, "noclamp", 1, true ) and 1 or 0
		mat_flags[ 6 ] = string.find( words, "smooth", 1, true ) and 1 or 0
		mat_flags[ 7 ] = string.find( words, "ignorez", 1, true ) and 1 or 0

		C_Material( name, table.concat( mat_flags, "" ) )
	end
} )


gluafuncbudget.Queue( {
	name = "Material: GMod Variant; inline concat";

	pre = g_pre;
	main = function( name, words )
		local str = (words:find("vertexlitgeneric") and "1" or "0")
		.. (words:find("nocull") and "1" or "0")
		.. (words:find("alphatest") and "1" or "0")
		.. (words:find("mips") and "1" or "0")
		.. (words:find("noclamp") and "1" or "0")
		.. (words:find("smooth") and "1" or "0")
		.. (words:find("ignorez") and "1" or "0")

		C_Material( name, str )
	end
} )

gluafuncbudget.Queue( {
	name = "Material: GMod Variant; inline concat + direct string.find";

	pre = g_pre;
	main = function( name, words )
		local str = (string.find(words, "vertexlitgeneric") and "1" or "0")
		.. (string.find(words, "nocull") and "1" or "0")
		.. (string.find(words, "alphatest") and "1" or "0")
		.. (string.find(words, "mips") and "1" or "0")
		.. (string.find(words, "noclamp") and "1" or "0")
		.. (string.find(words, "smooth") and "1" or "0")
		.. (string.find(words, "ignorez") and "1" or "0")

		C_Material( name, str )
	end
} )

@noaccessl
Copy link
Contributor

@callumok2004

You should've done something like

local C_Material = select( 2, debug.getupvalue( Material, 1 ) )

instead of just

local C_Material = Material

Aren't you getting the gmod's lua function?

@GrayWolf64
Copy link
Author

@noaccessl Does inlined concat actually mean using a fixed number of .. like local f = a1 .. a2 .. a3 .. a4? I am surprised to know this!

I'm also curious if the following one is the most performant:

--
-- Send C the flags for any materials we want to create
--
local mat_flags = {}
for a=0,1 do -- 128 different flags in total lol
    mat_flags[a] = {}
    for b=0,1 do
        mat_flags[a][b] = {}
        for c=0,1 do
            mat_flags[a][b][c] = {}
            for d=0,1 do
                mat_flags[a][b][c][d] = {}
                for e=0,1 do
                    mat_flags[a][b][c][d][e] = {}
                    for f=0,1 do
                        mat_flags[a][b][c][d][e][f] = {}
                        for g=0,1 do
                            mat_flags[a][b][c][d][e][f][g] = string.format("%d%d%d%d%d%d%d", a, b, c, d, e, f, g)
                        end
                    end
                end
            end
        end
    end
end
local C_Material = Material
function Material( name, words )

	if ( !words ) then return C_Material( name ) end

	return C_Material( name, mat_flags[string.find( words, "vertexlitgeneric", 1, true ) and 1 or 0]
                                      [string.find( words, "nocull", 1, true ) and 1 or 0]
                                      [string.find( words, "alphatest", 1, true ) and 1 or 0]
                                      [string.find( words, "mips", 1, true ) and 1 or 0]
                                      [string.find( words, "noclamp", 1, true ) and 1 or 0]
                                      [string.find( words, "smooth", 1, true ) and 1 or 0]
                                      [string.find( words, "ignorez", 1, true ) and 1 or 0])

end

This is over optimization I think and the setup code looks quite ugly. And I don't know if my optimization is really necessary because on the wiki page the Material() is already warned to be expensive in loops and people really shouldn't use this in loops.

@robotboy655 robotboy655 added the Enhancement The pull request enhances current functionality. label Feb 27, 2026
@noaccessl
Copy link
Contributor

Looks bulky. Plus it preoccupies some memory.

But ultimately, you're trying to optimize the "flags construction" part. And we have to remember, it still comes down to calling C_Material that's not that fast anyways. So let's look more closely at that part?

Here's my personal testing
image
image

Code
gluafuncbudget.Configure( {
	usercpu = 'Intel(R) Core(TM) i7-3770 CPU @ 3.40GHz';

	frames = 50;
	iterations_per_frame = 1e4;

	measure_unit --[[= 'ms']];
	digit --[[= 5]];

	comparison_basis --[[= 'average']];

	shown_metrics --[[= 'median min max average stddev avgfps']];

	-- jit_off_all = true
} )

local string_find = string.find

local function fnGetWords()

	return "vertexlitgeneric nocull alphatest mips noclamp smooth ignorez"

end

gluafuncbudget.Queue( {
	name = "Flags Construction: GMod's";
	standard = true;

	pre = fnGetWords;
	main = function( words )

		local str = ( words:find( 'vertexlitgeneric' ) and '1' or '0' )
		str = str .. ( words:find( 'nocull' ) and '1' or '0' )
		str = str .. ( words:find( 'alphatest' ) and '1' or '0' )
		str = str .. ( words:find( 'mips' ) and '1' or '0' )
		str = str .. ( words:find( 'noclamp' ) and '1' or '0' )
		str = str .. ( words:find( 'smooth' ) and '1' or '0' )
		str = str .. ( words:find( 'ignorez' ) and '1' or '0' )

	end
} )

gluafuncbudget.Queue( {
	name = "Flags Construction: GMod's + direct string.find";

	pre = fnGetWords;
	main = function( words )

		local str = ( string_find( words, 'vertexlitgeneric', 1, true ) and '1' or '0' )
		str = str .. ( string_find( words, 'nocull', 1, true ) and '1' or '0' )
		str = str .. ( string_find( words, 'alphatest', 1, true ) and '1' or '0' )
		str = str .. ( string_find( words, 'mips', 1, true ) and '1' or '0' )
		str = str .. ( string_find( words, 'noclamp', 1, true ) and '1' or '0' )
		str = str .. ( string_find( words, 'smooth', 1, true ) and '1' or '0' )
		str = str .. ( string_find( words, 'ignorez', 1, true ) and '1' or '0' )

	end
} )

gluafuncbudget.Queue( {
	name = "Flags Construction: GMod's + direct string.find + inline concat";

	pre = fnGetWords;
	main = function( words )

		local str = ( string_find( words, 'vertexlitgeneric', 1, true ) and '1' or '0' )
		  .. ( string_find( words, 'nocull', 1, true ) and '1' or '0' )
		    .. ( string_find( words, 'alphatest', 1, true ) and '1' or '0' )
		      .. ( string_find( words, 'mips', 1, true ) and '1' or '0' )
		        .. ( string_find( words, 'noclamp', 1, true ) and '1' or '0' )
		          .. ( string_find( words, 'smooth', 1, true ) and '1' or '0' )
		            .. ( string_find( words, 'ignorez', 1, true ) and '1' or '0' )

	end
} )

local _mat_flags = { 0, 0, 0, 0, 0, 0, 0 }
local table_concat = table.concat

gluafuncbudget.Queue( {
	name = "Flags Construction: table concat";

	pre = fnGetWords;
	main = function( words )

		_mat_flags[1] = string_find( words, 'vertexlitgeneric', 1, true ) and 1 or 0
		_mat_flags[2] = string_find( words, 'nocull', 1, true ) and 1 or 0
		_mat_flags[3] = string_find( words, 'alphatest', 1, true ) and 1 or 0
		_mat_flags[4] = string_find( words, 'mips', 1, true ) and 1 or 0
		_mat_flags[5] = string_find( words, 'noclamp', 1, true ) and 1 or 0
		_mat_flags[6] = string_find( words, 'smooth', 1, true ) and 1 or 0
		_mat_flags[7] = string_find( words, 'ignorez', 1, true ) and 1 or 0

		table_concat( _mat_flags, '' )

	end
} )

local mat_flags = {}
for a = 0,1 do
	mat_flags[a] = {}
	for b = 0,1 do
		mat_flags[a][b] = {}
		for c = 0,1 do
			mat_flags[a][b][c] = {}
			for d = 0,1 do
				mat_flags[a][b][c][d] = {}
				for e = 0,1 do
					mat_flags[a][b][c][d][e] = {}
					for f = 0,1 do
						mat_flags[a][b][c][d][e][f] = {}
						for g = 0,1 do
							mat_flags[a][b][c][d][e][f][g] = string.format( '%d%d%d%d%d%d%d', a, b, c, d, e, f, g )
						end
					end
				end
			end
		end
	end
end

gluafuncbudget.Queue( {
	name = "Flags Construction: Manifold Lookup";

	pre = fnGetWords;
	main = function( words )

		local str = mat_flags[string_find( words, 'vertexlitgeneric', 1, true ) and 1 or 0]
		  [string_find( words, 'nocull', 1, true ) and 1 or 0]
		    [string_find( words, 'alphatest', 1, true ) and 1 or 0]
		      [string_find( words, 'mips', 1, true ) and 1 or 0]
		        [string_find( words, 'noclamp', 1, true ) and 1 or 0]
		          [string_find( words, 'smooth', 1, true ) and 1 or 0]
		            [string_find( words, 'ignorez', 1, true ) and 1 or 0]

	end
} )

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Enhancement The pull request enhances current functionality.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants