simplify ternary transpilation#310
Conversation
|
It would probably be good to remove the lib function altogether. |
|
ok, I think I got it all now. |
|
I did some benchmarks and I'm getting mixed results: In Lua 5.3 IIFEs are always faster than box/unboxing around 400%. In LuaJIT IIFEs are faster around 20-50% for small arrays. Tested Code: -- box/unbox
for i=1, arrSize do
y = ((arr[i]) and {true} or {false})[1]
end-- IIFE
for i=1, arrSize do
z = (function (condtion, val1, val2)
if condtion then
return val1
else
return val2
end
end)(arr[i], true, false)
endI would argue that 99.9% of the time arrays have less than 50 Million entries, in which case IIFEs are defintly faster. Also I don't think you usually find ternaries that often in tight loops, but that's pure speculation. So we should still remove the lib call but replace it with IIFEs instead of boxing/unboxing. |
|
Wow thats really interesting. I had no idea IIFEs are that well optimised in lua. We should definitely use IIFEs then. |
|
Did some more testing seems like: performs exactly the same as: in both JIT and 5.3, so we might aswell keep the lib function since its a bit more readable and adjust it to take the 2 raw expressions as parameter without wrapping them in extra functions. |
|
However, the wrapping is necessary to perform lazy evaluation which otherwise might introduce unwanted side effects. Can you also benchmark the performance with function wrapping? |
|
Okay getting somewhat different results now: --lua maxntest.lua
arrSize: 5000000
boxUnboxTime time: 2.580266
IIFE time: 0.634437
lib time: 0.630949
wrap time: 0.640842--luajit maxntest.lua
arrSize: 5000000
boxUnboxTime time: 0.054687
IIFE time: 0.424994
lib time: 0.049987
wrap time: 0.770319That might be the case because I do all these tests in one file, should probably be executed all separately to make sure they all have the same conditions (stack, heap...). Here is the code I used: https://gist.github.com/lolleko/8cd4be25b91a8c13c6472863aa6b3868 Feel free to perform your own benchmarking. I think if we introduce a performance optimising change like this we should make sure it actually performs better. |
|
Completely agree. Here I just assumed that function calls must be slower than table allocation and lookup. I will try some own benchmarking later. |
|
Thanks for reminding me to always benchmark before optimising. So as boxing/unboxing is expensive we should definitely replace it with an IIFE. In the implementation below only one call is necessary as opposed to two when using the library function. So it seems performance using IIFE is better on Code: https://gist.github.com/TheLartians/0b8bbdc551b67b7205e347df7525c205 |
|
In my benchmark when using liajit, boxing outperforms the other approaches by an order of magnitude, so for luajit we should use the boxing approach. |
|
IMO we should have no boxing/unboxing on things guaranteed not to produce side effects, which is the most common case when referring to literals or variables |
|
@DoctorGester the boxing/closures are necessary even without side effects when dealing with boolean or possibly undefined values, when using the |
|
I understand the problem, however most cases could be handled with the usual lua ternary, I don't think settling for a general solution which is in many cases worse than a specific one is a good idea. Since it's typescript we should assume the type is known and understand what's a boolean/nullable type and what isn't. And yeah a temporary variable would be preferable in other cases |
|
One thing to keep in mind is that without strict null checks enabled, any type could be nullable. |
|
You can always go for the most defensive version when type information is unreliable. |
|
I've added a |
| const falsibleFlags = ts.TypeFlags.Boolean | ||
| | ts.TypeFlags.BooleanLiteral | ||
| | ts.TypeFlags.Undefined | ||
| | ts.TypeFlags.Any; |
There was a problem hiding this comment.
Will this catch null-able types?
declare function foo(): string | null;
const x = condition ? foo() : "bar";There was a problem hiding this comment.
Currently only literal types are allowed to be non-falsible as then we know the exact value of the expression. As we already excluded undefined and boolean literals before we now know that whenTrue must always be a valid non falsible expression.
There was a problem hiding this comment.
Update: now the above only applies when strictNullChecks are not enabled. Otherwise I recursively iterate through all union types and check if they are all non-falsible.
There was a problem hiding this comment.
Re-reading this I realize why I got confused. Will a null literal get caught by those flags?
There was a problem hiding this comment.
Good point, I wasn't aware that there is a difference between null and undefined. I added an additional check for it.
src/Transpiler.ts
Outdated
| return this.transpileLuaLibFunction(LuaLibFeature.Ternary, condition, | ||
| `function() return ${val1} end`, `function() return ${val2} end`); | ||
| public transpileConditionalExpression(node: ts.ConditionalExpression): string { | ||
| if (tsHelper.isNonFalsible(this.checker.getTypeAtLocation(node.whenTrue))) { |
There was a problem hiding this comment.
What about if strictNullChecks is off? Shouldn't the protected version always be used in this case?
There was a problem hiding this comment.
Currently we are playing it safe and always protecting non-literal expressions. With strictNullChecks enabled we could also allow non-literal types to be transpiled without protection.
It should be trivial to extend isNonFalsible to support strictNullChecks in the future.
There was a problem hiding this comment.
Update: the future is now.
src/TSHelper.ts
Outdated
| return defaultArrayPropertyNames.has(methodName); | ||
| } | ||
|
|
||
| public static isNonFalsible(type: ts.Type, strictNullChecks: boolean): boolean { |
There was a problem hiding this comment.
IsNonFalsible is a pretty confusing name - perhaps it should be reversed to be IsFalsible and checked with a not "!" operator later.
src/Transpiler.ts
Outdated
| return this.transpileLuaLibFunction(LuaLibFeature.Ternary, condition, | ||
| `function() return ${val1} end`, `function() return ${val2} end`); | ||
| public transpileConditionalExpression(node: ts.ConditionalExpression): string { | ||
| if (tsHelper.isFalsible(this.checker.getTypeAtLocation(node.whenTrue), this.options.strictNullChecks)) { |
There was a problem hiding this comment.
One more thing (sorry): "strictNullChecks" is implicitly enabled if "strict" is set in options, so you'll want to check for that too.
tomblind
left a comment
There was a problem hiding this comment.
Looks great - I think this is a really nice solution to handling ternary operations 😄
Transpile the ternary operator using and/or and boxing/unboxing. This reduces the cost of the ternary operator from a library call and the creation of two closures to a table allocation and lookup.
Example
Before
After
LuaJIT:
Other: