User:Braindawg/performance: Difference between revisions

From Valve Developer Community
Jump to navigation Jump to search
(update to use new benchmarking tool)
Line 1: Line 1:
This page includes tips and tricks for optimizing [[VScript]] performance.  All of these performance tests were done in {{tf2}} and many can be used in other {{src13}}-based titles.  Your mileage may vary in VScript supported games prior to the SDK update ({{l4d2}}{{portal2}}{{asw}}).
This page includes tips and tricks for optimizing [[VScript]] performance.  All of these performance tests were done in {{tf2}} and many can be used in other {{src13}}-based titles.  Your mileage may vary in VScript supported games prior to the SDK update ({{l4d2}}{{portal2}}{{asw}}).  Benchmarks figure come from [https://github.com/potato-tf/vscript-benchmark this benchmarking tool].


{{Warning|Only optimize your scripts if you need to!  Some of these tips may introduce extra unnecessary complexity to your projects.  Premature optimization without knowing where your performance issues actually come from is extremely ill-advised.}}
{{Warning|Only optimize your scripts if you need to!  Some of these tips may introduce extra unnecessary complexity to your projects.  Premature optimization without knowing where your performance issues actually come from is extremely ill-advised.}}
{{Note|Benchmarks were done using a benchmarking tool that is not publicly releasedSet </code>vscript_perf_warning_spew_ms</code> to 0 and run these scripts separately if you would like to benchmark them yourself. {{todo|update all benchmarks using this instead}}}}
{{Note|The built-in performance counter for VScript has a lot of "noise", and depends heavily on other things executing on the main game server thread.  Memory speed, CPU speed, And even file I/O, will greatly impact your results and the variance between them, even on repeated runs of the same code.  The numbers shown here are averages/ballpark figures taken from 5 or more repeated runs.  A third-party benchmarking tool does exist, however there is unfortunately no convenient download link for itLook around mapping/content creation discord servers for it.}}
= VScript Performance Tips =
== Folding Functions ==


= 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.
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 ===
=== Benchmark ===


{{Note|Benchmark done on tc_hydro}}
{{Note|Benchmark done on mvm_bigrock}}


<source lang=js>
<source lang=js>
::ROOT <- getroottable()
/***********************************************************************************************************
local tofold = [ "NetProps", "Entities", "EntityOutputs", "NavMesh", "Convars" ]
* FOLDING:                                                                                               *
* Folding functions from their original scope into local/root scope is noticeably faster (~15-30%)        *
* skips extra lookup instructions, also less verbose                                                      *
***********************************************************************************************************/
local GetPropString = NetProps.GetPropString.bindenv( NetProps )
local GetPropBool = NetProps.GetPropBool.bindenv( NetProps )
const MAX_EDICTS = 2048
 
function Benchmark::Unfolded() {


// these are defined multiple times in other classes, skip to avoid conflicts
    for ( local i = 0, ent; i < Constants.Server.MAX_EDICTS; ent = EntIndexToHScript( i ), i++ ) {
local foldblacklist = {
 
IsValid  = true
        if ( ent ) {
GetName  = true
 
GetCenter = true
            NetProps.GetPropString( ent, "m_iName" )
            NetProps.GetPropString( ent, "m_iClassname" )
            NetProps.GetPropBool( ent, "m_bForcePurgeFixedupStrings" )
        }
    }
}
}


// fold every class into the root table for performance
// 20% faster, maybe more
foreach( method_class in tofold)
function Benchmark::Folded() {


foreach( k, v in ROOT[method_class].getclass() )
    for ( local i = 0, ent; i < MAX_EDICTS; ent = EntIndexToHScript( i ), i++ ) {


if ( !( k in foldblacklist ) && !( k in ROOT ) )
        if ( ent ) {
 
ROOT[k] <- ROOT[method_class][k].bindenv( ROOT[method_class] )


            GetPropString( ent, "m_iName" )
            GetPropString( ent, "m_iClassname" )
            GetPropBool( ent, "m_bForcePurgeFixedupStrings" )
        }
    }
}
</source>
</source>


Line 41: Line 58:
|-
|-
|<code>Unfolded</code>
|<code>Unfolded</code>
|<code>0.1439ms</code>
|<code>1.76ms</code>
|-
|-
|<code>Folded</code>
|<code>Folded</code>
|<code>0.0999ms</code>
|<code>1.32ms</code>
|-
|-
|}
|}


== Constants ==
== Constants ==
=== Folding Constants ===


Similar to folding functions, folding pre-defined Constant values into the constant table (or the root table) increases performance significantly.
Similar to folding functions, folding pre-defined Constant values into the constant table (or the root table) increases performance significantly.


===Benchmark===
=== Benchmark ===


<source lang=js>
<source lang=js>
local j = 0
local _CONST = getconsttable()


for (local i = 1; i <= Constants.Server.MAX_EDICTS; i++)
// fold every pre-defined constant into the const table
    j++
if ( !( "ConstantNamingConvention" in ROOT ) )
foreach( a, b in Constants )
foreach( k, v in b )
            _CONST[k] <- v != null ? v : 0


