Page 1 of 1

Multi-tasking with 1700 Tasks or more

Posted: 22 January 2008, 21:26 PM
by mikep
So how many tasks can you run on your microcontroller? I have wanted to write this code for a long time. For those of who remember, the old BasicX-01 claimed it could do 1000 simultaneous tasks.

Here is the code to run 1700 tasks on your ZX-128e/ZX-1281e/ZX-1281/ZX-1280 with 64K RAM. The tasks are simple counters but who cares; this is a stress test and a good showcase of the ZX platform capabilities.

Here is some sample output (without the optional dump of all 1700 counters every 5 seconds).

Code: Select all

ZBasic v2.3
RAM heap Size is 63019
Creating 1700 tasks:
....................................................................................................100
....................................................................................................200
....................................................................................................300
....................................................................................................400
....................................................................................................500
....................................................................................................600
....................................................................................................700
....................................................................................................800
....................................................................................................900
....................................................................................................1000
....................................................................................................1100
....................................................................................................1200
....................................................................................................1300
....................................................................................................1400
....................................................................................................1500
....................................................................................................1600
....................................................................................................1700
Elapsed time is 15.29688 seconds
***** Elapsed time is 0.0429687 seconds
Average count is 0.0
***** Elapsed time is 5.693359 seconds
Average count is 143.2771
***** Elapsed time is 5.640625 seconds
Average count is 79.12118
***** Elapsed time is 5.638672 seconds
Average count is 69.62706
***** Elapsed time is 5.642578 seconds
Average count is 74.13824

Re: Multi-tasking with 1700 Tasks

