Jump to content

What is better for performance?


Recommended Posts

Case 1:

local prefab = "evergreen" --could be anything elselocal interval = 1 --secAddPrefabPostInit(prefab,function(inst)	inst:DoPeriodicTask(interval,function(inst)		--Do some staff with high CPU load	end)end)

And case 2:

local prefab = "evergreen" --could be anything elselocal interval = 1 --secAddPrefabPostInit(prefab,function(inst)	inst:AddTag("some_tag")end)AddPrefabPostInit("forest",function(inst)	inst:DoPeriodicTask(interval,function(inst)		local ents = GLOBAL.TheSim:FindEntities(0,0,0,1200,{"some_tag"})		for i,v in ipairs(ents) do			--Do some staff with high CPU load		end	end)end)

​What is better?

Link to comment
Share on other sites

@Maris, Both are pretty inefficient, I would say, but the first is better-- FindEntities is pretty CPU-expensive, I think.

 

Instead, what you want to do is have a periodic task on the world, that refers to a list of entities. Have AddPrefabPostInit on the entities to add them to the list (and make sure you remove them with the onremove). For efficiency reason, you probably want to make the list have the entities as keys, and the value can be whatever you want-- some lists just use the entity again as the value.

Link to comment
Share on other sites

@Maris, checking every prefab every second will cause massive CPU consumption and performance issues no matter what you are doing. It's a bad practice and shouldn't be used ever. Instead look for a common factor in what you are wanting to do and push an event which your prefab is listening for to do what you want to do. This will ensure that it 1) only runs when you want it to run, 2) causes less CPU performance related issues and 3) is the best practice to use when programming.

 

You can even listen for an event and push another event, however, that is technically bad practice as well if you are only listening and pushing a different event. You could easily just listen for the event and do what you are wanting to do without the overhead from the additional code.

 

~Kzisor/Ysovuka

Link to comment
Share on other sites

@Maris, how do you determine if an item is unwanted? What is the criteria for the unwantedness? I'm trying to determine exactly which events you should listen for in order to make it most efficient.

Item is unwanted if:

1) There are too many items of the same type in the world

2) Item was unused for long time.

3) Nobody even see this item for long time.

4) There is no players near item.

Link to comment
Share on other sites

Item is unwanted if:

1) There are too many items of the same type in the world

2) Item was unused for long time.

3) Nobody even see this item for long time.

4) There is no players near item.

ListenFor "entitysleep" -> DoTaskInTime (long time) -> inst:Remove()

ListenFor "entitywake" -> cancel above timed callback

You may also want to store how much time is left in "long time" as savedata, to keep the garbage collection timer across save reloading. Though, since all entities are awaken at the first game tick, you'd have to be careful in only resuming the "entitywake" event listening until later into the game.

But I can't see how achieving what you're trying to achieve would be beneficial, since it's essentially intentional savedata corruption.

Edited by simplex
Link to comment
Share on other sites

@simplex, I think he's trying to achieve clutter reduction. I think that's the term? In a game where items disappear after a while so the world doesn't get clogged up with tons and tons of items. It's a pretty common mechanic.

 

I'm not sure that modding it in will be very effective, though.

 

Edit: Ah, understood. I mostly agree with you-- I hate having to worry about things timing out on me. I was just thrown off by you calling it "savedata corruption".

Edited by rezecib
Link to comment
Share on other sites

@simplex, I think he's trying to achieve clutter reduction. I think that's the term? In a game where items disappear after a while so the world doesn't get clogged up with tons and tons of items. It's a pretty common mechanic.

 

I'm not sure that modding it in will be very effective, though.