const MAX_EDICTS = 2048
setconsttable(_CONST)
local e = 0
 
function Benchmark::UnfoldedConst() {
 
    for (local i = 1; i <= Constants.Server.MAX_EDICTS; i++)
        local temp = i
}
 
function Benchmark::FoldedConst() {


for (local i = 1; i <= MAX_EDICTS; i++)
    for (local i = 1; i <= MAX_EDICTS; i++)
    e++
        local temp = i
}
</source>
</source>


Line 76: Line 102:
|-
|-
|<code>Unfolded</code>
|<code>Unfolded</code>
|<code>0.1127ms</code>
|<code>0.356ms</code>
|-
|-
|<code>"Folded"</code>
|<code>Folded</code>
|<code>0.0423ms</code>
|<code>0.033ms</code>
|-
|-
|}
|}


=== Root table vs Constant table ===
== 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.
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.
If you intend to insert values into the constant table (<code>const</code> keyword or <code>getconsttable().foo <- "bar"</code>), you must do this ''before'' any other scripts are executed, otherwise your script will not be able to read any values from it.


=== Benchmark ===
== Benchmark ==
<source lang=js>
<source lang=js>
::ROOT_VALUE <- 2
::SomeGlobalVar <- 0
const CONST_VALUE = 2
const GLOBAL_VAR = 0x7FFFFFFF
for (local i = 0; i <= 10000; i++)
function Benchmark::RootSetLookup() {
    i += CONST_VALUE


for (local i = 0; i <= 10000; i++)
    for (local i = 1; i <= 10000; i++)
    i += ROOT_VALUE
        local temp = ::SomeGlobalVar
}


// ~20-40% faster
function Benchmark::ConstSetLookup() {
    for (local i = 1; i <= 10000; i++)
        local temp = GLOBAL_VAR
}
</source>
</source>


Line 107: Line 139:
! Results
! Results
|-
|-
|<code>Constant</code>
|<code>Root</code>
|<code>0.0767ms</code>
|<code>0.267ms</code>
|-
|-
|<code>Root</code>
|<code>Const</code>
|<code>0.1037ms</code>
|<code>0.154ms</code>
|-
|-
|}
|}


== String Formatting ==  
= String Formatting =


Squirrel supports two main ways to format strings: Concatenation using the + symbol, and the <code>format()</code> function.  <code>format()</code> is significantly faster than concatenation.
Squirrel supports two main ways to format strings: Concatenation using the + symbol, and the <code>format()</code> function.  <code>format()</code> is significantly faster than concatenation.
Line 121: Line 153:
{{Tip|For formatting entity handles and functions, use <code>.tostring()</code> and format it as a string}}
{{Tip|For formatting entity handles and functions, use <code>.tostring()</code> and format it as a string}}


=== ToKVString ===
== ToKVString ==


the <code>TOKVString()</code> VScript function takes a Vector/QAngle and formats the values into a string.  For example, <code>Vector(0, 0, 0).ToKVString()</code> would be <code>"0 0 0"</code>
the <code>TOKVString()</code> VScript function takes a Vector/QAngle and formats the values into a string.  For example, <code>Vector(0, 0, 0).ToKVString()</code> would be <code>"0 0 0"</code>
Line 127: Line 159:
On top of being less cumbersome to write, <code>ToKVString()</code> is marginally faster than <code>format()</code>.
On top of being less cumbersome to write, <code>ToKVString()</code> is marginally faster than <code>format()</code>.


When formatting multiple <code>ToKVString()</code> outputs into a new string, concatenation may be faster.
However, when formatting multiple <code>ToKVString()</code> outputs into a new string, concatenation is faster due to less function calls.


=== Benchmark ===  
== Benchmark ==  


<source lang=js>
<source lang=js>
local mins = Vector(-1, -2, -3)
function Benchmark::StringConcat() {
local maxs = Vector(1, 2, 3)
 
local kvstring = ""
    for ( local i = 0; i < 10000; i++ )
        kvstring = mins.x + "," + mins.y + "," + mins.z + "," + maxs.x + "," + maxs.y + "," + maxs.z
}
 
function Benchmark::StringFormat() {


for (local i = 0; i < 10000; i++)
    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()
        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++)
function Benchmark::KVStringFormat() {
    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++ )
for (local i = 0; i < 10000; i++)
        kvstring = format("%s %s", mins.ToKVString(), maxs.ToKVString())
    kvstring = format("%s %s", mins.ToKVString(), maxs.ToKVString())
}
 
function Benchmark::KVStringConcat() {


for (local i = 0; i < 10000; i++)
    for (local i = 0; i < 10000; i++ )
    kvstring = mins.ToKVString() + " " + maxs.ToKVString()
        kvstring = mins.ToKVString() + " " + maxs.ToKVString()
}
</source>
</source>