Posted: 24 January 2008, 11:20 AM
by mikep
mikep wrote:Here is the code to run 1700 tasks on your ZX-128e/ZX-1281e/ZX-1281/ZX-1280 with 64K RAM. The tasks are simple counters but who cares; this is a stress test and a good showcase of the ZX platform capabilities.
If you have a ZX-1281e, ZX-1281 or ZX-1280, the maximum number of tasks needs to be reduced to 1696 (1700 won't quite fit onto the heap).

Posted: 24 January 2008, 17:34 PM
by stevech
1700 tasks...
Doctor, it hurts when I do *this*.
- reply: Well, don't do that!

Posted: 24 January 2008, 17:46 PM
by mikep
stevech wrote:1700 tasks...
Doctor, it hurts when I do *this*.
- reply: Well, don't do that!
Maybe I missing your point here Steve.

The point is that ZBasic *can* do this painlessly and easily. It should give you a lot of confidence in writing an application with 10 or 20 complex tasks. You just need to follow the usual guidelines with multi-tasking - see AN209 and AN210.

Posted: 24 January 2008, 20:00 PM
by stevech
I was just being sarcastic to a fault about 1700 tasks, this being a 6 sigma use case!

Posted: 25 January 2008, 0:20 AM
by mikep
stevech wrote:I was just being sarcastic to a fault about 1700 tasks, this being a 6 sigma use case!
Quite so. I decided that I needed to go one step further and write the testcase so it could run on any ZX platform with a simple change to the TargetDevice and a recompile. In the process I was also able to increase the maximum number of tasks to 2021. Here is a table of task count by platform (not including the main task). I was unable to test a ZX-1280 as I don't own one :(

[table][mrow] Device[col]Tasks[col]Tasks with ext RAM
[row]ZX-24e, ZX-24, ZX-40, ZX-44[col]45[col]
[row]ZX-24ae, ZX-24a, ZX-40a, ZX-44a[col]109[col]
[row]ZX-128e[col]109[col]2021
[row]ZX-1281e, ZX-1281[col]237[col]2014
[/table]
If you compile this code yourself you may get one or both of these warning messages. They can be safely ignored.

Code: Select all

manytask2.bas:3: Warning: this Option directive has no effect for the target device ZX24e
manytask2.bas:51: Warning: parameter 1 of "Task" is defined as ByRef, using parameters passed by value is advised
It is instructive to read this source to see how I dynamically allocated both the memory for the tasks and the array memory for the counters.

Here is the source code to the task. Note the use of an alias for the byte array pointer (memory address) to convert it into the correct type for the counter.

Code: Select all

Public Sub Task(ByRef counterMemory() as Byte)
	Dim counter as Byte Alias counterMemory
	Do
		counter = counter + 1
		' Give some other task a chance after one increment
		Call Sleep(0)
	Loop
End Sub
Amazingly the ZBasic compiler figures this all out and converts it to a very simple increment of an indirect address on the stack. Here are the VM instructions:

Code: Select all

PSHR_W         bp-6      
INCI_B                   
PSHI_W         0x0000 (0)
SCALL          SLEEP                     
BRA            024d
The use of aliased variables to access data at a given memory address is an advanced but powerful feature. Use with care.

With some simple changes, it is also possible to change the byte counter to a Integer or Long counter. I will leave this as an exercise for the reader - there are several clues to help you in the source code. Here is some sample output using an Integer counter:

Code: Select all

Available RAM is 3584 so number of tasks is 106
Creating 106 tasks:
....................................................................................................100
......
Elapsed time is 0.0585937 seconds

***** Elapsed time is 0.0195312 seconds
Average count is 0.0

***** Elapsed time is 5.042969 seconds
Average count is 1252.67

***** Elapsed time is 5.042969 seconds
Average count is 2505.349

***** Elapsed time is 5.044922 seconds
Average count is 3757.99
This larger counter is not wrapping so in this case it is possible to see that the VM achieved on average of about 26,556 integer increments per second for 106 tasks (or about 250 per task per second). For 1791 tasks, the average number of increments reduces to 4990 per second (or about 3 per task per second). These counts are less than what one task would achieve because of thrashing. The maximum for a single task is on average 27,250 counts per second.

Posted: 25 January 2008, 7:03 AM
by mikep
mikep wrote:This larger counter is not wrapping so in this case it is possible to see that the VM achieved on average of about 26,556 integer increments per second for 106 tasks (or about 250 per task per second). For 1791 tasks, the average number of increments reduces to 4990 per second (or about 3 per task per second). These counts are less than what one task would achieve because of thrashing. The maximum for a single task is on average 27,250 counts per second.
I believe that one of the areas of thrashing is not really thrashing per se. Each timer tick, the VM has to go through each task and decrement it's sleep count and when it gets to zero, the task is runnable again. This is not noticeable for a few tasks but really mounts up if you have 2000 tasks and need to do this 512 times a second.

For my latest testcase (manytask2.bas), I set the sleep count to 0 which means that the task is probably marked runnable without waiting for a clock tick, but nevertheless the VM still has to examine each task every tick. Again for a small number of tasks (<10) this is not noticeable.

In case you are wondering about why I'm bothering with this stress test. The point is that stress tests often uncover problems that exist even in non-stress situations but are "less" noticeable or only happen "randomly". I wouldn't be surprised if Don adds this test into his test bucket.

Posted: 25 January 2008, 9:48 AM
by dkinzer
mikep wrote:I decided [to] write the testcase so it could run on any ZX platform [...]
Nice work.
mikep wrote:If you compile this code yourself you may get one or both of these warning messages.
You can eliminate the first one by using a conditional:

Code: Select all

#if &#40;Option.CPUType = "mega1281"&#41; Or &#40;Option.CPUType = "mega1280"&#41; Or &#40;Option.CPUType = "mega128"&#41;
Option Ramsize 64*1024-Register.RamStart
#endif
The second warning can be eliminated if you pass the address of the task's counter as an integral address. Technically, the net effect is that you're passing a parameter by reference but the compiler can't see that that is so. The generated code is same either way.

Code: Select all

Public Sub Task&#40;ByVal counterAddr as UnsignedInteger&#41;
  Dim counter as Byte Based counterAddr
    Do
      counter = counter + 1
      ' Give some other task a chance after one increment
      Call Sleep&#40;0&#41;
    Loop
End Sub

Posted: 25 January 2008, 10:46 AM
by mikep
dkinzer wrote:You can eliminate the first one by using a conditional
I knew about this but I just didn't get to it.
dkinzer wrote:The second warning can be eliminated if you pass the address of the task's counter as an integral address.
I haven't used the Based keyword that much so I learnt something here. Thanks.

Posted: 25 January 2008, 11:40 AM
by dkinzer
mikep wrote:I haven't used the Based keyword that much so I learnt something here.
The idea is the same as based variables in PL/M, PL/1 and other languages. It can be used to accomplish many of the same effects as using pointers and casting in C/C++. The equivalent in C using a macro would be:

Code: Select all

#define val&#40;valAddr&#41; *&#40;unsigned *&#41;&#40;valAddr&#41;
Essentially, you're telling the compiler to generate code to access memory at a specified address (which can be constant or computed at run-time) as if it were a variable of a given type.

An Alias in ZBasic is a restricted form of a Based variable in that the base address is a compile-time constant.