User:Braindawg/performance
This page includes tips and tricks for optimizing VScript performance. All of these performance tests were done in and many can be used in other
-based titles. Your mileage may vary in VScript supported games prior to the SDK update (
).


VScript Performance Tips
Folding Functions
Folding functions in the context of VScript means folding them into the root table. This only needs to be done once on script load, and is recommended for functions that are commonly used.
Benchmark

::ROOT <- getroottable()
local tofold = [ "NetProps", "Entities", "EntityOutputs", "NavMesh", "Convars" ]
// these are defined multiple times in other classes, skip to avoid conflicts
local foldblacklist = {
IsValid = true
GetName = true
GetCenter = true
}
// fold every class into the root table for performance
foreach( method_class in tofold)
foreach( k, v in ROOT[method_class].getclass() )
if ( !( k in foldblacklist ) && !( k in ROOT ) )
ROOT[k] <- ROOT[method_class][k].bindenv( ROOT[method_class] )
Result:
Configuration | Results |
---|---|
Unfolded
|
0.1439ms
|
Folded
|
0.0999ms
|
Constants
Folding Constants
Similar to folding functions, folding pre-defined Constant values into the constant table (or the root table) increases performance significantly.
Benchmark
local j = 0
for (local i = 1; i <= Constants.Server.MAX_EDICTS; i++)
j++
const MAX_EDICTS = 2048
local e = 0
for (local i = 1; i <= MAX_EDICTS; i++)
e++
Result:
Configuration | Results |
---|---|
Unfolded
|
0.1127ms
|
"Folded"
|
0.0423ms
|
Root table vs Constant table
Unlike values inserted into the root table, values inserted into the constant table are cached at the pre-processor level. What this means is, while accessing them is faster, it may not be feasible to fold your constants into the constant table if they are folded in the same script file that references them.
If you intend to insert values into the constant table, you must do this before any other scripts are executed, otherwise your script will not be able to read any values from it.
Benchmark
::ROOT_VALUE <- 2
const CONST_VALUE = 2
for (local i = 0; i <= 10000; i++)
i += CONST_VALUE
for (local i = 0; i <= 10000; i++)
i += ROOT_VALUE
Result:
Configuration | Results |
---|---|
Constant
|
0.0767ms
|
Root
|
0.1037ms
|
String Formatting
Squirrel supports two main ways to format strings: Concatenation using the + symbol, and the format()
function. format()
is significantly faster than concatenation.

