User talk:Braindawg: Difference between revisions

From Valve Developer Community
Jump to navigation Jump to search
No edit summary
Line 5: Line 5:
== Folding Functions ==
== Folding Functions ==


Folding functions in the context of VScript means folding them into the root table.  It is recommended that you do this for functions that are commonly used in expensive operations, and for functions that have unique names that are not shared by other classes.  Not only is this more readable and easier to write, but it also skips the extra step where the game needs to first find the function before executing it.
Folding functions in the context of VScript means folding them into the root table.  It is recommended that you do this for functions that are commonly used in expensive operations.  Not only is this more readable and easier to write, but it also skips the extra step where the game needs to first find the function before executing it.


The following example folds all NetProp related functions into the root table.  for example, <code>NetProps.GetPropString(...)</code> would simply become <code>GetPropString(...)</code>.  This can yield performance improvements of up to 20%{{confirm}}
=== Benchmark ===
 
{{Note|Benchmark done on tc_hydro}}


<source lang=js>
<source lang=js>
::ROOT <- getroottable()
foreach(k, v in ::NetProps.getclass())
foreach(k, v in ::NetProps.getclass())
if (k != "IsValid" && !(k in ROOT))
if (k != "IsValid" && !(k in ROOT))
ROOT[k] <- ::NetProps[k].bindenv(::NetProps)
ROOT[k] <- ::NetProps[k].bindenv(::NetProps)


local gamerules = Entities.FindByClassname(null, "tf_gamerules")
foreach(k, v in ::Entities.getclass())
local gamerules_targetname = GetPropString(gamerules, "m_iName") // "NetProps." prefix is no longer necessary
if (k != "IsValid" && !(k in ROOT))
ROOT[k] <- ::Entities[k].bindenv(::Entities)
 
BeginBenchmark()
 
for (local prop; prop = Entities.FindByClassname(prop, "prop_dynamic");)
    NetProps.GetPropString(prop, "m_iName")
 
printf("unfolded took %.4f ms\n", EndBenchmark());
 
BeginBenchmark()
 
for (local prop; prop = FindByClassname(prop, "prop_dynamic");)
    GetPropString(prop, "m_iName")
 
printf("folded took %.4f ms\n", EndBenchmark());
</source>
</source>


=== Benchmark ===
Result:
{{todo|Add Benchmark}}
 
{| class="standard-table"
! Configuration
! Results
|-
|<code>Unfolded</code>
|<code>0.1439ms</code>
|-
|<code>Folded</code>
|<code>0.0999ms</code>
|-
|}


== Constants ==
== Constants ==
Line 26: Line 55:


Similar to folding functions, folding pre-defined Constant values into the constant table (or the root table) makes referencing them less cumbersome, as well as increasing performance.  
Similar to folding functions, folding pre-defined Constant values into the constant table (or the root table) makes referencing them less cumbersome, as well as increasing performance.  
===Benchmark===


<source lang=js>
<source lang=js>
::ROOT <- getroottable();
local j = 0
if (!("ConstantNamingConvention" in ROOT)) // make sure folding is only done once
BeginBenchmark()
{
for (local i = 1; i <= Constants.Server.MAX_EDICTS; i++)
foreach (a,b in Constants)
    j++
foreach (k,v in b)
printf("unfolded constant took %.4f ms\n", EndBenchmark());
ROOT[k] <- v != null ? v : 0;
 
}
const MAX_EDICTS = 2048
local e = 0
BeginBenchmark()
for (local i = 1; i <= MAX_EDICTS; i++)
    e++
printf("folded constant took %.4f ms\n", EndBenchmark());
</source>
</source>


You may have noticed that we are folding our constants into the root table instead of the constant table. This will be explained below
Result:
 
{| class="standard-table"
! Configuration
! Results
|-
|<code>Unfolded</code>
|<code>0.1127ms</code>
|-
|<code>"Folded"</code>
|<code>0.0423ms</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 can be faster (up to 1.5x faster to be more precise), it may not be feasible to fold your constants into the 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 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, 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 ===
{{todo|Add Benchmark}}
<source lang=js>
::ROOT_VALUE <- 2
const CONST_VALUE = 2
 
BeginBenchmark();
 
for (local i = 0; i <= 10000; i++)
    i += CONST_VALUE
 
printf("CONST took %.4f ms\n", EndBenchmark());
 
BeginBenchmark();
 
for (local i = 0; i <= 10000; i++)
    i += ROOT_VALUE
 
printf("ROOT took %.4f ms\n", EndBenchmark());
</source>
 
Result:
 
{| class="standard-table"
! Configuration
! Results
|-
|<code>Constant</code>
|<code>0.0767ms</code>
|-
|<code>Root</code>
|<code>0.1037ms</code>
|-
|}


== String Formatting ==  
== String Formatting ==  
Line 124: Line 203:
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.


In situations like this, CreateByClassname + DispatchSpawn is roughly 4x faster in comparison to <code>SpawnEntityFromTable</code>, as there is no extra step with needing to parse a table of keyvalues.
In situations like this, CreateByClassname + DispatchSpawn is roughly 4x faster in comparison to <code>SpawnEntityFromTable</code>.


=== Benchmark ===
=== Benchmark ===
Line 152: Line 231:
</source>
</source>


result:
Result:
table took 0.0806 ms
 
manual took 0.0201 ms
{| class="standard-table"
! Configuration
! Results
|-
|<code>SpawnEntityFromTable</code>
|<code>0.0428ms</code>
|-
|<code>CreateByClassname</code>
|<code>0.0156ms</code>
|}


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


When spawning multiple entities at the same time, it is more efficient to use SpawnEntityGroupFromTable 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 SpawnEntityGroupFromTable 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.
 
point_script_template is not only more flexible, but significantly 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 ===
<source lang=js>
//spawn origins are right outside of bigrock spawn
BeginBenchmark()
 
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)
        }
    },
})
printf("SpawnEntityGroupFromTable took %.4f ms\n", EndBenchmark());
 