Line 156: Line 195:
! Results
! Results
|-
|-
|<code>concat</code>
|<code>StringConcat</code>
|<code>39.0847ms</code>
|<code>35.0847ms</code>
|-
|-
|<code>format</code>
|<code>StringFormat</code>
|<code>24.0123ms</code>
|<code>23.0143ms</code>
|-
|-
|<code>ToKVString</code>
|<code>KVStringFormat</code>
|<code> 19.9377ms</code>
|<code> 19.9377ms</code>
|-
|-
|<code>ToKVString + concat</code>
|<code>KVStringConcat</code>
|<code>18.5166ms</code>
|<code>18.3142ms</code>
|}
|}


== Spawning Entities ==
= Spawning Entities =


in VScript, there are four common ways to spawn entities:
in VScript, there are four common ways to spawn entities:
Line 181: Line 220:
- point_script_template entity + AddTemplate
- point_script_template entity + AddTemplate


=== CreateByClassname + DispatchSpawn vs SpawnEntityFromTable ===
== 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 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.
Line 187: Line 226:
In situations like this, CreateByClassname + DispatchSpawn is roughly 4x faster in comparison to <code>SpawnEntityFromTable</code>.
In situations like this, CreateByClassname + DispatchSpawn is roughly 4x faster in comparison to <code>SpawnEntityFromTable</code>.


=== Benchmark ===
== Benchmark ==


<source lang=js>
<source lang=js>
trigger_stun = SpawnEntityFromTable("trigger_stun",
local CreateByClassname = Entities.CreateByClassname.bindenv( Entities )
{
local SetPropBool = NetProps.SetPropBool.bindenv( NetProps )
    stun_type = 2,
local SetPropString = NetProps.SetPropString.bindenv( NetProps )
    stun_effects = true,
local DispatchSpawn = Entities.DispatchSpawn.bindenv( Entities )
    stun_duration = 3,
 
     move_speed_reduction = 0.1,
// anywhere from 15-30% faster for single entity spawning
    trigger_delay = 0.1,
// The table passed to SpawnEntityFromTable needs to be interpreted and converted to something C++ can understand
     spawnflags = 1,
// also, wide performance variations are likely due to garbage collection on the passed table
})
// meanwhile CreateByClassname/netprop/keyvaluefromstring are simple 1:1 C++ bindings
function Benchmark::ByClassname() {
 
     for (local i = 0; i < 100; i++) {
 
        local ent = CreateByClassname( "logic_relay" )
        DispatchSpawn( ent )
        SetPropString( ent, "m_iName", "__relay" )
     }
}
 
function Benchmark::FromTable() {
 
    for (local i = 0; i < 100; i++) {


trigger_stun = Entities.CreateByClassname("trigger_stun")
        SpawnEntityFromTable( "logic_relay", { targetname = "__relay" } )
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)
</source>
</source>


Line 223: Line 270:
|}
|}


=== SpawnEntityGroupFromTable vs point_script_template ===
== SpawnEntityGroupFromTable vs point_script_template ==


