-
-
Notifications
You must be signed in to change notification settings - Fork 374
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Bitwise operations are seemingly not minimized or optimized at all #1343
Comments
To my knowledge besides "use asm" binary operations aren't used much in JavaScript in the wild. Is there some compiler generating these in this inefficient manner? If not, it's probably not worth the few characters saved in each of these transforms. |
they're very common in javascript games, image manipulation code, etc. |
@fabiosantoscode They aren't used often, but when they are, they're almost unavoidable. When they're needed, they significantly improve performance, drastically cut down on memory usage, or both. To explain my need for them, expand on @xvln's, and to show a few more examples:
|
I'm implementing this right now. I must say, I appreciate all the examples written out with explanations, since I'm not very good at maths. I noticed that For |
Bug report or Feature request? Feature request
Version (complete output of
terser -V
or specific git commit) 5.16.3Complete CLI command or
minify()
options usedterser
inputterser
output or errorDelimited by line for clarity.
Expected result
Before #641
After #641
Note:
"use asm"
contexts have special semantics around this, and compilers are pretty good at minimizing them, so it's probably not worth applying this in those contexts.Here's an explanation of each:
Expression:
(bar & 1) | 0
| 0
is redundant - LHS is already guaranteed to be a 32-bit integer anda | 0
is equivalent toa
for all 1-bita
in{0, 1}
.Expression:
(bar & 1) & ~0
& ~0
is redundant - LHS is already guaranteed to be a 32-bit integer anda & 1
is equivalent toa
for all 1-bita
in{0, 1}
. (~0
and-1
are equivalent and simply represent masks with all bits set.)Expression:
(bar & 1) ^ 0
^ 0
is redundant - LHS is already guaranteed to be a 32-bit integer anda ^ 0
is equivalent toa
for all 1-bita
in{0, 1}
.Expression:
bar ^ -1
^ -1
and the equivalent (^ ~0
) are equivalent to inversion -a ^ 1
is equivalent to~a
for all 1-bita
in{0, 1}
. (~0
and-1
are equivalent and simply represent masks with all bits set.)Expression:
~(bar ^ 1)
This is equivalent to
~bar ^ ~1
and thusbar ^ 1
as inversion distributes over XOR.Expression:
(bar | 1) | 0
See above about
| 0
Expression:
(bar | 1) & 2
This can be reduced to
bar & 2
, but the logic is a little involved:(bar & 2) | (1 & 2)
.1 & 2
evaluates to0
, and so it ends up equivalent to(bar & 2) | 0
. As per earlier, the| 0
is a no-op here, and so it can be removed.You won't need to expand it to discover the shorter decoding, though.
Expression:
(bar | 1) & 3
The two operations can be reversed without effect to avoid the need for parentheses, with similar logic to above:
(bar & 3) | (1 & 3)
.1 & 3
evaluates to1
, and so it ends up equivalent to(bar & 3) | 1
. Unlike above, the| 1
isn't redundant, and so it must be kept.You won't need to expand it to discover the shorter decoding, though.
Expression:
bar | ~0
ORing with all bits set just returns all bits set, so this is equivalent to just evaluating
bar
and returning~0
. Asbar
in this case is a simple variable access and Terser assumes.valueOf()
to be side effect-free,bar
can safely be omitted altogether in this case.Expression:
bar & 0
Same logic as above, but with ANDing and it leaving all bits clear.
Expression:
(bar & 7) !== 0
a & b
always returns a 32-bit integer, and0
is the only falsy 32-bit integer. So for alla
andb
and binary bitwise operation@
, the expression(a @ b) === 0
is equivalent to the expression!(a @ b)
. Likewise,~a === 0
and anything equivalent to it (like(a | 0) === -1
) is equivalent to!~a
.In this case, it's
!==
, not===
, and so the result needs inverted again, but it's still shorter than===0
or even==0
.Statement:
if ((a & b) !== 0) c()
Same logic as with the above.
Expression:
(bar & 7) !== 7
There's two approaches to shorten this, both of which are viable:
a & 7
only returns integers within the range 0 to 7 inclusive. This means=== 7
and>= 7
are equivalent. Since this is inequality, the much shorter< 7
is equivalent to!== 7
or even!= 7
.(a & mask) === mask
is really testing if all bits ofa
selected bymask
are set. You could simply inverta
and check whether they're all clear instead via(~a & mask) === 0
, and optimize it similarly to the above example.This pattern holds for all
(a & b) !== b
whereb
is a 32-bit integer known to be one less than a power of 2. As for when to prefer which:<
variant should be preferred if all the following constraints are met:if
statement containing anelse
branch whose body is not just anotherif
statementa
requires parentheses to disambiguate when inverting, but not when using in a bitwise AND operation (ex: it's an addition expression)b
consists of at most two characters, including any prefixing-
or~
operatorsif
/else
condition whoseelse
body is just anif
statement,b
consists of only a single character (and thus no prefixing-
or~
operators)!(...)
can be omitted in conditions, and the literal is only specified once (making it smaller in practice).To show why two variants are useful here, here's an example with
a & b
as the expressionbar
:Here's why it should never be used in most conditions:
Obviously stopping far short of proposing a full boolean SAT solver here - it's just not necessary or useful here. Even Clang and LLVM only make a best effort (though they do know many reduction rules, including the above).
The text was updated successfully, but these errors were encountered: