Page 1 of 1

Sleep(), wait()

Posted: Fri Jan 06, 2017 12:52 pm
by mophydeen
Is there a way to make a code block sleep for a few tick, ms?

- os.clock is not available in factorio
- I could put some code in the event.on_tick and ignore most ticks. But intensive for the game ?

Re: Sleep(), wait()

Posted: Fri Jan 06, 2017 1:18 pm
by Nexela
The simplest way would probably be

function on_tick(event)
if event.tick % WAIT_TICKS == 0 then
--do code here
end
end

Re: Sleep(), wait()

Posted: Fri Jan 06, 2017 2:21 pm
by darkfrei

Code: Select all

function on_tick(event)
  if global.tickswait < 1 then
    --do code here
  else
    global.tickswait = global.tickswait - 1 -- 60 ticks pro second
  end
end
Don't forget to create it.

Re: Sleep(), wait()

Posted: Fri Jan 06, 2017 4:59 pm
by cpeosphoros
darkfrei wrote:

Code: Select all

function on_tick(event)
  if global.tickswait < 1 then
    --do code here
  else
    global.tickswait = global.tickswait - 1 -- 60 ticks pro second
  end
end
Don't forget to create it.
"if event.tick % WAIT_TICKS == 0" is way less expensive, performance-wise.

Re: Sleep(), wait()

Posted: Fri Jan 06, 2017 6:42 pm
by Rseding91
cpeosphoros wrote:
darkfrei wrote:

Code: Select all

function on_tick(event)
  if global.tickswait < 1 then
    --do code here
  else
    global.tickswait = global.tickswait - 1 -- 60 ticks pro second
  end
end
Don't forget to create it.
"if event.tick % WAIT_TICKS == 0" is way less expensive, performance-wise.
No it's not. That's about 6 times slower than doing the decrementing counter.

Re: Sleep(), wait()

Posted: Fri Jan 06, 2017 9:36 pm
by cpeosphoros
Rseding91 wrote:
cpeosphoros wrote:
darkfrei wrote:

Code: Select all

function on_tick(event)
  if global.tickswait < 1 then
    --do code here
  else
    global.tickswait = global.tickswait - 1 -- 60 ticks pro second
  end
end
Don't forget to create it.
"if event.tick % WAIT_TICKS == 0" is way less expensive, performance-wise.
No it's not. That's about 6 times slower than doing the decrementing counter.
Wow! Good to know. Going to switch to it then..

Re: Sleep(), wait()

Posted: Fri Jan 06, 2017 11:00 pm
by cpeosphoros
Ok, call me a stubborn OCD'ed person ;), but I went and time profiled it.

This is the harness:

Code: Select all

require "LOGGER"

local function tickCounter(tick)
	global.tickswait = global.tickswait - 1
	if global.tickswait == 0 then
		global.tickswait = 60
		global.counter = global.counter + 1
	end

end

local function tickModuler(tick)
	if tick % 60 == 0 then
		global.counter = global.counter + 1
	end
end

local function doCounter(f, limit, message)
	global.counter = 0

	for tick = 1, limit do
		f(tick)
	end

	LOGGER.log(message .. " " .. global.counter)
end

local function runCounter()

	global.tickswait = 60

	LOGGER.log("Starting counters")

	doCounter(tickCounter, 1200, "Counted")
	doCounter(tickModuler, 1200, "Moduled")

	doCounter(tickCounter, 12000, "Counted")
	doCounter(tickModuler, 12000, "Moduled")

	doCounter(tickCounter, 120000, "Counted")
	doCounter(tickModuler, 120000, "Moduled")

	global.tickswait = nil
	global.counter = nil

end

local function onTick(event)
	if not done then
		runCounter()
end

script.on_event(defines.events.on_tick, onTick)
And these the results (edited for readability):

Code: Select all

00000.283: 85:51:12.32: Counted 20
00000.179: 85:51:12.32: Moduled 20

00002.038: 85:51:12.32: Counted 200
00000.612: 85:51:12.32: Moduled 200

00018.513: 85:51:12.32: Counted 2000
00005.653: 85:51:12.32: Moduled 2000
First column values are time, in milliseconds.

