Definitive isNaN()
I wrote an article in November 2009 titled Faster isNaN() and a followup to it titled Even Faster isNaN() and continue to get comments on both, so today I’m doing a followup to bring together both articles and the many comments on them into one definitive article. (UPDATE: added Windows performance results)
In the first article, I derived a method of testing a Number against NaN that made use of the fact that no comparison operator (e.g. >, <) will evaluate to true when one of the operands is NaN. Here is that version:
!(val <= 0) && !(val > 0)
For the rest of the article, I'll refer to the above algorithm as the old algorithm. The new algorithm is much more simple:
val != valThis version was pointed out in a comment by skyboy and I have since seen this elsewhere. I'll refer to this version as the new algorithm. The reason it works is that, as with the old algorithm, it exploits the fact that no comparison operator will evaluate to true when one of the operands is NaN. Since both operands to the != operator are the same, there are only two cases: both NaN and neither NaN. So when val is NaN, this expression is true and we have therefore created another isNaN test.
The reverse of this is pretty simple, as pointed out in the comments on the last article:
val == val
This works for exactly the same reason that the isNaN works. Since thee only two cases are both NaN and both not NaN and NaN == NaN is false because, again, any operator will be false when one of its operands is NaN.
Next is another variant, also pointed out by skyboy, that will test for NaN or Infinity. The built-in isNaN does not check for Infinity, so this is an extra feature.
(n * 0) != 0
You can also take the reverse of this:
(n * 0) == 0
For the purposes of making utility functions (that you may inline with a tool like Apparat), you'll probably want to create both a isNaN and isNaNOrInfinity as well as a isNotNaN and isNotNaNOrInfinity so you can avoid the extra ! operator that you'd get by effectively writing !(val != val) or !((n * 0) != 0). So, here are the utility functions you need:
function isNaN(val:Number): Boolean { return val != val; } function isNotNaN(val:Number): Boolean { return val == val; } function isNaNOrInfinity(val:Number): Boolean { return (val * 0) != 0; } function isNotNaNOrInfinity(val:Number): Boolean { return (val * 0) == 0; }
Now let's do a test to take a look at the performance of the new algorithm in comparison with the old algorithm and the built-in isNaN:
package { import flash.display.*; import flash.utils.*; import flash.text.*; public class EvenFasterIsNaN extends Sprite { private var __logger:TextField = new TextField(); private function log(msg:*): void { __logger.appendText(msg + "\n"); } public function EvenFasterIsNaN() { __logger.autoSize = TextFieldAutoSize.LEFT; addChild(__logger); const NUM_ITERATIONS:int = 100000000; var i:int; var beforeTime:int; var local33:Number = 33; var localNaN:Number = NaN; log("Math.isNaN"); beforeTime = getTimer(); for (i = 0; i < NUM_ITERATIONS; ++i) { isNaN(local33); } log("\t33: " + (getTimer()-beforeTime)); beforeTime = getTimer(); for (i = 0; i < NUM_ITERATIONS; ++i) { isNaN(localNaN); } log("\tNaN: " + (getTimer()-beforeTime)); log("Old Algorithm:"); beforeTime = getTimer(); for (i = 0; i < NUM_ITERATIONS; ++i) { myOldIsNaN(local33); } log("\tfunction call 33: " + (getTimer()-beforeTime)); beforeTime = getTimer(); for (i = 0; i < NUM_ITERATIONS; ++i) { myOldIsNaN(localNaN); } log("\tfunction call NaN: " + (getTimer()-beforeTime)); beforeTime = getTimer(); for (i = 0; i < NUM_ITERATIONS; ++i) { !(local33 <= 0) && !(local33 > 0) } log("\tinline 33: " + (getTimer()-beforeTime)); beforeTime = getTimer(); for (i = 0; i < NUM_ITERATIONS; ++i) { !(localNaN <= 0) && !(localNaN > 0) } log("\tinline NaN: " + (getTimer()-beforeTime)); log("New Algorithm:"); beforeTime = getTimer(); for (i = 0; i < NUM_ITERATIONS; ++i) { myNewIsNaN(local33); } log("\tfunction call 33: " + (getTimer()-beforeTime)); beforeTime = getTimer(); for (i = 0; i < NUM_ITERATIONS; ++i) { myNewIsNaN(localNaN); } log("\tfunction call NaN: " + (getTimer()-beforeTime)); beforeTime = getTimer(); for (i = 0; i < NUM_ITERATIONS; ++i) { local33 != local33; } log("\tinline 33: " + (getTimer()-beforeTime)); beforeTime = getTimer(); for (i = 0; i < NUM_ITERATIONS; ++i) { localNaN != localNaN; } log("\tinline NaN: " + (getTimer()-beforeTime)); } private function myOldIsNaN(val:Number): Boolean { return !(val <= 0) && !(val > 0); } private function myNewIsNaN(val:Number): Boolean { return val != val; } } }
The results I get on a 2.4 Ghz Intel Core i5 running Flash Player 10.1.102.64 on Mac OS X 10.6 are:
| Algorithm | Function Call | Inline | ||
|---|---|---|---|---|
| isNaN | 1993 | 1994 | n/a | n/a |
| Old Algorithm | 950 | 944 | 218 | 210 | New Algorithm | 735 | 736 | 218 | 218 |
And here are the results I get on a 2.8 Ghz Intel Xeon W3530 running Flash Player 10.1.102.64 on Windows 7:
| Algorithm | Function Call | Inline | ||
|---|---|---|---|---|
| isNaN | 1519 | 1516 | n/a | n/a |
| Old Algorithm | 893 | 889 | 203 | 206 | New Algorithm | 787 | 781 | 207 | 209 |
As you can see, the new algorithm is indeed quicker in its function call form. However, these speed gains are eliminated when the test is inlined. Still, the simplicity of the new algorithm is a nice improvement to have. If you're keeping your isNaN replacement in a function though, you should really go with the new algorithm as it is nearly 3x faster than the built-in isNaN. This is, as the title of this article states, even faster than the 2x speedup we got with the old algorithm.
One final note regarding a comment on the first article: the new algorithm does work with untyped variables:
var a:* = 33; var b:* = NaN; log(a != a); // false log(b != b); // true
#1 by Hannes on December 20th, 2010
doesn’t this break when there are getters with side effects involved?
#2 by jackson on December 20th, 2010
It’s not the
isNaNthat’s broken, it’s any kind of macro expansion in that case. For example, a simple function to double the input value would be broken:#3 by Henke37 on December 20th, 2010
Yes, yes it does, if you use the inline variant.
But it was broken to begin with. Getters should not have side effects.
#4 by skyboy on December 20th, 2010
Per the source code of the AVM,
(n * 0) == 0is actually a replacement for isFinite rather than isNaN:http://hg.mozilla.org/tamarin-central/file/fbecf6c8a86f/core/Toplevel.cpp#l858
#5 by erik on April 8th, 2011
I think the code for two functions under “So, here are the utility functions you need:” need “n” changed to “val”?
#6 by jackson on April 8th, 2011
Good catch! I’ve updated the article.
#7 by skyboy on October 8th, 2011
I optimized the old variant with bitwise operators and int conversions to get this; doubled up on the results, same algo behind them:
1ms per 100,000,000 over != and isNaN
~27% improvement over && on older machines
#8 by skyboy on October 8th, 2011
(I hate that anti-HTML filter.)
#9 by Daniel Sigurdsson on December 13th, 2011
This also works for the inverse of isNaN
In cases where it is very slow to get the value of val (e.g. in cases of getter functions) this should be an even faster approach.
if (val <= Infinity) { //do something }if ( !(val <= Infinity) ) { //do something }#10 by Frank on December 3rd, 2013
These results are still valid for AIR 3.9. Just stumbled across this very easy performance optimisation and gained a whopping 10% throughout our application. Pretty significant improvement for such a small thing. Thanks you very much for this!