On a couple of other pages (here and here) I mentioned a technique for avoiding the cost of procedure suspension and resumption over a deeply nested stack of calls.
The question arises, how much time does this save?
Here is a simple test program which tries to answer this.
import io, util
global lim
procedure do_suspend(n)
if n = 0 then
suspend 1 to lim
else
suspend do_suspend(n - 1)
end
procedure do_coact(n)
if n = 0 then
every coact(1 to lim)
else
do_coact(n - 1)
end
procedure do_act_source(n)
if n = 0 then
every (1 to lim)@&source
else
do_act_source(n - 1)
end
procedure main(a)
local depth, e
depth := integer(a[1]) | 100
lim := integer(a[2]) | 1000000
&maxlevel := depth + 1000
write("Depth=", depth, " Generate=", lim)
note_time()
every do_suspend(depth)
note_time("procedure")
note_time()
e := create do_coact(depth)
while @e
note_time("coact()")
note_time()
e := create do_act_source(depth)
while @e
note_time("@&source")
write("Exit")
end
It creates a procedure call chain of a given depth, and produces a given number of results from the deepest level.
Three methods are used to produce the results :-
Conventional procedure suspension and resumption
Co-expression transmission with the coact
builtin function, and
Co-expression transmission using the @
operator.
The timing results are calculated and printed (in milliseconds) using the simple library procedure util.note_time
.
Run with the default for depth and number of results, I get the following results :-
$ oit -s ./bypass.icn -x
Depth=100 Generate=1000000
1522: procedure
193: coact()
49: @&source
Exit
With a shallow depth of 10 (and more results to give clearer timings), I get :-
$ oit -s ./bypass.icn -x 10 20000000
Depth=10 Generate=20000000
4009: procedure
3768: coact()
997: @&source
Exit
and with a depth of 1000, I get :-
$ oit -s ./bypass.icn -x 1000
Depth=1000 Generate=1000000
16506: procedure
174: coact()
50: @&source
Exit
These results show that for a depth of about 10, procedure call is about the same speed as coact
. In fact, if you try it for depths less than 10, you will find procedure call is rather faster. However, as the depth increases, coact
becomes relatively faster than procedure call.
Co-expression transmission with the @
operator seems to be consistently about 3-4 times faster than with the coact
function. This is just because invoking a builtin function is a relatively expensive operation when compared to invoking a builtin operator. However, the coact
function is more flexible than the @
operator. Particularly important is its ability to transmit a value without affecting the target co-expression’s &source
value; this is vital in avoiding co-expression black holes.
A more practical problem involves traversing a binary tree. Here is the next test program.
import io, util
class Node()
public const data
private l, r
public insert(val)
if val < data then
(/l := Node(val)) | l.insert(val)
else
(/r := Node(val)) | r.insert(val)
end
public traverse()
(\l).traverse()
coact(self)
(\r).traverse()
end
public traverse2()
(\l).traverse2()
self@&source
(\r).traverse2()
end
public gen()
suspend (\l).gen() | self | (\r).gen()
end
public depth()
local dl, dr
dl := (\l).depth() | 0
dr := (\r).depth() | 0
return 1 + max(dl, dr)
end
public new(i)
self.data := i
return
end
end
procedure main(a)
local root, e, r, n, i, v, nt
# n is the number of elements to insert
n := integer(a[1]) | 250000
# v is the range of values to insert (1..v)
v := integer(a[2]) | (n / 2)
# nt is the number of times to run each traversal (larger obviously
# gives more accuracy but takes longer).
nt := max(2000000 / n, 1)
# Make sure the procedure traversal doesn't exceed the call depth
# limit.
&maxlevel := n
# Create the tree
write("Creating tree with ", n, " entries in range 1..", v)
r := create |?v
root := Node(@r)
every i := 1 to n do {
root.insert(@r)
if i % 1000 = 0 then
writes("\e[K ", (i * 100) / n, "% complete\r")
}
write("\e[KComplete")
write("Tree depth = ", root.depth(), ", number of traversals = ", nt)
note_time()
every 1 to nt do
every root.gen()
note_time("procedure")
note_time()
every 1 to nt do {
e := create root.traverse()
while @e
}
note_time("coact()")
note_time()
every 1 to nt do {
e := create root.traverse2()
while @e
}
note_time("@&source")
end
This creates a simple binary tree and traverses over its elements, again using the three techniques from the first example.
With the default tree size, I get the following output :-
$ oit -s ./nodes.icn -x
Creating tree with 250000 entries in range 1..125000
Complete
Tree depth = 41, number of traversals = 8
1467: procedure
893: coact()
612: @&source
Here is much larger tree (this takes some time to build).
$ oit -s ./nodes.icn -x 5000000
Creating tree with 5000000 entries in range 1..2500000
Complete
Tree depth = 55, number of traversals = 1
4389: procedure
2408: coact()
1686: @&source
Finally, here is a small tree with only 200 entries.
$ oit -s ./nodes.icn -x 200
Creating tree with 200 entries in range 1..100
Complete
Tree depth = 16, number of traversals = 10000
769: procedure
657: coact()
388: @&source
Contents