DEV Community

Cover image for Code Smell 180 - BitWise Optimizations
Maxi Contieri
Maxi Contieri

Posted on • Originally published at maximilianocontieri.com

Code Smell 180 - BitWise Optimizations

Bitwise operators are faster. Avoid these micro-optimizations

TL;DR: Don't use bitwise operators unless your business model is bitwise logic.

Problems

  • Readability

  • Clevereness

  • Premature Optimization

  • Maintainability

  • Bijection Violation

Solutions

  1. Improve readability

Context

Some clever programmers solve problems we don't have.

We should optimize code based on evidence and use the scientific method.

We should benchmark only if necessary and improve code only if really necessary and bear the cost of changeability and maintainability.

Sample Code

Wrong

const nowInSeconds = ~~(Date.now() / 1000)
Enter fullscreen mode Exit fullscreen mode

Right

const nowInSeconds = Math.floor(Date.now() / 1000)
Enter fullscreen mode Exit fullscreen mode

Detection

[X] Semi-Automatic

We can tell our linters to warn us and manually check if it is worth the change.

Exceptions

  • Real-time and mission-critical software.

Tags

  • Premature Optimization

Conclusion

If we find this code in a pull request or code review, we need to understand the reasons. If they are not justified, we should do a rollback and change it to a normal logic.

Relations

More Info

Tilde Operator ~~

Javascript BitWise Operators

Disclaimer

Code Smells are just my opinion.

Credits

Photo by Frédéric Barriol on Unsplash

Original Article Here.


Watch the little things; a small leak will sink a great ship.

Benjamin Franklin


This article is part of the CodeSmell Series.

Top comments (29)

Collapse
 
vipert profile image
ViperT • Edited

You can use the JS bitwise operator "OR" once --> var x = y | 0 this is much more efficient for computation, it do exactly as the Math.floor() function except you don't have to go trough the "Math" object and so on... (it is used in asm.js)

Collapse
 
mcsee profile image
Maxi Contieri

why?

Please tell me your reasons to use the operator instead of the more declarative Math.foor ?

Do you have strong evidence this is really necessary ?

Collapse
 
vipert profile image
ViperT • Edited

Yes because formerly it doesn't have to access the Math object to perform the operation and it is used like this not humanly though but in evidence in asm.js... both are correct, I mean one is best for readability and one is just a "trick" to force the variable being coerced into an integer, mostly interesting Low-Level-JavaScript (LLJS) explain that it is best suited like this for code that should be optimized in terms of entire number.

Thread Thread
 
mcsee profile image
Maxi Contieri

Relevant question is the same: Why it should be optimized?

Thread Thread
 
vipert profile image
ViperT

Because computation on CPU can be slow...

I had a pixel art project, which enable one to draw on images and it does all the color blending computation in JS, which written in ASM.JS style is as much fast as it can be in webassembly, as I don't understand how I could make it work faster in WebGL...

Thread Thread
 
mcsee profile image
Maxi Contieri

ok. there are a few cases were you need to optimize the code.
There the 'Exceptions' in the article.
Most code does not need these optimizations

Thread Thread
 
vipert profile image
ViperT

Yes

Collapse
 
adam_cyclones profile image
Adam Crockett 🌀

Hey but it’s clever and clever is not good in teams or in the world of corporate programming. Example; I’m 30ish and I’m now cognitively challenged due to my advanced age I need simple code if I’m in your team I don’t want the cost of going wtf StackOverflow, not seen this in my gosh dang whole career 😬😡

See it’s not worth it because time is money

Collapse
 
mcsee profile image
Maxi Contieri

I am over 50.
And I am tired of young people programming like in the 1960s.

Thread Thread
 
adam_cyclones profile image
Adam Crockett 🌀

Is this why you have this wonderful series? I have been keenly following.

If I’m not mistaken you can explain the GOTO statement 😁

Thread Thread
 
mcsee profile image
Maxi Contieri
Collapse
 
cicirello profile image
Vincent A. Cicirello

Since JavaScript Numbers are double-precision floating-point values, it shouldn't even support bitwise operators. It leads to mathematical nonsense such as y | 0 not necessarily equalling y, and likewise ~~y not necessarily equalling y.

Collapse
 
mcsee profile image
Maxi Contieri
Collapse
 