I get the idea, and I get the performance motivation in reducing entity count (Klei's SelfStacker component was written precisely for that). I just have a strong objection to despawnable entities in a persistent sandbox world.

But either way, the good thing about mods is that you'll only use them if you want to, so my subjective view on what's proposed here isn't really significant.

Link to comment
Share on other sites

@outseeker, Well, sleeping entities don't really do anything while asleep, but it does create more things to wake/sleep as players move around.

The overhead comes from Lua's garbage collector. Entities (as in "inst.entity") are userdata, which means they are objects exported from the engine, but with their memory allocation handled by Lua's garbage collector. Lua collects garbage incrementally, i.e. in piecewise chunks, but there are some exceptions. Userdata form one of these exceptions: all userdata are checked for deallocation in one pass, which means a larger userdata count causes some periodic stutter. So even asleep entities have a significant impact on performance.

Anyway, this is how I'd implement this:

local assert = GLOBAL.assertlocal ipairs = GLOBAL.ipairslocal math = GLOBAL.math---local DESPAWN_DELAY = 4*TUNING.TOTAL_DAY_TIME-- Tags which, if present, should make an entity non-despawnable.local FORBIDDEN_TAGS = {"irreplaceable", "nonpotatable"}-- Condition for an entity to be despawnablelocal function is_despawnable(inst)	if not inst.components.inventoryitem then		return false	end	for _, tag in ipairs(FORBIDDEN_TAGS) do		if inst:HasTag(tag) then			return false		end	end	return trueend-----[[-- The callbacks are defined inside, on a per entity basis, in order to-- have access to metadata (such as a variable holding the callback)-- without storing info in the entity itself, which prevents naming-- clashes.--]]local function MakeDespawnable(inst, delay)	assert( delay > 0 )	-- Holds the callback.	local cb = nil	-- Holds the despawn time.	local despawn_time = nil	--[[	-- This extra layer of indirection is just to play nicely with a	-- hypothetical mod overriding inst.Remove dynamically.	--]]	local function doremove(inst)		inst:Remove()	end	local function onwake(inst)		if cb then			cb:Cancel()			cb = nil		end		despawn_time = nil	end	local function onsleep(inst)		if cb then			assert( despawn_time )			return		end		local actual_delay		if despawn_time then			actual_delay = math.max(0, despawn_time - GLOBAL.GetTime())		else			actual_delay = delay		end		cb = inst:DoTaskInTime(actual_delay, doremove)		despawn_time = GLOBAL.GetTime() + actual_delay	end	local function setupevents(inst)		inst:ListenForEvent("entitysleep", onsleep)		inst:ListenForEvent("entitywake", onwake)		if inst:IsAsleep() then			onsleep(inst)		else			onwake(inst)		end	end	local old_onsave, old_onload = inst.OnSave, inst.OnLoad	inst.OnSave = function(inst, data)		if old_onsave then			old_onsave(inst, data)		end		if despawn_time then			data.despawn_delay_left = math.max(0, despawn_time - GLOBAL.GetTime())		end	end	inst.OnLoad = function(inst, data)		if old_onload then			old_onload(inst, data)		end		if data.despawn_delay_left then			despawn_time = GLOBAL.GetTime() + data.despawn_delay_left		end	end	inst:DoTaskInTime(0.1 + 0.4*math.random(), setupevents)end---if GLOBAL.TheNet:GetIsMasterSimulation() then	AddPrefabPostInitAny(function(inst)		if is_despawnable(inst) then			MakeDespawnable(inst, DESPAWN_DELAY)		end	end)end

The above is only applied over items with the inventoryitem component, but this can be easily tweakable by changing the is_despawnable function.

Edited by simplex
Link to comment
Share on other sites

  • Developer
Lua collects garbage incrementally, i.e. in piecewise chunks, but there are some exceptions. Userdata form one of these exceptions: all userdata are checked for deallocation in one pass

 

Just for your knowledge, DS runs with a modified garbage collector where this is not the case, in order to reduce spikes in the atomic phase. 

Link to comment
Share on other sites

Just for your knowledge, DS runs with a modified garbage collector where this is not the case, in order to reduce spikes in the atomic phase.

Could you clarify? I understand I might be basing myself on outdated info, but my confusion stems from Lua's C API not allowing customisation of the garbage collector, only of the allocator function, so your statement would rely on changing the source code for the Lua VM itself.

That is, unless what you're referring to is simply keeping the garbage collector paused and then doing a full collection cycle every N frames (which is a common strategy for games using Lua, so I hear).

EDIT: By the way, is there any reason why entities are not light userdata, instead of full userdata? Since entities are explicitly removed ("inst.entity:Retire()"), they need not be garbage collected anyway, so they could be represented via light userdata for performance gains due to removing the garbage collection overhead. To keep the current interface, inst.entity could be a table with a metatable replicating the current engine level entity API, storing the actual entity as a light userdata in, say, "inst.entity[1]", and passing that to the underlying C++ functions.

Edited by simplex
Link to comment
Share on other sites

  • Developer

@simplex

 

Yes, DS runs with a modified garbage collector at the C level to reduce the impact of the atomic phase and to allow true timeslicing. The common practice is what DS used to do, but it won`t prevent gc spikes which is why I had to dive in. The garbage collector overhead itself is minimal and fairly controllable now so the gains from using light userdata wrt gc wouldn`t be what I was after (the other overhead related to it might make it worthwhile to explore light userdata but sadly (or rather, fortunately!) I am not scrambling to find tasks to fill my day ;)

 

Link to comment
Share on other sites

@simplex

 

Yes, DS runs with a modified garbage collector at the C level to reduce the impact of the atomic phase and to allow true timeslicing. The common practice is what DS used to do, but it won`t prevent gc spikes which is why I had to dive in. The garbage collector overhead itself is minimal and fairly controllable now so the gains from using light userdata wrt gc wouldn`t be what I was after (the other overhead related to it might make it worthwhile to explore light userdata but sadly (or rather, fortunately!) I am not scrambling to find tasks to fill my day ;)

Oh, certainly if the gc overhead wrt entity count was otherwise controlled the performance gains from representing entities via light userdata wouldn't be that large. Some performance, in the form of minimised memory fragmentation and increased locality of reference, could be gained by using light userdata together with a special purpose pool allocator (or by simply storing entities in a deque), but those gains would hardly be significant enough to justify the effort.

But was true timeslicing throughout the atomic gc phase achieved by actually editing lua-5.1.5/src/lgc.c, or did you manage to somehow do it though Lua's C API alone? I understand you may be unwilling or unable to discuss implementation details, what I'm mostly interested in is whether DST runs over a modified Lua VM or not.

Link to comment
Share on other sites

  • Developer
But was true timeslicing throughout the atomic gc phase achieved by actually editing lua-5.1.5/src/lgc.c

 

DST definitely uses a modified VM, including lgc.c. Don't get me wrong, the atomic phase itself is not timesliced (trust me I tried) but regular sweep has been timesliced to prevent spikiness when entering single gc steps, and the atomic phase has been shortened by pulling out userdata finalization. I considered publishing the changes as they are different from anything out there (and I am aware of the bitsquid article), but they may only be appliclable to our specific use of udata (and I lack the time to investigate) which is why I didn't.

 

The gains from using a pool allocator for the entities would be minimal (outside of fragmentation), given that most time is spent in the lua code itself and adding enough control to lua to be able to make it so that class members or data access in general would be cache friendly (without changing the lua codebase in ways that would make it very unfun to mod or develop) would seem like a very ambitious stretch.

Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
 Share

×
  • Create New...