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 != val
This 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
isNaN
that’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) == 0
is 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.
#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!