vipert profile image
ViperT • Edited

Numbers aren't double-precision floating-point values when one explicitely and systematically tells the compiler | 0 hey here is a integer (x | 0)>>>0 hey here is the same but positive, instead if our number is bigger than the maximum value of a 32 bits Unsigned integer, using >>>0 force the compiler to use, excuse me, "no doubles" for it. (in certain case ^^)

Collapse
 
cicirello profile image
Vincent A. Cicirello • Edited

JavaScript's Number type is a IEEE 754 double-precision floating-point. developer.mozilla.org/en-US/docs/W...

JavaScript's bitwise operators on floating point Numbers coerces it to a 32bit integer (with a call to ToInt32) and then applies the bitwise op to that integer. In this way, bitwise ops on a floating-point type is pure nonsense. The op isn't being used for the actual logical use of the bitwise op. It is being used to gain access to an internal side-effect, and an internal type not otherwise exposed to the JS programmer.

Thread Thread
 
vipert profile image
ViperT • Edited

That's not what the asmjs.org/ project did (from Mozilla), it wasn't non-sense at all, and all of this enabled porting C/C++ algorithms into JavaScript through Emscripten. Look, no, it is not nonsense, it is a missleading conception to think JS code isn't optimized within the JavaScript Engine. You hadn't being forced to use ASM.JS code alike for performance my friend, you should having been seen the results.

Thread Thread
 
cicirello profile image
Vincent A. Cicirello

Not sure what you mean by "not what asmjs.org did". Numbers in JS are according to the JS spec, floating-point. Period. There is a BigInt type but that isn't involved in any way when applying bitwise ops to Numbers. If you are referring to what happens when one compiles C to JS, then whatever JS is produced isn't intended for human readability. The JS is essentially acting as a sort of machine code in that instance, and shouldn't be used as a baseline for writing JS that is intended for human readability.

Thread Thread
 
peerreynders profile image
peerreynders

Numbers in JS are according to the JS spec, floating-point.

The spec also defines the abstract operations

returning an integral number, i.e. it ceases to be an IEEE 754 floating point value but is just a signed or unsigned 32 bit string that is handled by the CPU's typical bitwise operations.

