Be careful when using assert() and Why

The really good thing about Luau is that they have been going hard in the paint to make sure that the most standard practices are efficient/fast.

But yeah, the fact that assert must evaluate the arguments causes the slowness. In Google’s Flogger logging library, they solve this same issue in Java by passing a closure (basically anonymous function) to the log in order to avoid evaluation unless absolutely necessary. I wonder if this would work in Lua, or if the creation of the function slows it down?

local function AssertMaybeFast(condition, func)
   if not condition then
      error(func(), 2)
   end
   return condition
end

...

AssertMaybeFast(x > 0, function() return ("x should be > 0 but it is %f"):format(x) end)

Again, I would need to benchmark that to see if there’s actually any improvement at all, or if the allocation for a new function slows it down at all.

Update: Preliminary tests show that my AssertMaybeFast is ~6x faster than the normal assert when needing to do a string format, but normal assert with just a straight literal is around 3x faster than my AssertMaybeFast version.

17 Likes

I didn’t knew the usage of the Assert() function I think it’s a very nice function but as @EYYCHEEV Said the performance gets bad and idk what happens if the performance is low and I don’t wanna know

3 Likes
function assert(c,m)
  if not c then error(m) end
end
2 Likes

that’s pretty much what an assert function already looks like. function call is the reason it’s taking that long

3 Likes

I haven’t gotten around to it yet, but this post is strangely worded and often taken out of context on top of not being the most informative. The thread there by no means is attempting to discourage the use of FindFirstChild however noting some bad habits that I’ve observed being very common.

As I delve more into programming, I’m coming more to terms with understanding why developers prefer not to use dot syntax even in cases where it’s more appropriate to do so and I intend to rewrite or completely dispel any misinformation I may be providing via that. See the forewarning for information.

5 Likes

I think the slowdown here is not just the extra evaluation but also string concatenation in Lua(considering it is slow). I think you can speed it up a bit if you short-circuit the assertion which you can do by or. or will only evaluate until it’s true, unlike and.

Though, I haven’t benchmarked this yet, this might be a better alternative to assert:

function fasterAssert(s)
   return s == true or error(s)
end

fasterAssert(true == true or "true is not " .. "true")
6 Likes

It does. Think about how “or” statements are evaluated. They won’t do extra evaluation if you already know the outcome of the entire statement. The “shortcut” does speed things up.

Here’s a good example:

local function evaluate(s, true)
      print(s)
      return true
end

if evaluate(1, true) or evaluate(2, true) then -- prints 1 only and does not evaluate 2.
     
end

if evaluate(1, false) or evaluate(2, true) or evaluate(3, true) then --prints 1 and 2 only and does not evaluate 3.
     
end

The speed up really depends on the entire cost of that evaluation.

2 Likes

If you were doing something costly in each evaluation, yes you would definitely notice a difference.

I don’t get what you mean “faster than if statements”. I am literally talking about if statements and how they are short circuited for optimizations.

You are thinking of Lua’s idiomatic way of implementing ternary operators. I’m not talking specifically about that.

It’s not false information. It’s how languages work.

2 Likes

This whole thread is about how assert must evaluate the arguments. The arguments can be costly(i.e string concatenation, string formatting). If you were to avoid that evaluation, you would see a speed up.

2 Likes

You’re still lost… i’m literally talking about if statements and how they are short circuited. I’m not talking about ternaries.

2 Likes

That’s why I stated that.

However, my statements aren’t flawed and my example from before clearly demonstrates what I mean. Let’s say the cost of each evaluation was 500. You have 2 evaluations but since it’s an or statement, there is a chance you only have to do one evaluation. Hence, a chance of half a speed up(theoretically). Since computers work insanely fast, you won’t notice unless you are doing something very costly. However, that doesn’t mean what I stated is false information.

I don’t get why your talking about bytecode. The bytecode would still be the same. It’s a runtime optimization.

2 Likes

Good to know. I read this when I was planning to use it … Interesting and informative!

2 Likes

This is really good to know I frequently use assert() and was unaware of this.

2 Likes

It’s completely fine to use assert, however string formatting the error message may be slower since formatting a string includes checking the string types and goes through another work.

3 Likes

Why does roblox dev forum say someone mentioned me here? there’s no mention here

2 Likes

Sorry for the mention, I tagged you for the image credit.

2 Likes

Sorry to bump, but for this example, is it necessary to do == true?
Or can I simply write return v or error(errorMessage)?

Thanks in advance.

1 Like

I have also benchmarked this and these are my results

local function Assert(value)
	assert(type(value) == "number", "Bad Argument" .. value)
end

local function If(value)
	if type(value) ~= "number" then error("Bad Argument" .. value) end
end

local amount = 1000

local assertTotal = 0
for i = 1, amount do
	local t = os.clock()
	local a, b = 1, true
	for i = 1, 1000 do
		pcall(Assert, a)
		a, b = b, a
	end
	assertTotal += os.clock() - t
end
assertTotal /= amount

task.wait()

local ifTotal = 0
for i = 1, amount do
	local t = os.clock()
	local a, b = 1, true
	for i = 1, 1000 do
		pcall(If, a)
		a, b = b, a
	end
	ifTotal += os.clock() - t
end
ifTotal /= amount

print("Assert:", string.format("%.9f", assertTotal))
print("If    ", string.format("%.9f", ifTotal))

Results:

-- Assert:
0.004660541
0.004596083
0.005090250
0.004749107
0.004602356
0.004767745
0.004852163

-- If
0.004702491
0.004715973
0.004451382
0.004726072
0.004471088
0.004481933
0.004517734

so I would say if is around 0.0002 seconds faster

but if I change the a and b values to always be numbers and change the error message to
"Bad Argument" .. value .. string.format("%.3f", value)

then I get results

-- Assert:
0.000970206
0.001223040

-- If
0.000116331
0.000109242

now we can see that if is around 0.0009 seconds faster

so we should not blame assert for this but blame the string functions because this performance impact will effect any function if your passing string that has had any work done to it not only assert

so the best way to use assert is to pass a simple string into it like this

assert(type(value) == "number", "Bad Argument")

and now the performance should be very close to if

6 Likes

assert checks if the first parameter is true (or a non-nil value that isn’t false) and errors if it isn’t. In this case, we want to be sure the type is number.

2 Likes

Let’s take the example of:

fasterAssert(num == 123 or "Wrong number!")

When num is equal to 123, it results in true. This causes the or operator to short circuit and skip "Wrong number!" and instead pass true into fasterAssert.

But what if num is not equal to 123? Well in that case or looks at its second argument "Wrong number!" and because it is truthy (close enough to true), it accepts it and passes it into fasterAssert.

So there are two possibilities:

  1. true
  2. string which is truthy.

When something is truthy, it means that the if-statement will see it as true and evaluate its contents, but when comparing with == true, it will result in false as it isn’t exactly true. In both of these cases, the argument is always truthy. So in the fasterAssert function, the or operator will see "Wrong number!" as truthy, and return that instead always. It will in fact never error either.

Understanding JavaScript Truthy and Falsy - Stack Overflow

3 Likes