When spawning multiple entities at the same time, it is more efficient to use either <code>SpawnEntityGroupFromTable</code> or a [[point_script_template]] entity.  These options also have the added benefit of respecting parent hierarchy, so the <code>parentname</code> keyvalue works as intended.
When spawning multiple entities at the same time, it is more efficient to use either <code>SpawnEntityGroupFromTable</code> or a [[point_script_template]] entity.  These options also have the added benefit of respecting parent hierarchy, so the <code>parentname</code> keyvalue works as intended.
Line 229: Line 276:
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 [[Team_Fortress_2/Scripting/Script_Functions#CPointScriptTemplate|VScript documentation]] for more details on how to use point_script_template.
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 [[Team_Fortress_2/Scripting/Script_Functions#CPointScriptTemplate|VScript documentation]] for more details on how to use point_script_template.


=== Benchmark ===
== Benchmark ==
<source lang=js>
<source lang=js>


//spawn origins are right outside of bigrock spawn
function Benchmark::EntityGroupFromTable() {
SpawnEntityGroupFromTable({
 
    [0] = {
    // spawn origins are right outside of bigrock spawn
        func_rotating =
    SpawnEntityGroupFromTable({
        {
        [0] = {
            message = "hl1/ambience/labdrone2.wav",
            func_rotating =
            volume = 8,
            {
            responsecontext = "-1 -1 -1 1 1 1",
                message = "hl1/ambience/labdrone2.wav",
            targetname = "crystal_spin",
                volume = 8,
            spawnflags = 65,
                responsecontext = "-1 -1 -1 1 1 1",
            solidbsp = 0,
                targetname = "crystal_spin",
            rendermode = 10,
                vscripts = "rotatefix", // see func_rotating vdc page for this
            rendercolor = "255 255 255",
                spawnflags = 65,
            renderamt = 255,
                solidbsp = 0,
            maxspeed = 48,
                rendermode = 10,
            fanfriction = 20,
                rendercolor = "255 255 255",
            origin = Vector(278.900513, -2033.692993, 516.067200),
                renderamt = 255,
        }
                maxspeed = 48,
    },
                fanfriction = 20,
    [2] = {
                origin = Vector(278.900513, -2033.692993, 516.067200),
        tf_glow =
            }
        {
        },
            targetname = "crystalglow",
        [2] = {
            parentname = "crystal",
            tf_glow =
            target = "crystal",
            {
            Mode = 2,
                targetname = "crystalglow",
            origin = Vector(278.900513, -2033.692993, 516.067200),
                parentname = "crystal",
            GlowColor = "0 78 255 255"
                target = "crystal",
        }
                Mode = 2,
    },
                origin = Vector(278.900513, -2033.692993, 516.067200),
    [3] = {
                GlowColor = "0 78 255 255"
        prop_dynamic =
            }
        {
        },
            targetname = "crystal",
        [3] = {
            solid = 6,
            prop_dynamic =
            renderfx = 15,
            {
            rendercolor = "255 255 255",
                targetname = "crystal",
            renderamt = 255,
                solid = 6,
            physdamagescale = 1.0,
                renderfx = 15,
            parentname = "crystal_spin",
                rendercolor = "255 255 255",
            modelscale = 1.3,
                renderamt = 255,
            model = "models/props_moonbase/moon_gravel_crystal_blue.mdl",
                physdamagescale = 1.0,
            MinAnimTime = 5,
                parentname = "crystal_spin",
            MaxAnimTime = 10,
                modelscale = 1.3,
            fadescale = 1.0,
                model = "models/props_moonbase/moon_gravel_crystal_blue.mdl",
            fademindist = -1.0,
                MinAnimTime = 5,
            origin = Vector(278.900513, -2033.692993, 516.067200),
                MaxAnimTime = 10,
            angles = QAngle(45, 0, 0)
                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")
// ~15-25% faster for batch entity spawning
function Benchmark::PointScriptTemplate() {


script_template.AddTemplate("func_rotating", {
    local script_template = Entities.CreateByClassname("point_script_template")
    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", {
    script_template.AddTemplate("func_rotating", {
         target = "crystal2",
         message = "hl1/ambience/labdrone2.wav",
         Mode = 2,
        volume = 8,
        targetname = "crystal_spin2",
         spawnflags = 65,
        solidbsp = 0,
        rendermode = 10,
        rendercolor = "255 255 255",
        vscripts = "rotatefix",
        renderamt = 255,
        maxspeed = 48,
        fanfriction = 20,
         origin = Vector(175.907211, -2188.908691, 516.031311),
         origin = Vector(175.907211, -2188.908691, 516.031311),
        GlowColor = "0 78 255 255"
    })
})


script_template.AddTemplate("prop_dynamic",{
    script_template.AddTemplate("tf_glow", {
    targetname = "crystal2",
            target = "crystal2",
    solid = 6,
            Mode = 2,
    renderfx = 15,
            origin = Vector(175.907211, -2188.908691, 516.031311),
    rendercolor = "255 255 255",
            GlowColor = "0 78 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)
    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)
    })


    script_template.AcceptInput( "ForceSpawn", null, null, null )
}
</source>
</source>
Result:
Result:
Line 335: Line 390:
|-
|-
|<code>SpawnEntityGroupFromTable</code>
|<code>SpawnEntityGroupFromTable</code>
|<code>0.2382ms</code>
|<code>0.72ms</code>
|-
|-
|<code>point_script_template</code>
|<code>PointScriptTemplate</code>
|<code>0.1100ms</code>
|<code>0.61ms</code>
|}
|}


== Iterating through players ==
= 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 <code>MaxClients</code> number of entindexes and grabbing the player from <code>PlayerInstanceFromIndex(i)</code> 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.
When iterating over all players in the map, it is generally not recommended to use FindByClassname on the [[player]] entity in high playercount environments (>8-12 players).  Iterating over the first <code>MaxClients</code> number of entindexes and grabbing the player from <code>PlayerInstanceFromIndex(i)</code> is notably faster and not much more complex to write in these circumstances.   


=== Benchmark ===
The performance of player iteration depends heavily on how many players are actively in the server.  In low playercount environments, the <code>PlayerInstanceFromIndex</code> approach is slower due to extra unnecessary iterations.  In high playercount environments, `FindByClassname` runs a more expensive loop on every entity in the map to find players.
 
If you want the fastest option at the cost of complexity, you should collect player entities in your own global table or array in an event such as player_team or player_activate, remove them on player_disconnect, then iterate over that when necessary.  Using a table gives you the added bonus of having a cache of player user IDs, which is faster to look up compared to reading the <code>player_manager</code> netprop.
 
{{warning|<code>player_activate</code> does not fire for tfbots!}}
 
== Benchmark ==
The first script must be executed before the second one!
The first script must be executed before the second one!
{{todo|update benchmarks}}


<source lang=js>
<source lang=js>
::playerArray <- []
::ALL_PLAYERS <- {}
::Events <- {
::Events <- {
     function OnGameEvent_player_team(params)
     function OnGameEvent_player_team(params)
Line 355: Line 417:
         local player = GetPlayerFromUserID(params.userid)
         local player = GetPlayerFromUserID(params.userid)
          
          
         if (playerArray.find(player) != null) return
         if ( player in ALL_PLAYERS ) return
          
         ALL_PLAYERS[ player ] <- params.userid
        playerArray.append(player)
     }
     }
      
      
Line 364: Line 426:
         local player = GetPlayerFromUserID(params.userid)
         local player = GetPlayerFromUserID(params.userid)
      
      
         if (playerArray.find(player) == null) return
         if ( !(player in ALL_PLAYERS) ) return


         playerArray.remove(player)
         delete ALL_PLAYERS[ player ]
     }
     }
}
}
Line 389: Line 451:
}
}


foreach(player in playerArray)
foreach(player in ALL_PLAYERS.keys())
{
{
     printl(player)
     printl(player)
Line 407: Line 469:
|<code>0.0856ms</code>
|<code>0.0856ms</code>
|-
|-
|<code>Array iteration</code>
|<code>Array/Table iteration</code>
|<code>0.0679ms</code>
|<code>0.0679ms</code>
|}
|}


= Squirrel Performance Tips =
= Squirrel Performance Tips =
== arrays and tables ==
== 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 in squirrel are, in practice, tables where the index is an integer value.


=== Arrays ===
The .len() operator needs to first evaluate the data type (string or table/array), and uses a function call (expensive), we can avoid this overhead by directly checking the index.


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.
<source lang=js>
/*****************
  * LENGTH CHECKS *
*****************/
function Benchmark::Len() {


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
    for ( local i = 0; i < 1000; i++ )
        if ( arr.len() == 1000 )
            local len = true
}


<source lang=js>
// ~40% faster, no _OP_PREPCALLK/_OP_CALL instructions
local arr = array(1000)
function Benchmark::Idx() {


for (local i = 0; i < 1000, i++)
    for ( local i = 0; i < 1000; i++ )
    print(arr.len() == 1000)
        if ( 999 in arr && !(1000 in arr) )
 
            local len = true
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
</source>
</source>


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
Additionally, the integer 0 will return the value false in squirrel.  For specifically checking an empty array, this falsy evaluation is slightly faster than directly checking if length equals 0


<source lang=js</code>
<source lang=js</code>
local arr = []
/****************************
for (local i = 0; i < 1000, i++)
* EMPTY ARRAY/TABLE CHECKS *
    if (arr.len() == 0)
****************************/
        print(i)
function Benchmark::LenExplicit() {


for (local i = 0; i < 1000, i++)
    for ( local i = 0; i < 1000; i++ )
    if (!arr.len()) // 0 = false
        if ( arr.len() != 0 )
         print(i)
            local len = true
}
 
// ~2-5% faster, no _OP_NE instruction
function Benchmark::LenFalsy() {
   
    for ( local i = 0; i < 1000; i++ )
         if ( arr.len() )
            local len = true
}
</source>
</source>


=== Tables ===
=== 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.
As shown above, we can circumvent the performance cost of .len() by using direct index look-ups where possible.  Instead of using .len() for tables, We can create a helper class with a "length" member, and add/subtract from this whenever we insert/delete an item from the table.


<source lang=js>
<source lang=js>
// direct length index lookups instead of .len() calls.
Benchmark.NewTable <- class {


local tab = {length = 0}
    _tbl  = null // the real table in our class
    length = 0 // length variable, static so other functions can't override it.


//insert stuff into the table and increment the table length
    constructor( tbl = null ) {  this._tbl = ( tbl || {} ) ; this.length = this._tbl.len() }
for (local i = 0; i < 1001; i++)
 
    function get(k) { _tbl[k] }
    function set(k, v) { k in _tbl ? _tbl[k] = v : (length++, _tbl[k] <- v) }
    function del(k) { ( length--, delete _tbl[k] ) }
}
 
local tab = Benchmark.NewTable()
local _tbl = tab._tbl
 
// insert stuff into the table and increment the table length
for (local i = 0; i <= 1000; i++)
{
{
     tab[format("value_%d", i)] <- i
     tab.set("value_" + i, i )
    tab.length++
}
}


//.len() eval
// .len() eval
for (local i = 0; i < 1000; i++)
function Benchmark::Len() {
    print(tab.len() == 1000)
    for (local i = 0; i < 1000; i++)
        print(_tbl.len() == 1000)
}


//index lookup
// index lookup, ~2.5% faster
for (local i = 0; i < 1000; i++)
function Benchmark::Length() {
    print(tab.length == 1000)
    for (local i = 0; i < 1000; i++)
        print(tab.length == 1000)
}
</source>
</source>


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.
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. You may only see performance benefits if you are checking table lengths a lot, but writing/reading infrequently


== Variable look-up and caching ==
== Benchmark ==
 
{| class="standard-table"
! Configuration
! Results
|-
|<code>Len</code>
|<code>0.075ms</code>
|-
|<code>Idx</code>
|<code>0.046ms</code>
|-
|<code>LenExplicit</code>
|<code>0.071ms</code>
|-
|<code>LenFalsy</code>
|<code>0.067ms</code>
|-
|<code>Len (table)</code>
|<code>9.4ms</code>
|-
|<code>Length (table)</code>
|<code>9.3ms</code>
|}
 
= Variable look-up and caching =


Squirrel will look for variables in the following order:
Squirrel will look for variables in the following order:
Line 480: Line 598:
# root table
# root table


For example, this will print the number 3commenting out <code>local thing1</code> will print 2, <code>::thing1</code> would print 1, and uncommenting <code>local thing1 = 0</code> in the function would print 0.
For example:
# this will print the number 3 (outer local)
# commenting out <code>local thing1</code> will print 2 (const)
# commenting out <code>::thing1</code> would print 1 (root)
# uncommenting <code>local thing1 = 0</code> would print 0 (local)


<source lang=js>
<source lang=js>
Line 498: Line 620:


<source lang=js>
<source lang=js>
::SomeGlobalVar <- 10
::SomeGlobalVar <- 0


function SlowLookup()  
function Benchmark::_OnDestroy() { delete ::SomeGlobalVar }
 
function Benchmark::SlowIncrement()  
{
{
     for (local i = 0; i < 10000; i++)
     for (local i = 1; i <= 1000; i++)
         SomeGlobalVar += i
         SomeGlobalVar++
}
}


function FastLookup()
// 10x faster!?
function Benchmark::FastIncrement()
{
{
     local myvar = SomeGlobalVar
     local myvar = SomeGlobalVar
     for (local i = 0; i < 10000; i++)
 
         myvar += i
     for (local i = 1; i <= 1000; i++)
         myvar++
 
    SomeGlobalVar = myvar
}
}
SlowLookup()
FastLookup()
</source>
</source>


=== Root table lookups ===
=== Root table lookups ===


You may think that prefixing a root-scoped variable with <code>::</code> will skip this traversal process and improve performance.  Counterintuitively, this will reduce performance.  <code>::</code> will run additional instructions to load the root table first.
Prefixing a root-scoped variable with <code>::</code> will skip this traversal process and improve performance considerably.


<source lang=js>
<source lang=js>
::a <- 0;
function Benchmark::NormalLookup() {


function NormalLookup() {
    for (local i = 1; i <= 1000; i++)
        SomeGlobalVar++
}


    for (local i = 0; i < 1000000; i++) {
// 10x faster!?
        a += i
function Benchmark::RootLookup() {
    }
}


function RootLookup() {
     for (local i = 1; i <= 1000; i++)
     for (local i = 0; i < 1000000; i++) {
         ::SomeGlobalVar++
         ::a += i
    }
}
}
NormalLookup()
RootLookup() // slower!
</source>
</source>
== Benchmark ==
{{todo|this is the exact ''opposite'' result of the third-party benchmarking tool by a significant degree...?}}
{| class="standard-table"
! Configuration
! Results
|-
|<code>SlowIncrement</code>
|<code>0.591ms</code>
|-
|<code>FastIncrement</code>
|<code>0.021ms</code>
|-
|<code>NormalLookup</code>
|<code>0.584ms</code>
|-
|<code>RootLookup</code>
|<code>0.058ms</code>
|}

Revision as of 10:44, 22 October 2025

This page includes tips and tricks for optimizing VScript performance. All of these performance tests were done in Team Fortress 2 and many can be used in other Source 2013-based titles. Your mileage may vary in VScript supported games prior to the SDK update (Left 4 Dead 2Portal 2Alien Swarm). Benchmarks figure come from this benchmarking tool.

Warning.pngWarning:Only optimize your scripts if you need to! Some of these tips may introduce extra unnecessary complexity to your projects. Premature optimization without knowing where your performance issues actually come from is extremely ill-advised.
Note.pngNote:The built-in performance counter for VScript has a lot of "noise", and depends heavily on other things executing on the main game server thread. Memory speed, CPU speed, And even file I/O, will greatly impact your results and the variance between them, even on repeated runs of the same code. The numbers shown here are averages/ballpark figures taken from 5 or more repeated runs. A third-party benchmarking tool does exist, however there is unfortunately no convenient download link for it. Look around mapping/content creation discord servers for it.

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

Note.pngNote:Benchmark done on mvm_bigrock
/***********************************************************************************************************
 * FOLDING:                                                                                                *
 * Folding functions from their original scope into local/root scope is noticeably faster (~15-30%)        *
 * skips extra lookup instructions, also less verbose                                                      *
 ***********************************************************************************************************/
local GetPropString = NetProps.GetPropString.bindenv( NetProps )
local GetPropBool = NetProps.GetPropBool.bindenv( NetProps )
const MAX_EDICTS = 2048

function Benchmark::Unfolded() {

    for ( local i = 0, ent; i < Constants.Server.MAX_EDICTS; ent = EntIndexToHScript( i ), i++ ) {

        if ( ent ) {

            NetProps.GetPropString( ent, "m_iName" )
            NetProps.GetPropString( ent, "m_iClassname" )
            NetProps.GetPropBool( ent, "m_bForcePurgeFixedupStrings" )
        }
    }
}

// 20% faster, maybe more
function Benchmark::Folded() {

    for ( local i = 0, ent; i < MAX_EDICTS; ent = EntIndexToHScript( i ), i++ ) {

        if ( ent ) {

            GetPropString( ent, "m_iName" )
            GetPropString( ent, "m_iClassname" )
            GetPropBool( ent, "m_bForcePurgeFixedupStrings" )
        }
    }
}

Result:

Configuration Results
Unfolded 1.76ms
Folded 1.32ms

Constants

Similar to folding functions, folding pre-defined Constant values into the constant table (or the root table) increases performance significantly.

Benchmark

local _CONST = getconsttable()

// fold every pre-defined constant into the const table
if ( !( "ConstantNamingConvention" in ROOT ) )
	foreach( a, b in Constants )
		foreach( k, v in b )
            _CONST[k] <- v != null ? v : 0

setconsttable(_CONST)

function Benchmark::UnfoldedConst() {

    for (local i = 1; i <= Constants.Server.MAX_EDICTS; i++)
        local temp = i
}

function Benchmark::FoldedConst() {

    for (local i = 1; i <= MAX_EDICTS; i++)
        local temp = i
}

Result:

Configuration Results
Unfolded 0.356ms
Folded 0.033ms

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 (const keyword or getconsttable().foo <- "bar"), you must do this before any other scripts are executed, otherwise your script will not be able to read any values from it.

Benchmark

::SomeGlobalVar <- 0
const GLOBAL_VAR = 0x7FFFFFFF
function Benchmark::RootSetLookup() {

    for (local i = 1; i <= 10000; i++)
        local temp = ::SomeGlobalVar
}

// ~20-40% faster
function Benchmark::ConstSetLookup() {

    for (local i = 1; i <= 10000; i++)
        local temp = GLOBAL_VAR
}

Result:

Configuration Results
Root 0.267ms
Const 0.154ms

String Formatting

Squirrel supports two main ways to format strings: Concatenation using the + symbol, and the format() function. format() is significantly faster than concatenation.

Tip.pngTip:For formatting entity handles and functions, use .tostring() and format it as a string

ToKVString

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().

However, when formatting multiple ToKVString() outputs into a new string, concatenation is faster due to less function calls.

Benchmark

function Benchmark::StringConcat() {

    for ( local i = 0; i < 10000; i++ )
        kvstring = mins.x + "," + mins.y + "," + mins.z + "," + maxs.x + "," + maxs.y + "," + maxs.z
}

function Benchmark::StringFormat() {

    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)
}

function Benchmark::KVStringFormat() {

    for (local i = 0; i < 10000; i++ )
        kvstring = format("%s %s", mins.ToKVString(), maxs.ToKVString())
}

function Benchmark::KVStringConcat() {

    for (local i = 0; i < 10000; i++ )
        kvstring = mins.ToKVString() + " " + maxs.ToKVString()
}

Result:

Configuration Results
StringConcat 35.0847ms
StringFormat 23.0143ms
KVStringFormat 19.9377ms
KVStringConcat 18.3142ms

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

local CreateByClassname = Entities.CreateByClassname.bindenv( Entities )
local SetPropBool = NetProps.SetPropBool.bindenv( NetProps )
local SetPropString = NetProps.SetPropString.bindenv( NetProps )
local DispatchSpawn = Entities.DispatchSpawn.bindenv( Entities )

// anywhere from 15-30% faster for single entity spawning
// The table passed to SpawnEntityFromTable needs to be interpreted and converted to something C++ can understand
// also, wide performance variations are likely due to garbage collection on the passed table
// meanwhile CreateByClassname/netprop/keyvaluefromstring are simple 1:1 C++ bindings
function Benchmark::ByClassname() {

    for (local i = 0; i < 100; i++) {

        local ent = CreateByClassname( "logic_relay" )
        DispatchSpawn( ent )
        SetPropString( ent, "m_iName", "__relay" )
    }
}

function Benchmark::FromTable() {

    for (local i = 0; i < 100; i++) {

        SpawnEntityFromTable( "logic_relay", { targetname = "__relay" } )
    }
}

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

function Benchmark::EntityGroupFromTable() {

    // 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",
                vscripts = "rotatefix", // see func_rotating vdc page for this
                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)
            }
        },
    })
}

// ~15-25% faster for batch entity spawning
function Benchmark::PointScriptTemplate() {

    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",
        vscripts = "rotatefix",
        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)
    })

    script_template.AcceptInput( "ForceSpawn", null, null, null )
}

