Assert caveats

General discussion about LÖVE, Lua, game development, puns, and unicorns.
Post Reply
User avatar
pgimeno
Party member
Posts: 3688
Joined: Sun Oct 18, 2015 2:58 pm

Assert caveats

Post by pgimeno »

A few days ago I did some experiments about the use of assert and was going to write about the results here. Then I thought it was worth a blog post, so I asked on IRC. Since it's not really LÖVE-specific, I was advised to post it on lua.space. So I did.

Here's the post, if someone is interested:

http://lua.space/general/assert-usage-caveat

Thanks to undef for the help polishing and debugging it.
User avatar
slime
Solid Snayke
Posts: 3172
Joined: Mon Aug 23, 2010 6:45 am
Location: Nova Scotia, Canada
Contact:

Re: Assert caveats

Post by slime »

LuaJIT 2.1 can JIT-compile string concatenation operations.
User avatar
pgimeno
Party member
Posts: 3688
Joined: Sun Oct 18, 2015 2:58 pm

Re: Assert caveats

Post by pgimeno »

Nice. It would be interesting to run the benchmark with it.
User avatar
ivan
Party member
Posts: 1919
Joined: Fri Mar 07, 2008 1:39 pm
Contact:

Re: Assert caveats

Post by ivan »

There is a subtlety with the line:

Code: Select all

local args = {...}
This creates an intermediate table in memory.
So when you run the code it's quite fast but
LATER when then the garbage collector kicks in
you may get a slight delay.
In short, the benchmark should take memory usage into account as well.

Also, the following is an unfair test:

Code: Select all

xassert(true, function() return "blah" .. i end)
unless the function is moved outside of any loops:

Code: Select all

local _func = function(i) return "blah" .. i end
for i = 1, ntests do xassert(true, _func, i) end
PS. Table concat is good when you have a lot of arguments.
Something with recursion may work better for small number of args:

Code: Select all

local function _concat(a, ...)
  if a == nil then return '' end
  return a .. _concat(...)
end

function rassert(condition, ...)
  if condition then return true end
  error(_concat(...))
end
or string format could work pretty well:

Code: Select all

function fassert(condition, f, ...)
  if condition then return true end
  error(string.format(f, ...))
end
User avatar
airstruck
Party member
Posts: 650
Joined: Thu Jun 04, 2015 7:11 pm
Location: Not being time thief.

Re: Assert caveats

Post by airstruck »

Interesting article. You could get rid of the intermediary table, table.remove and unpack by using a named parameter for the callback; I guess this would probably improve performance a bit.

Code: Select all

function xassert(a, f, ...)
  if a then return a, f, ... end
  if type(f) == "function" then
    error(f(...), 2)
  else
    error(f or "assertion failed!", 2)
  end
end
User avatar
pgimeno
Party member
Posts: 3688
Joined: Sun Oct 18, 2015 2:58 pm

Re: Assert caveats

Post by pgimeno »

ivan wrote:There is a subtlety with the line:

Code: Select all

local args = {...}
This creates an intermediate table in memory.
That only happens when the assertion fails, which typically makes the program stop and therefore at that point it doesn't matter.

That's the reason for the note at the very end. In some use cases, an assertion failure might not make the program stop, and as the note says, speed might be affected in such cases, for the reason you mention.

ivan wrote:Also, the following is an unfair test:

Code: Select all

xassert(true, function() return "blah" .. i end)
unless the function is moved outside of any loops:

Code: Select all

local _func = function(i) return "blah" .. i end
for i = 1, ntests do xassert(true, _func, i) end
My first idea to solve the problem was to make inline functions that would construct the string, which would not be called unless the assertion failed. In your version, what is being benchmarked is basically the same as in the string.concat version, namely parameter passing.

ivan wrote:PS. Table concat is good when you have a lot of arguments.
Something with recursion may work better for small number of args:

Code: Select all

local function _concat(a, ...)
  if a == nil then return '' end
  return a .. _concat(...)
end
It's only for the failed assertion case, so speed and memory usage don't really need to be considered. That's why I went with a short and readable version.
Last edited by pgimeno on Wed Feb 10, 2016 6:53 am, edited 1 time in total.
User avatar
pgimeno
Party member
Posts: 3688
Joined: Sun Oct 18, 2015 2:58 pm

Re: Assert caveats

Post by pgimeno »

airstruck wrote:Interesting article. You could get rid of the intermediary table, table.remove and unpack by using a named parameter for the callback; I guess this would probably improve performance a bit.
Thanks. There's a subtlety with that approach: with one arg, it returns two values instead of one, making it incompatible. With assert, you can do e.g. io.write(assert("x")) but if you try to do io.write(xassert("x")) using your definition, it will fail with "bad argument #2 to 'write' (string expected, got nil)".

Edit: it would be nice to be able to do something like: select("2-999", ...) or simply select("2-", ...)
User avatar
airstruck
Party member
Posts: 650
Joined: Thu Jun 04, 2015 7:11 pm
Location: Not being time thief.

Re: Assert caveats

Post by airstruck »

Ahh, I see what you mean. I guess you could do this instead:

Code: Select all

function xassert(a, ...)
  if a then return a, ... end
  local f = ...
  if type(f) == "function" then
    error(f(select(2, ...)), 2)
  else
    error(f or "assertion failed!", 2)
  end
end
Here's another idea if you just want built-in concatenation with no separator and no callback:

Code: Select all

function xassert (a, ...)
    if a then return a, ... end
    local argc = select('#', ...)
    if argc > 0 then
        error(('%s'):rep(argc):format(...), 2)
    else
        error('assertion failed!', 2)
    end
end

Edit: it would be nice to be able to do something like: select("2-999", ...) or simply select("2-", ...)
Select already does what you want in this case; it selects the arg at that index and everything after it. This is a pain when you only want to select a fixed range of arguments, but you can use something like this:

Code: Select all

function pluck (n, ...)
    if n < 1 then return end
    n = math.min(n, select('#', ...))
    return (...), pluck(n - 1, select(2, ...))
end

pluck(5, select(3, ...)) -- selects five args, starting at arg 3
Not really relevant here, but maybe useful.
Last edited by airstruck on Wed Feb 10, 2016 8:24 am, edited 1 time in total.
User avatar
ivan
Party member
Posts: 1919
Joined: Fri Mar 07, 2008 1:39 pm
Contact:

Re: Assert caveats

Post by ivan »

pgimeno wrote:That only happens when the assertion fails, which typically makes the program stop and therefore at that point it doesn't matter.
I see. In that event everything after:

Code: Select all

function xassert(a, ...)
  if a then return a, ... end
doesn't affect runtime performance.
My first idea to solve the problem was to make inline functions that would construct the string, which would not be called unless the assertion failed. In your version, what is being benchmarked is basically the same as in the string.concat version, namely parameter passing.
Yep, the point is when a function is defined within a loop it has its own scope (and unique reference) so it's not a fair benchmark IMO.
User avatar
pgimeno
Party member
Posts: 3688
Joined: Sun Oct 18, 2015 2:58 pm

Re: Assert caveats

Post by pgimeno »

Thanks again for your feedback, guys, I appreciate it.
airstruck wrote:
Edit: it would be nice to be able to do something like: select("2-999", ...) or simply select("2-", ...)
Select already does what you want in this case; it selects the arg at that index and everything after it.
Oh! That would have indeed simplified the function. Thanks for the information.
Post Reply

Who is online

Users browsing this forum: Semrush [Bot] and 5 guests