For the larger ranges, moduling performed about three times as fast as counting down, when doing exactly the same work (in this case, incrementing a global var).

Otoh, unless you have literally hundreds of thousands mods, that will do practically no noticeable difference at all, performance-wise.

Re: Sleep(), wait()

Posted: Mon Jan 09, 2017 9:14 pm
by darkfrei
Dividing ist difficult. It's better to multiply, or adding something. For example adding -1.

Re: Sleep(), wait()

Posted: Wed Jan 11, 2017 5:14 pm
by cpeosphoros
darkfrei wrote:Dividing ist difficult. It's better to multiply, or adding something. For example adding -1.
The problem is apparently not in the math operation, but In table accessing. Doing some caching for global.tick, etc shaves some milliseconds off, but the module approach is still cheaper, due to no table access.

Re: Sleep(), wait()

Posted: Wed Jan 11, 2017 6:48 pm
by Klonan
cpeosphoros wrote:Ok, call me a stubborn OCD'ed person ;), but I went and time profiled

For the larger ranges, moduling performed about three times as fast as counting down, when doing exactly the same work (in this case, incrementing a global var).

Otoh, unless you have literally hundreds of thousands mods, that will do practically no noticeable difference at all, performance-wise.
Could you try profiling another 'countdown' code which is something like this:

Code: Select all

function check_action()
  if not game.tick == global.action_tick then return end
  global.action_tick = game.tick + 300 --Do the action every 300 ticks
  do_something()
end

Re: Sleep(), wait()

Posted: Thu Jan 12, 2017 10:04 am
by bobingabout
cpeosphoros wrote:"if event.tick % WAIT_TICKS == 0" is way less expensive, performance-wise.
I've done assembly programming before.

what's the difference between ==, ~=, >, <, >= and <= for descissions?

Well... when you actually look down at the raw machine code produced... one of these comparator operators of an if block boils down to...
Subtract one number from the other, Then check the Carry flag, or Zero flag.

Keep in mind that > is the opposite of <=, so asking "A >= B" is the same as asking if "NOT (A < B)", and you've basically reduced those 6 checks to 3 "if check, then do this, else do that" checks.

A > or < check is basically subtracting one number from the other and performing a Carry bit check. Subtracting the larger number from the smaller number will set the carry flag, if this isn't the case (They're the same, or you subtracted the smaller from the larger) then the carry flag is not set. So to perform a greater than check, you subtract the number you think is bigger from the number you think is smaller. If the carry flag gets set, then you are correct, and the number is greater than the other one. If it is false, then it's less than or equal to. swapping around the then, and else (internally remember) basically changes your greater than check to a less than or equal to check, so they're the same check. Swapping the two numbers around (subtracting the one you think is smaller from the one you think is larger) will perform the less than vs greater than or equal to check instead. So just doing this one thing can perform all 4 of these checks, the only thing that changes is which number is subtracted from the other, then do you perform the action if the carry is set, or not?

Equal to is similar, subtract any one from the other, and check the zero flag. this will only ever be set if both numbers were the same. Again, you can do something If it's true, or something if it's not true, so swap these around, and you have a not equal to check.


So, internally, this is what each check looks like:
(Depending on system, might be some memory copy to working register instructions here)
subtract one number from the other.
check a flag bit.
goto for true.
goto for false.

That's 4 instructions specifically for the check. for all checks. The only thing that changes is which number you subtract from which, and which system flag you check.


I hope I was insightful here.

Re: Sleep(), wait()

Posted: Sat Jan 14, 2017 1:39 pm
by cpeosphoros
Klonan wrote:Could you try profiling another 'countdown' code which is something like this:
All right, I did it. Nexela also sent me a PM with another implementation, which gives the flexibility of performing a different action each time actionTick comes up - in my test harness I went on and performed the same action every time, since wee need uniformity for profiling purposes, but the flexibility Nexela's method provides is very welcome.

I've also done some other implementations of the decrementer, (ab)using some different features of Lua, to see if any of them would give better performance.

Just for testing purposes, I've done two different implementations for Klonan's code. One with not(==) and one with ~=.

