Be careful when using assert() and Why

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

Just use warn() so the code doesn’t stop. Error stops the code.

2 Likes

CC: @bloodbonniekingnoob1

The whole point of erroring is to stop the code from continuing in a broken state. If a function expected a number but got a boolean, then you’re going to want it to stop and tell you that before it tries to evaluate with it.

Warning is more for informing the user of a possible misuse that isn’t severe enough to cause issues, i.e. a Disable method that warns when the object is already disabled but continues because it won’t do anything.

6 Likes

This is interesting. Although I have been getting nearer to 90% improvements with more calculations.
image

1 Like

Maybe team behind Luau can work on optimizing assert? I hope they do it at some point.

1 Like

The problem doesn’t lie with assert.

The problem is that the string.format is slow which is also calculated even though it’s not needed

Here is a one which shouldn’t lag that much

local function assertf(c, format, ...)
	if c then
		return c
	else
		error(string.format(format, ...), 2)
	end
end

example

assertf(type(test) == "string", "Expected string got %s", type(test))
2 Likes

how about with the new `` string?

assert(1+1 == 2, `the correct answer is {1+1}`)
1 Like

The formatting is still calculated where you call the function, so yes.

4 Likes

you can remake the function tho

function assert(condition, message)
    if not condition then
        error(message or "assertion failed!", 2)
    end
    return condition
end
1 Like

that does nothing, the problem is with computing the formatting of the message

1 Like

Assert itself isn’t really the problem, generating the message is the costly part. If you are doing thousands of assert calls with dynamic messages, you are generating the message every time. So if you use assert in a hot code path, you will get better performance by not generating a message dynamically and instead just passing a static string. You can also use string interpolation and you’ll get slightly better performance from that compared to concatenation.

This all only matters in really hot code paths though. Manual string concatenation has some overhead simply because you’re generating a brand new string, and this adds up over thousands.

That is why using table.concat to join lots of strings together is so fast compared to concatenation, because you are taking a table and building a string from it once vs building a new string every iteration.

4 Likes