BeginBenchmark()
 
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)
 
printf("point_script_template took %.4f ms\n", EndBenchmark());
</source>


SpawnEntityGroupFromTable has several major limitations in comparison to point_script_template.  The most significant limitation is it does not return any entity handles that can be accessed elsewhere in the script, and is generally not a good option in comparison to point_script_template.


{{todo|investigate these further, add benchmarks}}
Result:
 
{| class="standard-table"
! Configuration
! Results
|-
|<code>SpawnEntityGroupFromTable</code>
|<code>0.1752ms</code>
|-
|<code>point_script_template</code>
|<code>0.0197ms</code>
|}

Revision as of 22:18, 14 April 2024

This page includes performance tips and tricks for VScript. Many of these tips can be used in other games, however, all of these were tested in Team Fortress 2 Team Fortress 2. Your mileage may vary in other games.

Warning.pngWarning:Only optimize your scripts if you need to! Some of these tips may introduce extra unnecessary complexity to your project. Premature optimization without knowing where your performance issues actually come from is extremely ill-advised.

Folding Functions

Folding functions in the context of VScript means folding them into the root table. It is recommended that you do this for functions that are commonly used in expensive operations. Not only is this more readable and easier to write, but it also skips the extra step where the game needs to first find the function before executing it.

Benchmark

Note.pngNote:Benchmark done on tc_hydro
::ROOT <- getroottable()
foreach(k, v in ::NetProps.getclass())
	if (k != "IsValid" && !(k in ROOT))
		ROOT[k] <- ::NetProps[k].bindenv(::NetProps)

foreach(k, v in ::Entities.getclass())
	if (k != "IsValid" && !(k in ROOT))
		ROOT[k] <- ::Entities[k].bindenv(::Entities)

BeginBenchmark()

for (local prop; prop = Entities.FindByClassname(prop, "prop_dynamic");)
    NetProps.GetPropString(prop, "m_iName")

printf("unfolded took %.4f ms\n", EndBenchmark());

BeginBenchmark()

for (local prop; prop = FindByClassname(prop, "prop_dynamic");)
    GetPropString(prop, "m_iName")

printf("folded took %.4f ms\n", EndBenchmark());

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) makes referencing them less cumbersome, as well as increasing performance.

Benchmark

local j = 0
BeginBenchmark()
for (local i = 1; i <= Constants.Server.MAX_EDICTS; i++)
    j++
printf("unfolded constant took %.4f ms\n", EndBenchmark());

const MAX_EDICTS = 2048
local e = 0
BeginBenchmark()
for (local i = 1; i <= MAX_EDICTS; i++)
    e++
printf("folded constant took %.4f ms\n", EndBenchmark());

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

BeginBenchmark();

for (local i = 0; i <= 10000; i++)
    i += CONST_VALUE

printf("CONST took %.4f ms\n", EndBenchmark());

BeginBenchmark();

for (local i = 0; i <= 10000; i++)
    i += ROOT_VALUE

printf("ROOT took %.4f ms\n", EndBenchmark());

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() does not support formatting entity handles and other VScript-specific datatypes, however it does support formatting strings, integers, and floats. It is also significantly faster than concatenation.

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(). Interestingly though, 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 keyvalues = { responsecontext = "" }

BeginBenchmark();
for (local i = 0; i < 10000; i++)
    keyvalues.responsecontext <- mins.x.tostring() + "," + mins.y.tostring() + "," + mins.z.tostring() + "," + maxs.x.tostring() + "," + maxs.y.tostring() + "," + maxs.z.tostring()
printf("concat took %.4f ms\n", EndBenchmark());

BeginBenchmark();
for (local i = 0; i < 10000; i++)
    keyvalues.responsecontext <- format("%g,%g,%g,%g,%g,%g", mins.x, mins.y, mins.z, maxs.x, maxs.y, maxs.z)
printf("format took %.4f ms\n", EndBenchmark());    

BeginBenchmark();
    
for (local i = 0; i < 10000; i++)
    keyvalues.responsecontext <- format("%s %s", mins.ToKVString(), maxs.ToKVString())
printf("kvstring took %.4f ms\n", EndBenchmark());    

BeginBenchmark();
for (local i = 0; i < 10000; i++)
    keyvalues.responsecontext <- mins.ToKVString() + " " + maxs.ToKVString()
printf("kvstring concat took %.4f ms\n", EndBenchmark());

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 functions

- SpawnEntityFromTable function

- SpawnEntityGroupFromTable function

- point_script_template entity + AddTemplate functions

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

BeginBenchmark();
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,
});
printf("table took %.4f ms\n", EndBenchmark());

BeginBenchmark();
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);
printf("manual took %.4f ms\n", EndBenchmark());

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 not only more flexible, but significantly 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
BeginBenchmark()

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)
        }
    },
})
printf("SpawnEntityGroupFromTable took %.4f ms\n", EndBenchmark());

BeginBenchmark()

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)

printf("point_script_template took %.4f ms\n", EndBenchmark());


Result:

Configuration Results
SpawnEntityGroupFromTable 0.1752ms
point_script_template 0.0197ms