Also for testing purposes, I've also came up with a naive implementation of the decremeter, without saving to global. That implementation will not, of course, work in normal gaming environment, since it will not remember the counter's previous value between actual calls.

So, this is the harness (sorry for the long code block - I've omitted the on_tick call, requires, etc - They are the same as in the previous harness):

Code: Select all

local naive = 60

local function doSomething()
	global.counter = global.counter + 1
end

local function decrementerCounter1(tick)
   global.tickswait = global.tickswait - 1
   if global.tickswait == 0 then
      global.tickswait = 60
      doSomething()
   end
end

local function decrementerCounter2(tick)
	local tw = global.tickswait - 1
	if tw == 0 then
		global.tickswait = 60
		doSomething()
	else 
		global.tickswait = tw
	end
end

local function decrementerCounter3(tick)
	local tw = global.tickswait - 1
	if tw == 0 then
		doSomething()
	end
	global.tickswait = (tw == 0) and 60 or tw
end

local function incCounter(tw)
	if (tw == 0) then
		doSomething()
		return true
	end
	return false
end

local function decrementerCounter4(tick)
	local tw = global.tickswait - 1
	global.tickswait = (incCounter(tw) and 60 or tw)
end

local function klonanCounter1(tick)
	if not (tick == global.actionTick) then return end
	global.actionTick = tick + 60 --Do the action every 60 ticks
	doSomething()
end

local function klonanCounter2(tick)
	if tick ~= global.actionTick2 then return end
	global.actionTick2 = tick + 60 --Do the action every 60 ticks
	doSomething()
end

local function nexelaCounter(tick)
	action = global.queue[tick]
	if not action then return end
	action.toDo()
	global.queue[tick] = nil
	global.queue[tick + 60] = action --Do the action every 60 ticks
end

local function naiveCounter(tick)
	naive = naive - 1
	if naive == 0 then
		naive = 60
		doSomething()
	end
end


local function tickModuler(tick)
	if tick % 60 == 0 then
		doSomething()
	end
end

local function doCounter(f, limit, message)
	global.counter = 0
	for tick = 1, limit do
		f(tick)
	end
	LOGGER.log(message .. " " .. global.counter)
end

local function setGlobals(v)
	global.tickswait = 60
	global.actionTick = 60
	global.actionTick2 = 60
	global.queue = {}
	global.queue[60] = {toDo = doSomething}
	LOGGER.log("Starting counters v=" .. v)
end

local function nilGlobals()
	global.tickswait = nil
	global.actionTick = nil
	global.actionTick2 = nil
	global.queue = nil
end

function runCounter()
	for _, v in ipairs({1200, 12000, 120000}) do
		setGlobals(v)

		doCounter(decrementerCounter1, v, "Decrementer 1 Counted -")
		doCounter(decrementerCounter2, v, "Decrementer 2 Counted -")
		doCounter(decrementerCounter3, v, "Decrementer 3 Counted -")
		doCounter(decrementerCounter4, v, "Decrementer 4 Counted -")
		doCounter(klonanCounter1, v, "Klonan 1 Counted -")
		doCounter(klonanCounter2, v, "Klonan 2 Counted -")
		doCounter(nexelaCounter, v, "Nexela Counted -")
		doCounter(naiveCounter, v, "Naive Counted -")
		doCounter(tickModuler, v, "Moduled -")
	end
	nilGlobals()
end
And the results (edited for readability):

Code: Select all

00001.073: 102:43:33.28: Decrementer 1 Counted - 20
00000.982: 102:43:33.28: Decrementer 2 Counted - 20
00000.982: 102:43:33.28: Decrementer 3 Counted - 20
00001.045: 102:43:33.28: Decrementer 4 Counted - 20
00000.978: 102:43:33.28: Klonan 1 Counted - 20
00000.521: 102:43:33.28: Klonan 2 Counted - 20
00000.248: 102:43:33.28: Nexela Counted - 20
00000.114: 102:43:33.28: Naive Counted - 20
00000.105: 102:43:33.28: Moduled - 20

00001.846: 102:43:33.28: Decrementer 1 Counted - 200
00001.441: 102:43:33.28: Decrementer 2 Counted - 200
00001.831: 102:43:33.28: Decrementer 3 Counted - 200
00002.342: 102:43:33.28: Decrementer 4 Counted - 200
00001.001: 102:43:33.28: Klonan 1 Counted - 200
00001.008: 102:43:33.28: Klonan 2 Counted - 200
00001.387: 102:43:33.28: Nexela Counted - 200
00000.665: 102:43:33.28: Naive Counted - 200
00000.590: 102:43:33.28: Moduled - 200

00018.336: 102:43:33.28: Decrementer 1 Counted - 2000
00014.130: 102:43:33.28: Decrementer 2 Counted - 2000
00017.860: 102:43:33.28: Decrementer 3 Counted - 2000
00022.216: 102:43:33.28: Decrementer 4 Counted - 2000
00009.666: 102:43:33.28: Klonan 1 Counted - 2000
00009.568: 102:43:33.28: Klonan 2 Counted - 2000
00013.678: 102:43:33.28: Nexela Counted - 2000
00006.288: 102:43:33.28: Naive Counted - 2000
00005.519: 102:43:33.28: Moduled - 2000
Analysis:
- Moduling slightly outperformed even the naive decrementer implementation. The only explanation I can see for that behavior is that probably Lua's implementation of the module operator works internally as a simple subtraction, not a division at all.
- As expected, caching global values is a bit better than accessing them directly (Decremeter 2 vs. Decrementer 1).
- Contrary to Lua performance guides, if-then-else outperformed the ternary and-or construct (Decremeter 3 vs. Decrementer 2), and inserting a function inside the ternary is even uglier (Decrementer 4).
- Klonan's approach is very fast and second best only to directly moduling values, and gives the flexibility to variable cycle lengths. You can decide internally in your cycle code when the next cycle will come up.
- Curiously, not(==) and ~= performed almost exactly the same, which suggests Lua treats both as the same operation, internally.
- Nexela's approach is par with the best decrementer, and gives, on top of what Klonan's provide, also the flexibility of changing behavior for the next cycle.
- Again, unless you have hundred thousands of mods doing that, the time difference between all those approaches is completely negligible, in the order of a couple milliseconds.

Re: Sleep(), wait()

Posted: Sat Jan 14, 2017 2:05 pm
by cpeosphoros
bobingabout wrote:So, internally, this is what each check looks like:
(Depending on system, might be some memory copy to working register instructions here)
subtract one number from the other.
check a flag bit.
goto for true.
goto for false.

That's 4 instructions specifically for the check. for all checks. The only thing that changes is which number you subtract from which, and which system flag you check.


I hope I was insightful here.
Ultimately, you are right, of course.

But Lua - as any other interpreted language - will add some layers of complexity to that.

Put simply, the work of preprocessing code to the Virtual Machine's opcode, and then actually running it on the VM will sometimes upend the traditional thinking we are used to with assembly code, or even with compiled languages.

As an example, with the profiling code I've provided above, I truly expected the naive decrementer to outperform the module operator, as Rseding91 have said before, probably by an entire magnitude order. But the actual decrementing code went on and surprised me by performing almost the same, even a bit slower, than the module.

In the end, however, as a mod writer, I think I shall stick with code readability, maintainability and flexibility, in that order, as what I will choose for the methods of counting. The time difference between the different implementations is almost negligible, in the order of tenths of milliseconds, unless you have thousands of mods.

I think moduling, Klonan's and Nexela's implementations are the best ones for now. Moduling is simple, readable, maitainable and works very fast, without much flexibility. Klonan's and Nexela's each adds one layer of flexibility, without sacrificing much of readability and maintainability, at the cost of some performance, which is still negligible in the order of hundreds or even a couple thousands of mods.

Re: Sleep(), wait()

Posted: Sat Jan 14, 2017 2:57 pm
by Nexela
Awesome findings. Thanks for looking into it.

Re: Sleep(), wait()

Posted: Sat Jan 14, 2017 3:29 pm
by cpeosphoros
Nexela wrote:Awesome findings. Thanks for looking into it.
You're welcome. You may find those results interesting, too: viewtopic.php?f=25&t=39069