Lua Coroutines Versus Python Generators

lua-users home

The next version of Javascript will apparently have a feature very much like Python generators; the alternative of Lua-style coroutines was rejected after a long debate which is summarised on [Neil Mix's blog].

How do Python generators differ from Lua coroutines? The most important feature is that in Lua, coroutine.yield() is an ordinary function which can be invoked anywhere in the dynamic extent of a coroutine.resume() with the limitation that you cannot yield through a C callback (unless you use MikePall 's [Coco] library.) In Python, yield is syntactic, and can only be in the lexical body of the generator function.

This means that Python generators must be written as generators, and cannot easily be factored into smaller functions; nor can they easily be recursive. You can achieve recursion by a form of chaining; a message on the [Python mailing list] describes both the mechanism:

def inorder(t):
     if t:
         for x in inorder(t.left):
             yield x
         yield t.label
         for x in inorder(t.right):
             yield x

In Lua, one might write a more agnostic inorder function as a higher-order function:

function inorder(f, t)
  if t then
    inorder(f, t.left)
    return inorder(f, t.right)

The Lua function could then be used either as the iterator in a for loop:

for label in coroutine.wrap(inorder), coroutine.yield, t do
  -- something with label

or as a sort of foreach function:

inorder(print, t)

In an attempt to reduce the comparison of recursive generators to its minimum, I wrote a pair of simple programs which generate the infinite [Ruler function]. (A more interesting page about this function is Michael Naylor's [Abacaba-Dabacaba] which includes a musical exploration.)

The ruler function can be generated non-recursively, but in this example it's standing in for something like a depth-first search, so we'll just look at the recursive implementation. The programs generate the first 2^k values in the sequence and add them up as a sort of validity test, since it's easy to show that the sum of the first 2^k elements of the ruler function is 2^(k+1)-1. Python's built-in functions and standard library were handy here; in Lua, I had to implement the sum and islice (first k elements of a sequence) functions myself, but fortunately that's not difficult.

So here's the Lua implementation:

function ruler(put, k)
  for i = 1, k do
    ruler(put, i-1)
function eachruler()
  return coroutine.wrap(function()
    return ruler(coroutine.yield, 100)
function sumseq(k, f, o, s)
  local sum = 0   
  for v in f, o, s do
    k, sum = k-1, sum + v
    if k <= 0 then break end
  return sum
local howmany = tonumber(arg[1]) or 24
local now = os.clock()
local sum = sumseq(2^howmany, eachruler())
local took = os.clock()-now
print(("%2i: %7.3f seconds  %i check %i"):format(howmany, took, sum, 2^(howmany+1)-1))

and the very similar and somewhat shorter Python implementation:

from itertools import islice
from sys import argv
from time import clock
def ruler(k):
  for i in range(1, k+1):
    yield i
    for x in ruler(i-1): yield x
howmany = int(argv[1])
now = clock()
total = sum(islice(ruler(100), 2**howmany))
took = clock()-now
print "%2d: %7.3f seconds %d check %d" % (howmany, took, total, 2**(howmany+1)-1)

The slightly odd formulation of the recursion is the result of manually changing the tailcall into a for loop, because Python doesn't do tailcalls, and the original tailcall implementation put Python at even more of a disadvantage.

Since both programs passed the check, I'll only paste the comparative timings here (in seconds as reported above). I don't believe this is simply a "Lua is faster than Python" benchmark; I think it shows that coroutines are inherently faster; the main difference is not having to pass the values up through a chain of yields.

 N      Lua   Python
--    -----   ------
12:   0.000    0.008
13:   0.000    0.023 
14:   0.008    0.055 
15:   0.016    0.109 
16:   0.031    0.227 
17:   0.078    0.469 
18:   0.148    0.961 
19:   0.305    1.961 
20:   0.602    4.000 
21:   1.211    8.148 
22:   2.430   17.211 
23:   4.820   40.094 
24:   9.875   94.992 

RecentChanges · preferences
edit · history
Last edited September 8, 2009 2:02 pm GMT (diff)