For example NumberBitwiseOp(op,x,y) converts x and y to Int32 (i.e. doesn't operate on them as IEEE 754 floating-point values).

And the reality is that JavaScript runtimes are allowed to optimize values. For example V8 can handle an array of integers as PACKED_SMI_ELEMENTS (small integers, Int32; Element Kinds).

Thread Thread
 
cicirello profile image
Vincent A. Cicirello

The main point is still the same: using a bitwise op as a clever trick to call the abstract ToInt32, when you don't really actually need the bitwise operation itself, only obfuscates your logic, negatively impacting readability and maintainability.

Thread Thread
 
mcsee profile image
Maxi Contieri

exactly

Thread Thread
 
peerreynders profile image
peerreynders

using a bitwise op as a clever trick to call the abstract ToInt32, when you don't really actually need the bitwise operation itself

That is not a good reason to reject bitwise operations wholesale and much less a justification to stay ignorant about their semantics.

“clever, adjective

1b: mentally quick and resourceful but lacking in depth and soundness

from: Webster's Ninth New Collegiate Dictionary (1983; ISBN 0919028667)

And

const nowInSeconds = ~~(Date.now() / 1000);
Enter fullscreen mode Exit fullscreen mode

reflects that unsound thinking.

Clearly an Int32 with a range of -2147483648…2147483647 cannot cover the domain of values -8.64e12…8.64e12 that (Date.now() / 1000) is capable of producing.

const now = Date.now();
console.log(typeof now); // "number"

const MAX_MS = 8_640_000_000_000_000; // https://tc39.es/ecma262/#sec-time-values-and-time-range
const maxDate = new Date(MAX_MS);
console.log(maxDate.toISOString()); // "+275760-09-13T00:00:00.000Z"

const max_secs = MAX_MS / 1000;
const maxInt32 = 0x7fff_ffff;
console.log(max_secs > maxInt32); // true

const overflowInt32 = maxInt32 + 1;

console.log(new Date(maxInt32).toISOString());        // "1970-01-25T20:31:23.647Z"
console.log(new Date(overflowInt32).toISOString());   // "1970-01-25T20:31:23.648Z"
console.log(new Date(~~overflowInt32).toISOString()); // "1969-12-07T03:28:36.352Z"

console.log(overflowInt32);   // 2147483648
console.log(~~overflowInt32); // -2147483648

const toInt32 = (n) => n | 0;
const toUInt32 = (n) => n >>> 0;

console.log(overflowInt32.toString(16));           // "80000000"
console.log(toUInt32(overflowInt32).toString(16)); // "80000000"
console.log(toInt32(overflowInt32).toString(16));  // "-80000000"

console.log((~overflowInt32).toString(16));  // "7fffffff"
console.log((~~overflowInt32).toString(16)); // "-80000000"

console.log((~0).toString(16));                 // "-1"
console.log(toInt32(0xffff_ffff).toString(16)); // "-1"
Enter fullscreen mode Exit fullscreen mode
Thread Thread
 
mcsee profile image
Maxi Contieri

Your explanation is fine if you need bitwise logic.
The sample from the code smell has nothing to do with bitwise logic.

Thread Thread
 
peerreynders profile image
peerreynders • Edited

I was primarily responding to:

bitwise ops on a floating-point type is pure nonsense.

which suggests that bitwise operators should be summarily banned from usage.

Ultimately bitwise operators do not operate on floating point numbers; the 32-bit bit strings they do operate on simply use the IEEE 754 binary64 as a conveyance.

There is an issue in TypeScript to add a BitwiseInt32 type to clearly differentiate that usage from number.


To me the sample code is indicative of a much bigger problem which is only tangentially related to bitwise operators; using code without fully understanding its limitations.

Aside from the readability issues, the statement that ~~(value) and Math.trunc(value) are equivalent is nonsense:

Now some people may argue that most JavaScript code will not need to use bitwise operators, so it should be treated as an advanced topic (that may be more useful in server side code).

However lots of interview preparation materials (e.g. Elements of Programming Interview in python) have plenty of exercises related to manipulating bit strings.

The article seems to unfairly single out bitwise operations when the actual issues are:

  • Don't use code you don't fully understand (because once understood it may become clear that it isn't appropriate)
  • Don't micro-optimize without critically examining the trade-offs of actual measured performance gain (rather than some unverified, assumed performance gain) and reduction in readability.
Thread Thread
 
cicirello profile image
Vincent A. Cicirello

Using 2 bitwise complements as a means of calling ToInt32 a function that isn't otherwise directly exposed is sneaky. "Clever" lacks the negative connotation of "sneaky". So you are correct that it isn't clever. It is sneaky. Clever is just a nicer way of saying it.

Thread Thread
 
cicirello profile image
Vincent A. Cicirello • Edited

@peerreynders the BitwiseInt32 proposal for TypeScript looks like a good way to add the missing semantics inherent in applying bitwise ops, not because you need bitwise logic, but because you are trying to utilize the side-effect of the op for type coercion.

Understanding bit manipulation, such as the article that you linked to on interview prep, can be very important. You'll notice that article is Python focused. I work with genetic algorithms a lot. Bit manipulation is extremely important. I mostly use Java for my GA work but occasionally Python, long ago C. Genetic algs is a case where bitwise ops are used because you need bitwise logic. In a GA, you aren't using a bitwise op to accomplish something else, such as the various ways they are used in JS for type coercion as a micro-optimization rather than just using a call to the more semantic trunc.

Thread Thread
 
peerreynders profile image
peerreynders

I think a title of Code Smell 180 - Micro Optimizations would have been more appropriate.

The whole idea of micro-optimizations is that they only really apply to code that has been proven to be on the hot path. From that perspective the sample code qualifies because under most circumstances using Math.trunc() wouldn't be pessimizing prematurely.

Working with bit strings in 32-bit chunks isn't ideal but sometimes you gotta do what you gotta do.

Thread Thread
 
mcsee profile image
Maxi Contieri

indeed. I booked the title for another smell :)

Collapse
 
jonrandy profile image
Jon Randy 🎖️

Math.floor is not the same as ~~ - Math.trunc is the equivalent

Collapse
 
selimaelliott profile image
SelimaElliott

How to Find the Stinky Parts of Your Code ? shiva vashikaran mantra for love