LUA Blueprint and Category lookup speeds - FAForever/fa GitHub Wiki

TestThread = function(self)
    WARN("Test thread activated")

    local t = GetSystemTimeSecondsOnlyForProfileUse()
    for i = 1, 10000 do
        local cat = EntityCategoryContains(categories.AIR, self)
    end
    t = GetSystemTimeSecondsOnlyForProfileUse() - t
    WARN("Test 1 complete, time elapsed for EntityCategoryContains is " .. t)
    
    t = GetSystemTimeSecondsOnlyForProfileUse()
    for i = 1, 10000 do
        local bp = self:GetBlueprint()
        local cat = bp.CategoriesHash.AIR
    end
    t = GetSystemTimeSecondsOnlyForProfileUse() - t
    WARN("Test 2 complete, time elapsed for self:GetBlueprint followed by bp.CategoriesHash.AIR is " .. t)
    
    t = GetSystemTimeSecondsOnlyForProfileUse()
    for i = 1, 10000 do
        local bp = GetBlueprint(self)
        local cat = bp.CategoriesHash.AIR
    end
    t = GetSystemTimeSecondsOnlyForProfileUse() - t
    WARN("Test 3 complete, time elapsed for globally localised GetBlueprint(self) followed by bp.CategoriesHash.AIR is " .. t)
    
    t = GetSystemTimeSecondsOnlyForProfileUse()
    for i = 1, 10000 do
        local cat = BluePrints[self.UnitId].CategoriesHash.AIR
    end
    t = GetSystemTimeSecondsOnlyForProfileUse() - t
    WARN("Test 4 complete, time elapsed for globally localised __blueprints table lookup of BluePrints = __blueprints.Unit -> BluePrints[self.UnitId].CategoriesHash.AIR is " .. t)
    
    t = GetSystemTimeSecondsOnlyForProfileUse()
    for i = 1, 10000 do
        local cat = __blueprints.Unit[self.UnitId].CategoriesHash.AIR
    end
    t = GetSystemTimeSecondsOnlyForProfileUse() - t
    WARN("Test 5 complete, time elapsed for __blueprints.Unit[self.UnitId].CategoriesHash.AIR is " .. t)
    
    t = GetSystemTimeSecondsOnlyForProfileUse()
    for i = 1, 10000 do
        local cat = _G.__blueprints.Unit[self.UnitId].CategoriesHash.AIR
    end
    t = GetSystemTimeSecondsOnlyForProfileUse() - t
    WARN("Test 6 complete, time elapsed for _G.__blueprints.Unit[self.UnitId].CategoriesHash.AIR is " .. t)
    
    local bp = GetBlueprint(self)
    t = GetSystemTimeSecondsOnlyForProfileUse()
    for i = 1, 10000 do
        local cat = bp.CategoriesHash.AIR
    end
    t = GetSystemTimeSecondsOnlyForProfileUse() - t
    WARN("Test 7 complete, time elapsed for cases where local bp = GetBlueprint(self) has been done for other reasons already, followed by bp.CategoriesHash.AIR is " .. t)
end,

This code resulted the following:

WARNING: Test thread activated
WARNING: Test 1 complete, time elapsed for EntityCategoryContains is 0.00274658203125
WARNING: Test 2 complete, time elapsed for self:GetBlueprint followed by bp.CategoriesHash.AIR is 0.00299072265625
WARNING: Test 3 complete, time elapsed for globally localised GetBlueprint(self) followed by bp.CategoriesHash.AIR is 0.00274658203125
WARNING: Test 4 complete, time elapsed for globally localised __blueprints table lookup of BluePrints = __blueprints.Unit -> BluePrints[self.UnitId].CategoriesHash.AIR is 0.00079345703125
WARNING: Test 5 complete, time elapsed for __blueprints.Unit[self.UnitId].CategoriesHash.AIR is 0.00103759765625
WARNING: Test 6 complete, time elapsed for _G.__blueprints.Unit[self.UnitId].CategoriesHash.AIR is 0.00115966796875
WARNING: Test 7 complete, time elapsed for cases where local bp = GetBlueprint(self) has been done for other reasons already, followed by bp.CategoriesHash.AIR is 0.000244140625

