* * * * *
A missed optimization in Lua
Yesterday [1], I made brief mention of optimizing some Lua [2] code, and said
it was for another post.
This is said post.
The code in question (not identical, but this exhibits the same problem):
> require "ddt".tron() -- trace the execution
> local lpeg = require "lpeg"
>
> local Cs = lpeg.Cs
> local C = lpeg.C
> local R = lpeg.R
> local P = lpeg.P
>
> function canonical(header)
> local function Pi(text)
>
> local pattern = P(true)
> for c in text:gmatch(".") do
> pattern = pattern * (P(c:lower()) + P(c:upper()))
> end
>
> return pattern / text
> end
>
> local ALPHA = R("AZ","az")
>
> local id = Pi "ID" -- exceptions to Camel-Case
> + Pi "MIME"
> + Pi "CSeq"
>
> local word = (C(ALPHA) * C(ALPHA^0))
> / function(i,r)
> return i:upper() .. r:lower()
> end
> local other = C((P(1) - ALPHA)^1)
> local hdr = Cs((id + word + other)^1)
>
> return hdr:match(header)
> end
>
> print(canonical "return-from")
> print(canonical "message-id")
> print(canonical "mime-version")
> print(canonical "cseq")
>
The code in question transforms a header name like return-from to the
canonical form Return-From; it'll also transform ReTuRn-FRom into the
canonical form. The code is used to match headers in Internet based messages
like email, HTTP (HyperText Transport Protocol) or SIP (Session Initiation
Protocol) (as the header names need to match case-insensitively—I'll leave
how it works to you, the reader (here are some hints [3]) since what the code
does isn't germane to this discussion). Now, when you trace the execution,
you'll notice something:
> @./ddt.lua 187: end
> @code.lua 2: local lpeg = require "lpeg"
> @code.lua 4: local Cs = lpeg.Cs
> @code.lua 5: local C = lpeg.C
> @code.lua 6: local R = lpeg.R
> @code.lua 7: local P = lpeg.P
> @code.lua 34: end
> @code.lua 9: function canonical(header)
> @code.lua 36: print(canonical "return-from")
> @code.lua 18: end
> @code.lua 20: local ALPHA = R("AZ","az")
> @code.lua 22: local id = Pi "ID" -- exceptions to Camel-Case
> @code.lua 12: local pattern = P(true)
> @code.lua 13: for c in text:gmatch(".") do
> @code.lua 14: pattern = pattern * (P(c:lower()) + P(c:upper()))
> @code.lua 13: for c in text:gmatch(".") do
> @code.lua 14: pattern = pattern * (P(c:lower()) + P(c:upper()))
> @code.lua 13: for c in text:gmatch(".") do
> @code.lua 17: return pattern / text
> @code.lua 23: + Pi "MIME"
> @code.lua 12: local pattern = P(true)
> @code.lua 13: for c in text:gmatch(".") do
> @code.lua 14: pattern = pattern * (P(c:lower()) + P(c:upper()))
> @code.lua 13: for c in text:gmatch(".") do
> @code.lua 14: pattern = pattern * (P(c:lower()) + P(c:upper()))
> @code.lua 13: for c in text:gmatch(".") do
> @code.lua 14: pattern = pattern * (P(c:lower()) + P(c:upper()))
> @code.lua 13: for c in text:gmatch(".") do
> @code.lua 14: pattern = pattern * (P(c:lower()) + P(c:upper()))
> @code.lua 13: for c in text:gmatch(".") do
> @code.lua 17: return pattern / text
> @code.lua 24: + Pi "CSeq"
> @code.lua 12: local pattern = P(true)
> @code.lua 13: for c in text:gmatch(".") do
> @code.lua 14: pattern = pattern * (P(c:lower()) + P(c:upper()))
> @code.lua 13: for c in text:gmatch(".") do
> @code.lua 14: pattern = pattern * (P(c:lower()) + P(c:upper()))
> @code.lua 13: for c in text:gmatch(".") do
> @code.lua 14: pattern = pattern * (P(c:lower()) + P(c:upper()))
> @code.lua 13: for c in text:gmatch(".") do
> @code.lua 14: pattern = pattern * (P(c:lower()) + P(c:upper()))
> @code.lua 13: for c in text:gmatch(".") do
> @code.lua 17: return pattern / text
> @code.lua 26: local word = (C(ALPHA) * C(ALPHA^0))
> @code.lua 29: end
> @code.lua 30: local other = C((P(1) - ALPHA)^1)
> @code.lua 31: local hdr = Cs((id + word + other)^1)
> @code.lua 33: return hdr:match(header)
> @code.lua 28: return i:upper() .. r:lower()
> @code.lua 28: return i:upper() .. r:lower()
> @code.lua 37: print(canonical "message-id")
> @code.lua 18: end
> @code.lua 20: local ALPHA = R("AZ","az")
> @code.lua 22: local id = Pi "ID" -- exceptions to Camel-Case
> @code.lua 12: local pattern = P(true)
> @code.lua 13: for c in text:gmatch(".") do
> @code.lua 14: pattern = pattern * (P(c:lower()) + P(c:upper()))
> @code.lua 13: for c in text:gmatch(".") do
> @code.lua 14: pattern = pattern * (P(c:lower()) + P(c:upper()))
> @code.lua 13: for c in text:gmatch(".") do
> @code.lua 17: return pattern / text
> @code.lua 23: + Pi "MIME"
> @code.lua 12: local pattern = P(true)
> @code.lua 13: for c in text:gmatch(".") do
> @code.lua 14: pattern = pattern * (P(c:lower()) + P(c:upper()))
> @code.lua 13: for c in text:gmatch(".") do
> @code.lua 14: pattern = pattern * (P(c:lower()) + P(c:upper()))
> @code.lua 13: for c in text:gmatch(".") do
> @code.lua 14: pattern = pattern * (P(c:lower()) + P(c:upper()))
> @code.lua 13: for c in text:gmatch(".") do
> @code.lua 14: pattern = pattern * (P(c:lower()) + P(c:upper()))
> @code.lua 13: for c in text:gmatch(".") do
> @code.lua 17: return pattern / text
> @code.lua 24: + Pi "CSeq"
> @code.lua 12: local pattern = P(true)
> @code.lua 13: for c in text:gmatch(".") do
> @code.lua 14: pattern = pattern * (P(c:lower()) + P(c:upper()))
> @code.lua 13: for c in text:gmatch(".") do
> @code.lua 14: pattern = pattern * (P(c:lower()) + P(c:upper()))
> @code.lua 13: for c in text:gmatch(".") do
> @code.lua 14: pattern = pattern * (P(c:lower()) + P(c:upper()))
> @code.lua 13: for c in text:gmatch(".") do
> @code.lua 14: pattern = pattern * (P(c:lower()) + P(c:upper()))
> @code.lua 13: for c in text:gmatch(".") do
> @code.lua 17: return pattern / text
> @code.lua 26: local word = (C(ALPHA) * C(ALPHA^0))
> @code.lua 29: end
> @code.lua 30: local other = C((P(1) - ALPHA)^1)
> @code.lua 31: local hdr = Cs((id + word + other)^1)
> @code.lua 33: return hdr:match(header)
> @code.lua 28: return i:upper() .. r:lower()
> @code.lua 38: print(canonical "mime-version")
> @code.lua 18: end
> @code.lua 20: local ALPHA = R("AZ","az")
> @code.lua 22: local id = Pi "ID" -- exceptions to Camel-Case
> @code.lua 12: local pattern = P(true)
> @code.lua 13: for c in text:gmatch(".") do
> @code.lua 14: pattern = pattern * (P(c:lower()) + P(c:upper()))
> @code.lua 13: for c in text:gmatch(".") do
> @code.lua 14: pattern = pattern * (P(c:lower()) + P(c:upper()))
> @code.lua 13: for c in text:gmatch(".") do
> @code.lua 17: return pattern / text
> @code.lua 23: + Pi "MIME"
> @code.lua 12: local pattern = P(true)
> @code.lua 13: for c in text:gmatch(".") do
> @code.lua 14: pattern = pattern * (P(c:lower()) + P(c:upper()))
> @code.lua 13: for c in text:gmatch(".") do
> @code.lua 14: pattern = pattern * (P(c:lower()) + P(c:upper()))
> @code.lua 13: for c in text:gmatch(".") do
> @code.lua 14: pattern = pattern * (P(c:lower()) + P(c:upper()))
> @code.lua 13: for c in text:gmatch(".") do
> @code.lua 14: pattern = pattern * (P(c:lower()) + P(c:upper()))
> @code.lua 13: for c in text:gmatch(".") do
> @code.lua 17: return pattern / text
> @code.lua 24: + Pi "CSeq"
> @code.lua 12: local pattern = P(true)
> @code.lua 13: for c in text:gmatch(".") do
> @code.lua 14: pattern = pattern * (P(c:lower()) + P(c:upper()))
> @code.lua 13: for c in text:gmatch(".") do
> @code.lua 14: pattern = pattern * (P(c:lower()) + P(c:upper()))
> @code.lua 13: for c in text:gmatch(".") do
> @code.lua 14: pattern = pattern * (P(c:lower()) + P(c:upper()))
> @code.lua 13: for c in text:gmatch(".") do
> @code.lua 14: pattern = pattern * (P(c:lower()) + P(c:upper()))
> @code.lua 13: for c in text:gmatch(".") do
> @code.lua 17: return pattern / text
> @code.lua 26: local word = (C(ALPHA) * C(ALPHA^0))
> @code.lua 29: end
> @code.lua 30: local other = C((P(1) - ALPHA)^1)
> @code.lua 31: local hdr = Cs((id + word + other)^1)
> @code.lua 33: return hdr:match(header)
> @code.lua 28: return i:upper() .. r:lower()
> @code.lua 39: print(canonical "cseq")
> @code.lua 18: end
> @code.lua 20: local ALPHA = R("AZ","az")
> @code.lua 22: local id = Pi "ID" -- exceptions to Camel-Case
> @code.lua 12: local pattern = P(true)
> @code.lua 13: for c in text:gmatch(".") do
> @code.lua 14: pattern = pattern * (P(c:lower()) + P(c:upper()))
> @code.lua 13: for c in text:gmatch(".") do
> @code.lua 14: pattern = pattern * (P(c:lower()) + P(c:upper()))
> @code.lua 13: for c in text:gmatch(".") do
> @code.lua 17: return pattern / text
> @code.lua 23: + Pi "MIME"
> @code.lua 12: local pattern = P(true)
> @code.lua 13: for c in text:gmatch(".") do
> @code.lua 14: pattern = pattern * (P(c:lower()) + P(c:upper()))
> @code.lua 13: for c in text:gmatch(".") do
> @code.lua 14: pattern = pattern * (P(c:lower()) + P(c:upper()))
> @code.lua 13: for c in text:gmatch(".") do
> @code.lua 14: pattern = pattern * (P(c:lower()) + P(c:upper()))
> @code.lua 13: for c in text:gmatch(".") do
> @code.lua 14: pattern = pattern * (P(c:lower()) + P(c:upper()))
> @code.lua 13: for c in text:gmatch(".") do
> @code.lua 17: return pattern / text
> @code.lua 24: + Pi "CSeq"
> @code.lua 12: local pattern = P(true)
> @code.lua 13: for c in text:gmatch(".") do
> @code.lua 14: pattern = pattern * (P(c:lower()) + P(c:upper()))
> @code.lua 13: for c in text:gmatch(".") do
> @code.lua 14: pattern = pattern * (P(c:lower()) + P(c:upper()))
> @code.lua 13: for c in text:gmatch(".") do
> @code.lua 14: pattern = pattern * (P(c:lower()) + P(c:upper()))
> @code.lua 13: for c in text:gmatch(".") do
> @code.lua 14: pattern = pattern * (P(c:lower()) + P(c:upper()))
> @code.lua 13: for c in text:gmatch(".") do
> @code.lua 17: return pattern / text
> @code.lua 26: local word = (C(ALPHA) * C(ALPHA^0))
> @code.lua 29: end
> @code.lua 30: local other = C((P(1) - ALPHA)^1)
> @code.lua 31: local hdr = Cs((id + word + other)^1)
> @code.lua 33: return hdr:match(header)
>
There's quite a bit of code executed. That's because the Lua compiler isn't
sufficiently smart [4] to notice that most of the code in canonical() never
changes—it's independent of the passed in parameter and thus, it could be
compiled once. And it's this behavior that I noticed the other day. It's an
easy fix (just lift the invarient code out of the function body) and the
results are about a third the processing:
> @./ddt.lua 187: end
> @c3.lua 2: local lpeg = require "lpeg"
> @c3.lua 4: local Cs = lpeg.Cs
> @c3.lua 5: local C = lpeg.C
> @c3.lua 6: local R = lpeg.R
> @c3.lua 7: local P = lpeg.P
> @c3.lua 18: end
> @c3.lua 20: local ALPHA = R("AZ","az")
> @c3.lua 22: local id = Pi "ID" -- exceptions to Camel-Case
> @c3.lua 12: local pattern = P(true)
> @c3.lua 13: for c in text:gmatch(".") do
> @c3.lua 14: pattern = pattern * (P(c:lower()) + P(c:upper()))
> @c3.lua 13: for c in text:gmatch(".") do
> @c3.lua 14: pattern = pattern * (P(c:lower()) + P(c:upper()))
> @c3.lua 13: for c in text:gmatch(".") do
> @c3.lua 17: return pattern / text
> @c3.lua 23: + Pi "MIME"
> @c3.lua 12: local pattern = P(true)
> @c3.lua 13: for c in text:gmatch(".") do
> @c3.lua 14: pattern = pattern * (P(c:lower()) + P(c:upper()))
> @c3.lua 13: for c in text:gmatch(".") do
> @c3.lua 14: pattern = pattern * (P(c:lower()) + P(c:upper()))
> @c3.lua 13: for c in text:gmatch(".") do
> @c3.lua 14: pattern = pattern * (P(c:lower()) + P(c:upper()))
> @c3.lua 13: for c in text:gmatch(".") do
> @c3.lua 14: pattern = pattern * (P(c:lower()) + P(c:upper()))
> @c3.lua 13: for c in text:gmatch(".") do
> @c3.lua 17: return pattern / text
> @c3.lua 24: + Pi "CSeq"
> @c3.lua 12: local pattern = P(true)
> @c3.lua 13: for c in text:gmatch(".") do
> @c3.lua 14: pattern = pattern * (P(c:lower()) + P(c:upper()))
> @c3.lua 13: for c in text:gmatch(".") do
> @c3.lua 14: pattern = pattern * (P(c:lower()) + P(c:upper()))
> @c3.lua 13: for c in text:gmatch(".") do
> @c3.lua 14: pattern = pattern * (P(c:lower()) + P(c:upper()))
> @c3.lua 13: for c in text:gmatch(".") do
> @c3.lua 14: pattern = pattern * (P(c:lower()) + P(c:upper()))
> @c3.lua 13: for c in text:gmatch(".") do
> @c3.lua 17: return pattern / text
> @c3.lua 26: local word = (C(ALPHA) * C(ALPHA^0))
> @c3.lua 29: end
> @c3.lua 30: local other = C((P(1) - ALPHA)^1)
> @c3.lua 31: local hdr = Cs((id + word + other)^1)
> @c3.lua 35: end
> @c3.lua 33: function canonical(header)
> @c3.lua 35: end
> @c3.lua 38: print(canonical "return-from")
> @c3.lua 34: return hdr:match(header)
> @c3.lua 28: return i:upper() .. r:lower()
> @c3.lua 28: return i:upper() .. r:lower()
> @c3.lua 39: print(canonical "message-id")
> @c3.lua 34: return hdr:match(header)
> @c3.lua 28: return i:upper() .. r:lower()
> @c3.lua 40: print(canonical "mime-version")
> @c3.lua 34: return hdr:match(header)
> @c3.lua 28: return i:upper() .. r:lower()
> @c3.lua 41: print(canonical "cseq")
> @c3.lua 34: return hdr:match(header)
>
I also recorded the execution with LuaJIT (Lua Just-In-Time) [5] (it's faster
because it compiles Lua into native code) and it, too, is not sufficiently
smart to lift the constant code out of the function. It may be that detecting
this is hard for a compiler to do, or that such transformations might not be
considered safe (due to possible side effects).
In any case, I found it a bit surprising (although in retrospect, it
shouldn't have been).
[1]
gopher://gopher.conman.org/0Phlog:2015/02/12.1
[2]
http://www.lua.org/
[3]
http://www.inf.puc-rio.br/~roberto/lpeg/
[4]
http://c2.com/cgi/wiki?SufficientlySmartCompiler
[5]
http://luajit.org/
Email author at
[email protected]