.tostring()
and format it as a stringToKVString
the TOKVString()
VScript function takes a Vector/QAngle and formats the values into a string. For example, Vector(0, 0, 0).ToKVString()
would be "0 0 0"
On top of being less cumbersome to write, ToKVString()
is marginally faster than format()
.
When formatting multiple ToKVString()
outputs into a new string, concatenation may be faster.
Benchmark
local mins = Vector(-1, -2, -3)
local maxs = Vector(1, 2, 3)
local kvstring = ""
for (local i = 0; i < 10000; i++)
kvstring = mins.x.tostring() + "," + mins.y.tostring() + "," + mins.z.tostring() + "," + maxs.x.tostring() + "," + maxs.y.tostring() + "," + maxs.z.tostring()
for (local i = 0; i < 10000; i++)
kvstring = format("%g,%g,%g,%g,%g,%g", mins.x, mins.y, mins.z, maxs.x, maxs.y, maxs.z)
for (local i = 0; i < 10000; i++)
kvstring = format("%s %s", mins.ToKVString(), maxs.ToKVString())
for (local i = 0; i < 10000; i++)
kvstring = mins.ToKVString() + " " + maxs.ToKVString()
Result:
Configuration | Results |
---|---|
concat
|
39.0847ms
|
format
|
24.0123ms
|
ToKVString
|
19.9377ms
|
ToKVString + concat
|
18.5166ms
|
Spawning Entities
in VScript, there are four common ways to spawn entities:
- CreateByClassname + DispatchSpawn
- SpawnEntityFromTable
- SpawnEntityGroupFromTable
- point_script_template entity + AddTemplate
CreateByClassname + DispatchSpawn vs SpawnEntityFromTable
In general, performance is not a major concern when spawning entities. In special circumstances though, you may need to spawn and kill a temporary entity in an already expensive function. A notable example of an entity that would need this is trigger_stun. This entity will not attempt to re-stun the same player multiple times, so it is not possible to spawn a single entity and repeatedly fire StartTouch/EndTouch on the same target.
In situations like this, CreateByClassname + DispatchSpawn is roughly 4x faster in comparison to SpawnEntityFromTable
.
Benchmark
trigger_stun = SpawnEntityFromTable("trigger_stun",
{
stun_type = 2,
stun_effects = true,
stun_duration = 3,
move_speed_reduction = 0.1,
trigger_delay = 0.1,
spawnflags = 1,
})
trigger_stun = Entities.CreateByClassname("trigger_stun")
trigger_stun.KeyValueFromInt("stun_type", 2)
trigger_stun.KeyValueFromInt("stun_effects", 1)
trigger_stun.KeyValueFromFloat("stun_duration", 3.0)
trigger_stun.KeyValueFromFloat("move_speed_reduction", 0.1)
trigger_stun.KeyValueFromFloat("trigger_delay", 0.1)
trigger_stun.KeyValueFromInt("spawnflags", 1)
DispatchSpawn(trigger_stun)
Result:
Configuration | Results |
---|---|
SpawnEntityFromTable
|
0.0428ms
|
CreateByClassname
|
0.0156ms
|
SpawnEntityGroupFromTable vs point_script_template
When spawning multiple entities at the same time, it is more efficient to use either SpawnEntityGroupFromTable
or a point_script_template entity. These options also have the added benefit of respecting parent hierarchy, so the parentname
keyvalue works as intended.
point_script_template is both more flexible and faster. SpawnEntityGroupFromTable has several major limitations in comparison to point_script_template, and is generally not recommended. See the VScript documentation for more details on how to use point_script_template.
Benchmark
//spawn origins are right outside of bigrock spawn
SpawnEntityGroupFromTable({
[0] = {
func_rotating =
{
message = "hl1/ambience/labdrone2.wav",
volume = 8,
responsecontext = "-1 -1 -1 1 1 1",
targetname = "crystal_spin",
spawnflags = 65,
solidbsp = 0,
rendermode = 10,
rendercolor = "255 255 255",
renderamt = 255,
maxspeed = 48,
fanfriction = 20,
origin = Vector(278.900513, -2033.692993, 516.067200),
}
},
[2] = {
tf_glow =
{
targetname = "crystalglow",
parentname = "crystal",
target = "crystal",
Mode = 2,
origin = Vector(278.900513, -2033.692993, 516.067200),
GlowColor = "0 78 255 255"
}
},
[3] = {
prop_dynamic =
{
targetname = "crystal",
solid = 6,
renderfx = 15,
rendercolor = "255 255 255",
renderamt = 255,
physdamagescale = 1.0,
parentname = "crystal_spin",
modelscale = 1.3,
model = "models/props_moonbase/moon_gravel_crystal_blue.mdl",
MinAnimTime = 5,
MaxAnimTime = 10,
fadescale = 1.0,
fademindist = -1.0,
origin = Vector(278.900513, -2033.692993, 516.067200),
angles = QAngle(45, 0, 0)
}
},
})
local script_template = Entities.CreateByClassname("point_script_template")
script_template.AddTemplate("func_rotating", {
message = "hl1/ambience/labdrone2.wav",
volume = 8,
targetname = "crystal_spin2",
spawnflags = 65,
solidbsp = 0,
rendermode = 10,
rendercolor = "255 255 255",
renderamt = 255,
maxspeed = 48,
fanfriction = 20,
origin = Vector(175.907211, -2188.908691, 516.031311),
})
script_template.AddTemplate("tf_glow", {
target = "crystal2",
Mode = 2,
origin = Vector(175.907211, -2188.908691, 516.031311),
GlowColor = "0 78 255 255"
})
script_template.AddTemplate("prop_dynamic",{
targetname = "crystal2",
solid = 6,
renderfx = 15,
rendercolor = "255 255 255",
renderamt = 255,
physdamagescale = 1.0,
parentname = "crystal_spin2",
modelscale = 1.3,
model = "models/props_moonbase/moon_gravel_crystal_blue.mdl",
MinAnimTime = 5,
MaxAnimTime = 10,
fadescale = 1.0,
fademindist = -1.0,
origin = Vector(175.907211, -2188.908691, 516.031311),
angles = QAngle(45, 0, 0)
})
EntFireByHandle(script_template, "ForceSpawn", "", -1, null, null)
Result:
Configuration | Results |
---|---|
SpawnEntityGroupFromTable
|
0.2382ms
|
point_script_template
|
0.1100ms
|
Iterating through players
When iterating over all players in the map, it is generally not recommended to use FindByClassname on the player entity if performance is a concern. Iterating over the first MaxClients
number of entindexes and grabbing the player from PlayerInstanceFromIndex(i)
is notably faster and not much more complex to write. If you want the fastest option at the cost of complexity though, you should collect player entities in your own global array in an event such as player_team or player_activate (and remove them in player_disconnect), then iterate over that array when necessary.
Benchmark
The first script must be executed before the second one!
::playerArray <- []
::Events <- {
function OnGameEvent_player_team(params)
{
local player = GetPlayerFromUserID(params.userid)
if (playerArray.find(player) != null) return
playerArray.append(player)
}
function OnGameEvent_player_disconnect(params)
{
local player = GetPlayerFromUserID(params.userid)
if (playerArray.find(player) == null) return
playerArray.remove(player)
}
}
__CollectGameEventCallbacks(Events)
::maxClients <- MaxClients().tointeger()
for (local player; player = Entities.FindByClassname(player, "player");)
{
printl(player)
}
for (local i = 1; i <= maxClients; i++)
{
local player = PlayerInstanceFromIndex(i)
if (player == null) continue
printl(player)
}
foreach(player in playerArray)
{
printl(player)
}
Result:
Configuration | Results |
---|---|
FindByClassname
|
0.1289ms
|
Index iteration
|
0.0856ms
|
Array iteration
|
0.0679ms
|
Squirrel Performance Tips
arrays and tables
If you know the variable you are working with is always going to be an array or table, you can optimize your array/table length checks significantly
Arrays
Arrays in squirrel are, effectively, tables where the index is an integer value. This means that we can perform an index lookup on our array to check if our array is a specified length.
The .len() operator needs to first evaluate the data type so it can return the correct value for strings as well, we can avoid this overhead entirely by doing this index lookup method instead
local arr = array(1000)
for (local i = 0; i < 1000, i++)
print(arr.len() == 1000)
for (local i = 0; i < 1000, i++)
printl((999 in arr && (!1000 in arr)) //check if the index 999 exists, but not if index 1000 exists. If index 1000 doesn't exist then this will return true
Additionally, the integer 0 will return the value false in squirrel. For specifically checking an empty array, this falsy evaluation is faster than directly checking if length equals 0
local arr = []
for (local i = 0; i < 1000, i++)
if (arr.len() == 0)
print(i)
for (local i = 0; i < 1000, i++)
if (!arr.len()) // 0 = false
print(i)
Tables
As shown above, we can circumvent the performance cost of .len() by using direct index look-ups where possible. Instead of using .len(), We can keep an index called "length" in our table, and add/subtract from this table value whenever we insert/delete an item from the table.
local tab = {length = 0}
//insert stuff into the table and increment the table length
for (local i = 0; i < 1001; i++)
{
tab[format("value_%d", i)] <- i
tab.length++
}
//.len() eval
for (local i = 0; i < 1000; i++)
print(tab.len() == 1000)
//index lookup
for (local i = 0; i < 1000; i++)
print(tab.length == 1000)
This of course has performance implications of its own, and heavily depends on how often you are reading data from a table vs writing to it.
Variable look-up and caching
Squirrel will look for variables in the following order:
- local variables
- "outer" local variables (locals that are in parent scope)
- constants
- root table
For example, this will print the number 3. commenting out local thing1
will print 2, ::thing1
would print 1, and uncommenting local thing1 = 0
in the function would print 0.
::thing1 <- 1
const thing1 = 2
local thing1 = 3
::GetThing1 <- function() {
// local thing1 = 0
return thing1
}
print( GetThing1() )
Traversing scopes to find variables like this will negatively impact performance. It is better to cache variables as locals before expensive loops or fast-firing functions (thinks).
::SomeGlobalVar <- 10
function SlowLookup()
{
for (local i = 0; i < 10000; i++)
SomeGlobalVar += i
}
function FastLookup()
{
local myvar = SomeGlobalVar
for (local i = 0; i < 10000; i++)
myvar += i
}
SlowLookup()
FastLookup()
Root table lookups
You may think that prefixing a root-scoped variable with ::
will skip this traversal process and improve performance. Counterintuitively, this will reduce performance. ::
will run additional instructions to load the root table first.
::a <- 0;
function NormalLookup() {
for (local i = 0; i < 1000000; i++) {
a += i
}
}
function RootLookup() {
for (local i = 0; i < 1000000; i++) {
::a += i
}
}
NormalLookup()
RootLookup() // slower!