Result:

Configuration Results
SpawnEntityGroupFromTable 0.72ms
PointScriptTemplate 0.61ms

Iterating through players

When iterating over all players in the map, it is generally not recommended to use FindByClassname on the player entity in high playercount environments (>8-12 players). 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 in these circumstances.

The performance of player iteration depends heavily on how many players are actively in the server. In low playercount environments, the PlayerInstanceFromIndex approach is slower due to extra unnecessary iterations. In high playercount environments, `FindByClassname` runs a more expensive loop on every entity in the map to find players.

If you want the fastest option at the cost of complexity, you should collect player entities in your own global table or array in an event such as player_team or player_activate, remove them on player_disconnect, then iterate over that when necessary. Using a table gives you the added bonus of having a cache of player user IDs, which is faster to look up compared to reading the player_manager netprop.

Warning.pngWarning:player_activate does not fire for tfbots!

Benchmark

The first script must be executed before the second one!

Todo: update benchmarks
::ALL_PLAYERS <- {}
::Events <- {
    function OnGameEvent_player_team(params)
    {
        local player = GetPlayerFromUserID(params.userid)
        
        if ( player in ALL_PLAYERS ) return
        ALL_PLAYERS[ player ] <- params.userid
 
    }
    
    function OnGameEvent_player_disconnect(params) 
    {
        local player = GetPlayerFromUserID(params.userid)
    
        if ( !(player in ALL_PLAYERS) ) return

        delete ALL_PLAYERS[ 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 ALL_PLAYERS.keys())
{
    printl(player)
}

Result:

Configuration Results
FindByClassname 0.1289ms
Index iteration 0.0856ms
Array/Table iteration 0.0679ms

Squirrel Performance Tips

Arrays and Tables

Arrays in squirrel are, in practice, tables where the index is an integer value.

The .len() operator needs to first evaluate the data type (string or table/array), and uses a function call (expensive), we can avoid this overhead by directly checking the index.

/*****************
 * LENGTH CHECKS *
 *****************/
function Benchmark::Len() {

    for ( local i = 0; i < 1000; i++ )
        if ( arr.len() == 1000 )
            local len = true
}

// ~40% faster, no _OP_PREPCALLK/_OP_CALL instructions
function Benchmark::Idx() {

    for ( local i = 0; i < 1000; i++ )
        if ( 999 in arr && !(1000 in arr) )
            local len = true
}

Additionally, the integer 0 will return the value false in squirrel. For specifically checking an empty array, this falsy evaluation is slightly faster than directly checking if length equals 0

/****************************
 * EMPTY ARRAY/TABLE CHECKS *
 ****************************/
function Benchmark::LenExplicit() {

    for ( local i = 0; i < 1000; i++ )
        if ( arr.len() != 0 )
            local len = true
}

// ~2-5% faster, no _OP_NE instruction
function Benchmark::LenFalsy() {
    
    for ( local i = 0; i < 1000; i++ )
        if ( arr.len() )
            local len = true
}

Tables

As shown above, we can circumvent the performance cost of .len() by using direct index look-ups where possible. Instead of using .len() for tables, We can create a helper class with a "length" member, and add/subtract from this whenever we insert/delete an item from the table.

// direct length index lookups instead of .len() calls.
Benchmark.NewTable <- class {

    _tbl   = null // the real table in our class
    length = 0 // length variable, static so other functions can't override it.

    constructor( tbl = null ) {  this._tbl = ( tbl || {} ) ; this.length = this._tbl.len() }

    function get(k) { _tbl[k] }
    function set(k, v) { k in _tbl ? _tbl[k] = v : (length++, _tbl[k] <- v) }
    function del(k) { ( length--, delete _tbl[k] ) }
}

local tab = Benchmark.NewTable()
local _tbl = tab._tbl

// insert stuff into the table and increment the table length
for (local i = 0; i <= 1000; i++)
{
    tab.set("value_" + i, i )
}

// .len() eval
function Benchmark::Len() {
    for (local i = 0; i < 1000; i++)
        print(_tbl.len() == 1000)
}

// index lookup, ~2.5% faster
function Benchmark::Length() {
    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. You may only see performance benefits if you are checking table lengths a lot, but writing/reading infrequently

Benchmark

Configuration Results
Len 0.075ms
Idx 0.046ms
LenExplicit 0.071ms
LenFalsy 0.067ms
Len (table) 9.4ms
Length (table) 9.3ms

Variable look-up and caching

Squirrel will look for variables in the following order:

  1. local variables
  2. "outer" local variables (locals that are in parent scope)
  3. constants
  4. root table

For example:

  1. this will print the number 3 (outer local)
  2. commenting out local thing1 will print 2 (const)
  3. commenting out ::thing1 would print 1 (root)
  4. uncommenting local thing1 = 0 would print 0 (local)
::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 <- 0

function Benchmark::_OnDestroy() { delete ::SomeGlobalVar }

function Benchmark::SlowIncrement() 
{
    for (local i = 1; i <= 1000; i++)
        SomeGlobalVar++
}

// 10x faster!?
function Benchmark::FastIncrement()
{
    local myvar = SomeGlobalVar

    for (local i = 1; i <= 1000; i++)
        myvar++

    SomeGlobalVar = myvar
}

Root table lookups

Prefixing a root-scoped variable with :: will skip this traversal process and improve performance considerably.

function Benchmark::NormalLookup() {

    for (local i = 1; i <= 1000; i++)
        SomeGlobalVar++
}

// 10x faster!?
function Benchmark::RootLookup() {

    for (local i = 1; i <= 1000; i++)
        ::SomeGlobalVar++
}


Benchmark

Todo: this is the exact opposite result of the third-party benchmarking tool by a significant degree...?
Configuration Results
SlowIncrement 0.591ms
FastIncrement 0.021ms
NormalLookup 0.584ms
RootLookup 0.058ms