From this we can see several things.

Calling self:GetBlueprint() is slightly slower than using a local GetBlueprint(self) defined as local GetBlueprint = moho.entity_methods.GetBlueprint at top of file.

Where bp has already been fetched, it is much faster to simply grab the value from bp.CategoriesHash[cat] than to use EntityCategoryContains

Where it hasn't. the hash lookup + a new function call to get the blueprint is slower than just using EntityCategoryContains

All methods of getting a blueprint directly from the Global table are faster than either of the functions, but using local bps = __blueprints.Unit at top of file results in the fastest of these (It skips checking _G and __blueprints and jumps right in)

Conclusion:

  • All unit blueprint lookups should be changed from using functions to using bps[self.UnitId], with UnitId stored in OnCreate. Further investigation of function vs lookup below.
  • Where a single category is being looked up, use the CategoriesHash blueprint entry

Additional testing of a more realistic set of situations is as follows:

    TestThread = function(self)
        WARN('Test thread activated')

        local t = GetSystemTimeSecondsOnlyForProfileUse()
        for i = 1, 10000 do
            local bp = GetBlueprint(self)
            local a = bp.Air.AutoLandTime
            local b = bp.Defense.Health
            local c = bp.Defense.RegenRate
            local d = bp.Economy.BuildCostEnergy
            local e = bp.Economy.BuildCostMass
        end
        t = GetSystemTimeSecondsOnlyForProfileUse() - t
        WARN('Test 1 complete, time elapsed for GetBlueprint(self) is ' .. t)

        local t = GetSystemTimeSecondsOnlyForProfileUse()
        for i = 1, 10000 do
            local bp = self:GetBlueprint()
            local a = bp.Air.AutoLandTime
            local b = bp.Defense.Health
            local c = bp.Defense.RegenRate
            local d = bp.Economy.BuildCostEnergy
            local e = bp.Economy.BuildCostMass
        end
        t = GetSystemTimeSecondsOnlyForProfileUse() - t
        WARN('Test 1 complete, time elapsed for self:GetBlueprint() is ' .. t)

        local t = GetSystemTimeSecondsOnlyForProfileUse()
        for i = 1, 10000 do
            local bp = bps[self.UnitId]
            local a = bp.Air.AutoLandTime
            local b = bp.Defense.Health
            local c = bp.Defense.RegenRate
            local d = bp.Economy.BuildCostEnergy
            local e = bp.Economy.BuildCostMass
        end
        t = GetSystemTimeSecondsOnlyForProfileUse() - t
        WARN('Test 1 complete, time elapsed for local bps[self.UnitId] is ' .. t)

        local t = GetSystemTimeSecondsOnlyForProfileUse()
        for i = 1, 10000 do
            local a = bps[self.UnitId].Air.AutoLandTime
            local b = bps[self.UnitId].Defense.Health
            local c = bps[self.UnitId].Defense.RegenRate
            local d = bps[self.UnitId].Economy.BuildCostEnergy
            local e = bps[self.UnitId].Economy.BuildCostMass
        end
        t = GetSystemTimeSecondsOnlyForProfileUse() - t
        WARN('Test 1 complete, time elapsed for bps[self.UnitId] instead of any local is ' .. t)
    end,

Resulting in:

WARNING: Set unit ID to uea0303
WARNING: Test thread activated
WARNING: Test 1 complete, time elapsed for GetBlueprint(self) is 0.0035743713378906
WARNING: Test 1 complete, time elapsed for self:GetBlueprint() is 0.0038337707519531
WARNING: Test 1 complete, time elapsed for local bps[self.UnitId] is 0.0019111633300781
WARNING: Test 1 complete, time elapsed for bps[self.UnitId] instead of any local is 0.00372314453125

And so we find that, by replacing all function-based Blueprint lookup calls with a local based on the __blueprints table, we cut execution time roughly in half. Small gains, but it's something!