
Show HN: Fast.js – faster reimplementations of native JavaScript functions - phpnode
https://github.com/codemix/fast.js
======
danabramov
Reminds me of this comment by Petka Antonov on native V8 Promises being way
slower than Bluebird[1]:

 _> I'd expect native browser methods to be an order of magnitude faster._

 _Built-ins need to adhere to ridiculous semantic complexity which only gets
worse as more features get added into the language. The spec is ruthless in
that it doesn 't leave any case as "undefined behavior" \- what happens when
you use splice on an array that has an indexed getter that calls
Object.observe on the array while the splice is looping?_

 _If you implemented your own splice, then you probably wouldn 't even think
of supporting holed arrays, observable arrays, arrays with funky
setters/getters and so on. Your splice would not behave well in these cases
but that's ok because you can just document that. Additionally, since you
pretty much never need the return value of splice, you can just not return
anything instead of allocating a wasted array every time (you could also make
this controllable from a parameter if needed)._

 _Already with the above, you could probably reach the perf of "native splice"
without even considering the fact that user code is actually optimized and
compiled into native code. And when you consider that, you are way past any
"native" except when it comes to special snowflakes like charCodeAt, the math
functions and such._

 _Thirdly, built-ins are not magic that avoid doing any work, they are normal
code implemented by humans. This is biggest reason for the perf difference
specifically in the promise case - bluebird is extremely carefully optimized
and tuned to V8 optimizing compiler expectations whereas the V8 promise
implementation[2] is looking like it 's directly translated from the spec
pseudo-code, as in there is no optimization effort at all._

[1]:
[https://github.com/angular/angular.js/issues/6697#issuecomme...](https://github.com/angular/angular.js/issues/6697#issuecomment-42771111)

[2]:
[https://github.com/v8/v8/blob/master/src/promise.js](https://github.com/v8/v8/blob/master/src/promise.js)

~~~
JoshTriplett
> Additionally, since you pretty much never need the return value of splice,
> you can just not return anything instead of allocating a wasted array every
> time

That kind of optimization seems easily done with a builtin as well: have an
option to return the array, and only do so if the JavaScript engine indicates
that the calling code actually uses the return value.

~~~
babby
That seems like something well suited to compile-to-js languages like
Coffeescript. It could substitute cases as you mentioned with fast.js
equivalent functions.

~~~
couchand
CoffeeScript inserts calls to native `indexOf` all over the place. After
seeing this I'm wondering if there's an easy way to shim fast.js in there?

------
Joeri
Today i was refactoring some js code that rendered a graph to svg. It was
taking several seconds in some cases, and the developer had done a bunch of
micro-optimizations, inlining functions, building caches to avoid nested
loops, and so on.

I ran a profile. The code ran for 2.5 seconds, 4 ms of which in the micro-
optimized js code, the rest updating the dom, five times all over again.
Needless to say that i threw out all the micro-optimizations, halving the
number of lines, and fixed it so the dom was updated once.

Anyway, the point i'm making is this: you should micro-optimize for
readability and robustness, not performance, unless profiling shows it's worth
it. I haven't known a case where pure (non-dom, non-xhr) js code needed micro-
optimization for performance in half a decade.

~~~
throwaway_yy2Di
An ignorant question -- why do people prefer to render graphics to SVG/DOM,
when that's such a noticeable performance bottleneck?

~~~
felxh
I can't speak for others, but if you write browser apps/websites for a generic
audience you basically have no choice. The reason consist of two letters: IE

~~~
throwaway_yy2Di
But IE didn't support SVG either until recently? If I read right, it was
supported since the same version as HTML5 canvas (IE 9),

[http://caniuse.com/#cats=SVG](http://caniuse.com/#cats=SVG)

[http://caniuse.com/canvas](http://caniuse.com/canvas)

~~~
timmclean
Tools like [http://raphaeljs.com/](http://raphaeljs.com/) allow you to support
older versions of IE.

~~~
vidarh
But likewise you can use excanvas.js to let your canvas code run on IE
(emulates it using VML)

------
throwaway_yy2Di
I think in most cases where you'd worry about JS array performance you should
use actual numeric arrays [0] rather than the kitchen sink Array(). Also, I
think those function abstractions have a pretty significant overhead?

[0] [https://developer.mozilla.org/en-
US/docs/Web/JavaScript/Type...](https://developer.mozilla.org/en-
US/docs/Web/JavaScript/Typed_arrays)

(edit): Yeah, the abstraction overhead is ridiculous. Here's the forEach()
benchmark again, compared to an explicit for loop (no function calls):

    
    
        // new benchmark in bench/for-each.js    
    
        exports['explicit iteration'] = function() {
            acc = 0;
            for (var j=0; j<input.length; ++j) {
                acc += input[j];
            }
        }
    
      Native .forEach() vs fast.forEach() vs explicit iteration
        ✓  Array::forEach() x 2,101,860 ops/sec ±1.50% (79 runs sampled)
        ✓  fast.forEach() x 5,433,935 ops/sec ±1.12% (90 runs sampled)
        ✓  explicit iteration x 28,714,606 ops/sec ±1.44% (87 runs sampled)
    
        Winner is: explicit iteration (1266.15% faster)
    

(I ran this on Node "v0.11.14-pre", fresh from github).

~~~
netghost
One other micro optimization for you. Assuming the length of the array does
not change in the operation, this is usually faster:

    
    
        for (var j=0, len=input.length; j < len; ++j) {
    

This prevents rechecking the array length on every iteration.

~~~
throwaway_yy2Di
I actually didn't get any speedup with that one -- looks like V8 can optimize
that already. But you're right, that's an important one on some browsers.

You can squeeze out another factor of 2 with typed arrays:

    
    
        var input_typed = new Uint32Array(input)
        
        exports['numeric typed array'] = function() {
            var acc = 0;
            for (var j=0; j<input_typed.length; ++j) {
                acc += input_typed[j];
            }
        }
    
        ✓  Array::forEach() x 1,999,244 ops/sec ±2.93% (84 runs sampled)
        ✓  fast.forEach() x 5,161,137 ops/sec ±2.05% (85 runs sampled)
        ✓  explicit iteration x 27,851,200 ops/sec ±1.32% (86 runs sampled)
        ✓  explicit iteration w/precomputed array limit x 28,567,527 ops/sec ±1.33% (86 runs sampled)
        ✓  numeric typed array x 42,951,837 ops/sec ±0.97% (88 runs sampled)
    
        Winner is: numeric typed array (2048.40% faster)
    
    

And probably more still if you can figure out the asm.js incantation that
makes everything statically typed.

~~~
WickyNilliams
The optimisation mentioned by the grandparent is specific to looping overso-
called "live" collections of the DOM. NodeList [0] is sometimes a live
collection, and is the return type of querySelectorAll, so it's likely you've
dealt with this type of collection.

The reason it incurs an overhead is because the DOM is traversed _every single
time_ the property is read, to ensure nothing has changed. You can see why
caching the length is a reasonable optimisation, as you're unlikely to be
modifying the collection while looping.

[0]
[https://developer.mozilla.org/en/docs/Web/API/NodeList](https://developer.mozilla.org/en/docs/Web/API/NodeList)

~~~
BrandonLive
Exactly. Although, I am curious if there's a reason the JS engine (possibly
working with the DOM implementation) _can 't_ optimize that to one look-up, if
the JS in the loop doesn't change that part of the DOM or call anything that
forces it to yield.

I suspect a lot of the optimization opportunity in browsers today is less
about JS or DOM in isolation but more about ways they could work together to
improve situations like this one.

(Note: I understand this one is solvable by a proficient/attentive developer,
but not all developers are like that, and not all such DOM/JS transition
problems are as easily solvable from your JS code)

------
sheetjs
Premature optimization is the root of all evil -- Knuth

V8 has excellent profiling tools (exposed in chrome and in nodejs) which
should be used first before considering fallbacks. Before seeking a third
party library, be sure to check if the function is called many times or is
taking a long time.

For example, I found that throwing map out and using a straight array
(avoiding the function calls entirely) can be up to 50% faster than using the
functional suspects. But that, in the eyes of some people, unnecessarily adds
complexity to the code and may not be worth changing

~~~
xahrepap
I think it's a bit unfair to label using a known fast library over a known
slow library as "premature optimization". I would much rather start a project
or feature with the coding standard of, "use the fast ones", rather than have
to go back through all the code, profile it, and replace only the ones hurting
performance.

~~~
sheetjs
> start a project or feature with the coding standard of, "use the fast ones"

Ironically, that involves certain habit changes that completely obviate the
library:

1) avoid map. Just create an array and write a for loop directly at the
callsite, putting the code in a block rather than as a separate function

2) avoid indexOf. For single-character indexOf, it's much faster and much more
efficient to match the character code (loop and check charCodeAt) than to use
the function form

3) avoid lastIndexOf. Same as indexOf, except you loop in the opposite
direction.

4) avoid forEach. Learn to love the for loop.

5) avoid reduce. See forEach

Anyone embracing fast.js is sacrificing some performance to begin with.

~~~
BrandonLive
Umm okay, but that rather misses the point.

The point of this library is that it provides _the same behavior_ (for 99.99%
of cases) and the same abstraction (so same readability / maintainability) as
the functions it replaces, but with significantly better performance (for
wildly varying degrees of "significantly" depending on usage).

I don't think the idea is that you profile your code and then micro-optimize
it using this library. If you're doing that (which you should, for varying
definition of "should"), then yes you will want to consider sacrificing the
abstraction / brevity for performance.

However, this library seems like a handy way to maintain the abstraction,
while gaining performance essentially for free without sacrificing anything
worth mentioning. Then you profile (if you were going to / had time to / cared
enough to) and optimize from a better starting point. Don't see anything wrong
with that.

------
olliej
As a person who works on a JS engine I can say that a lot of the speed up in
this library is the failure to handle holes correctly - it's surprisingly
expensive to do a hole check, although there's still room in most engines to
optimise it, those checks are fairly time consuming :-/

~~~
mmastrac
This seems like a potential win for JS performance in real-world applications
-- an optimization hint to indicate whether an array is "overwhelmingly full
of holes" or something less sparse where more optimized versions of the
functions can be used.

~~~
gsnedders
But then you need to check if you need to alter the flag that says whether the
array is full of holes, which is itself an extra cost. It's hard to know
what's an overall win, adding cost to save it elsewhere.

------
grhmc
Amazing, the "performance tests" here operate on a list of only ten items
long: [https://github.com/codemix/fast.js/blob/master/bench/for-
eac...](https://github.com/codemix/fast.js/blob/master/bench/for-each.js)

I'm sure that is a statistically valid way to measure performance.

~~~
phpnode
you're right in that it's important to consider inputs of different sizes. I
did this for the indexOf() functions, I'll do the same here, thanks!

~~~
alttab
To prove its worth, try showing a _Real_ use case.

------
netcraft
I wonder how these compare to the ones in lodash.

~~~
spyder
... and to lazy.js :
[http://danieltao.com/lazy.js/](http://danieltao.com/lazy.js/)

~~~
netcraft
Thanks, I hadn't come across this one before. Looks like they are expecting
some API changes in the future though so I'll just bookmark this one for now.

~~~
platz
or to scoreunder: [https://github.com/loop-
recur/scoreunder](https://github.com/loop-recur/scoreunder)

------
jrajav
Here's a jsperf of fast.js / lodash / native: [http://jsperf.com/fast-vs-
lodash](http://jsperf.com/fast-vs-lodash)

~~~
phpnode
thanks for this! it looks like there's some room for improvement in the .map /
.forEach implementations but the rest seems to stack up pretty well.

~~~
bzbarsky
The native bind in Firefox is much faster than the fast.js one (not
surprising, since it has explicit JIT support).

------
dgreensp
It's fairly well-known that Array#forEach is up to an order of magnitude
slower than a for-loop, across browsers. The usual reason given is the
overhead of invoking a closure from the VM. A JS implementation of forEach
ought to be somewhere in the middle.

The speed-up for "concat" is surprising to me. I wonder if it holds for
"splice" and if that is true across browsers.

~~~
nijiko
And it can be made even faster ;)

[http://jsperf.com/fast-iter](http://jsperf.com/fast-iter)

------
Kiro
So the forEach magic that is so much faster is... a normal for loop:

    
    
      exports.forEach = function fastForEach (subject, fn, thisContext) {
        var length = subject.length,
            i;
        for (i = 0; i < length; i++) {
          fn.call(thisContext, subject[i], i, subject);
        }
      };
    

I knew that forEach was slower than a normal for loop but I was expecting
something more.

------
simonsarris
Here's another one for you, one of my favorites that used to be drastic:
[http://jsperf.com/pipetrunc](http://jsperf.com/pipetrunc)

    
    
        blah | 0; // fast!
        Math.floor(blah); // slow(er)! (except on FF nightly)
    

Caveat: Only works with numbers greater than -2147483649 and less than
2147483648.

~~~
sheetjs
Another caveat: for negative numbers, the bit-or rounds to zero while the
floor rounds to negative infinity:

    
    
        > -0.5
        -0.5
    
        > (-0.5)|0
        0
    
        > Math.floor(-0.5)
        -1

~~~
al2o3cr
A fine example why this sort of "who needs the specs, FULL SPEED AHEAD" hyper-
optimization can get people into trouble.

------
illumen
I did this with CPython and psyco. It turns out that writing map() in python
was faster than the builtin C version. Because the JIT was allowed to do some
tricks, like inlining the function.

------
syg
The slowness of functional methods like .map and .forEach for a time was due
to their not being self-hosted. Since then, both V8 and SpiderMonkey self-host
them, and bz has posted some numbers below [1].

But perf problems are more numerous still for these functional methods,
because compilers in general have trouble inlining closures, especially for
very polymorphic callsites like calls to the callback passed in via .map or
.forEach. For an account of what's going on in SpiderMonkey, I wrote an
explanation about a year ago [2]. Unfortunately, the problems still persist
today.

[1]
[https://news.ycombinator.com/item?id=7938101](https://news.ycombinator.com/item?id=7938101)
[2] [http://rfrn.org/~shu/2013/03/20/two-reasons-functional-
style...](http://rfrn.org/~shu/2013/03/20/two-reasons-functional-style-is-
slow-in-spidermonkey.html)

------
VeejayRampay
Javascript. The language where you can reimplement basic functions such as
map, each, reduce (which by the way are still available for objects in 2014)
and have them be faster than their native counterparts.

It might be that I don't particularly like the language. but it's kind of
frightening that we're building the world on that stuff.

~~~
phpnode
An alternative interpretation might be - JavaScript, the high level language
language that's so awesome that it can out perform native code*

* __as long as you make it do less work __

~~~
VeejayRampay
Why isn't the native version doing less work? Isn't that what you expect from
a language design, that the native version will be as close to optimal as
possible in the first place?

~~~
kristiandupont
Did you read the article? These functions are not 100% equivalent.

~~~
VeejayRampay
Fair enough. Why doesn't the title of the post say so then? "Faster incomplete
versions of Javascript native functions" would have been a better summary.

Or even discuss the fact that if this library is good enough for people to use
then maybe the edge cases that the native versions are covering might not be
so useful after all?

------
nathanb
I'm a bit concerned about this...

On one hand, I'm a big proponent of "know your tools". I'll gladly use a fast
sort function that falls apart when sorting arrays near UINT32_MAX size if I'm
aware of that caveat ahead of time and I use it only in domains where the size
of the array is logically limited to something much less than that, for
example.

But on the other hand, I write operating system code in C. I need to know that
the library functions I call are going to protect me against edge cases so I
don't inadvertently introduce security holes or attack vectors.

If I know that some JS I'm interacting with is using fast.js, maybe there will
be some input I can craft in order to force the system into a 1% edge case.

The lesson here is probably "don't use this for your shopping cart", but we
need to be careful deriding Javascript's builtins for being slow when really
they're just being safe.

~~~
phpnode
This is actually unlikely to be a problem. The edge cases ignored here are
actually the same as those ignored by underscore.js, which is obviously a very
popular library.

~~~
nathanb
I agree that it's unlikely to be a problem...until your site gets popular and
a .01% corner case becomes a weekly occurrence or until someone sees that
you're using fast and exploits an attack vector.

~~~
phpnode
it's not that kind of edge case. It simply means that some very uncommon
constructs are not supported. If you never use those constructs, which
basically nobody does, then it will never fail. (and in fact the failure mode
is pretty soft and unlikely to crash your application). If your code worked
with this once, it will always work.

------
MokiD
A while back I was programming for a naive but well-written JS interpreter for
set top box apps, where map was generally being avoided because of
performance.

I wrote quite a fast "map" (along with the others) that looked a bit like:

    
    
      exports.map = function fastMap (subject, fn, thisContext) {
        var i = subject.length,
            result = new Array(i);
        if (thisContext) {
          while (i--) {
            result[i] = fn.call(thisContext, subject[i], i, subject);
          }
        } else {
          while (i--) {
            result[i] = fn(subject[i], i, subject);
          }
        }
        return result;
      };
    

I'm not sure if I just used "result = []", but on modern browsers I think
that'd be recommended. But yeah, if you're programming for a web browser then
using another impl of map is probably going to be a waste of time.

------
aikah
Some V8 people here ? how can a JS re-implementation be faster than the native
implementation of a function ?

~~~
chrisseaton
They're non compliant - it's a meaningless comparison as the two
implementations do different things.

~~~
coldtea
Nothing "meaningless" about it.

It's not like they do apples and oranges.

They do slightly different varieties of apples -- ignore some BS edge cases
that few ever use in the real world.

If I rewrite project X into X v2 and throw away 2-3 seldom used features in
the process, it doesn't mean that comparing v1 and v2 is meaningless. You
compare for what people actually use it for -- the main use cases. Not for
everything nobody cares about.

~~~
Isofarro
Developers use forEach to apply a function to every element in an array. That
is the main use case.

This faster version fails on some arrays (any array that isn't contiguous).
This is a bug, not a feature. The library user is now responsible for checking
that the array is contiguous before using the faster function.

Is it documented somewhere whether array returning/manipulating functions
cause non-contiguity?

~~~
coldtea
> _This faster version fails on some arrays (any array that isn 't
> contiguous). This is a bug, not a feature._

I'd argue it's actually a future.

For one, it's not forEach for one. It doesn't mask the native implementation.
It exists in its own namespace.

Second, it's not like forEach doesn't have its own problems. Like not being
backwards compatible to older browsers without shims.

Third, nobody (or very very small values of "somebody") use non contiguous
arrays with forEach.

The speed is a great feature, especially if you don't sacrifice anything to
achieve it.

> _The library user is now responsible for checking that the array is
> contiguous before using the faster function._

The library user already knows if he is using contiguous arrays or not. After
all, it can fuck him over in tons of other cases, even a simple for loop, or a
length check, if he treats one like the other. So it's not like he doesn't
already have to be vigilant about it.

------
cordite
I'm interested in what those edge cases are, to say it works in 99% of the
cases but provide no caveats makes me think that I might be surprised by
something if I use it.

~~~
phpnode
As well as the example of "sparse arrays" shown in the README, there are some
additional slight gotchas around `.bind()`, e.g.
[https://developer.mozilla.org/en-
US/docs/Web/JavaScript/Refe...](https://developer.mozilla.org/en-
US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind#Bound_functions_used_as_constructors)

These fall outside of the common uses of these methods, and most people don't
even know they exist.

------
seanewest
I think this shows that standards bodies could implement _less_ native
language functionality and let community-made libraries/tools compete for
those areas. ES6 goes so far as to implement a native module system, seriously
calling into question any effort by the community at large to implement a
competing system (e.g. browserify, requirejs).

------
franze
micro-benchmarks are the root of all evil

[http://www.youtube.com/watch?v=65-RbBwZQdU](http://www.youtube.com/watch?v=65-RbBwZQdU)
Vyacheslav Egorov - LXJS 2013 talk

i don't know if he actually said these word, but it was the overall theme of
this (very entertaining and very enlightening) talk.

------
dabernathy89
> there is essentially no performance difference between native functions and
> their JavaScript equivalents

> native functions often have to cover complicated edge cases from the
> ECMAScript specification, which put them at a performance disadvantage.

Aren't these opposing statements?

~~~
phpnode
no, but I could probably have worded it better. What I mean to say is that if
you do this in JS:

    
    
       function add (a, b) {
         return a + b;
       }
    

and this in C:

    
    
      int add(int a,int b)
      {       
         return a + b;
      }
    

you're going to get essentially the same performance.

The latter sentence you quoted refers to the specific builtin functions that
fast.js re-implements. So we first get close to native performance in JS, then
we beat the builtin functions by doing less work overall.

~~~
dabernathy89
gotcha, thanks for the clarification

------
idbehold
Can you provide benchmarks against Lo-Dash?

------
peterkelly
I think the forEach issue is a bad example, and something that could (and
arguably should) be handled by the native implementation. The reason they get
faster execution here is by breaking the spec.

A native implementation could have a single flag associated with the array
recording whether it is sparse, and use the more efficient code path given
here in the common space where it's non-sparse.

~~~
scott_s
The point I took away from that one example was: The native functions have to
deal with lots of edge cases, which causes them to be slower. By implementing
similar functions which, per their spec, do not handle those edge cases, we
can gain a significant performance improvement.

------
gpvos
It would have been nice though if they would have documented exactly _which_
edge cases they're neglecting.

------
talles
I wonder which projects does actually _need_ that.

Hey I'm not bashing here, I thing it's kind cool for learning purposes
attempts to do such thing, but I truly wonder if there is an actual production
need for such thing.

~~~
nijiko
Game engines, anything that needs to do cycles at a high rate (rendering,
algorithms, etc)

------
CountHackulus
No mention of relative memory usage though. I've been bitten by this kind of
thing too many times in node in the past to not wonder about it here.
Especially for such core functions.

------
hexleo
First time I meet fast.js, I think it's running fast. When I read introduction
I was wrong, it just write fast... I hope one day js run in phone is really
FAST.

~~~
hexleo
I make a big mistake, next time I will read more carefully.

------
nijiko
Submitted a pull-request that does decrementing iterations which in some
browsers / engines can give an increase in performance due to less
instruction.

------
webXL
Looks like John-David Dalton has some work cut out for him!

~~~
grumblestumble
From what I remember, a primary reason Lodash was created as a fork of
underscore was to address the "solution" that fastjs provides - underscore had
problems with edge cases like sparse arrays, so lodash was created as a spec-
compliant alternative.
[http://stackoverflow.com/a/13898916/1869609](http://stackoverflow.com/a/13898916/1869609)

------
golem_de
did you ever google for fartscroll?

------
cristiantincu
> In fact, native functions often have to cover complicated edge cases from
> the ECMAScript specification, which put them at a performance disadvantage.

What. Is. This. I don’t even.

~~~
sp332
So if you simplify the implementation, it gets faster but more fragile. Why is
that confusing?

~~~
pestaa
If you simplify the specification, a proper implementation gets faster with no
downside.

Why can't we build software on simpler platforms with better implementations?

~~~
sp332
Doing something complex on a simple platform requires complex code.
[https://en.wikipedia.org/wiki/Turing_tarpit](https://en.wikipedia.org/wiki/Turing_tarpit)
Having a more complex platform can make things a lot easier for the
programmer.

~~~
pestaa
Simplicity never meant easy. Doing something complex on a simple platform may
as well use simple constructs and benefit from no edge cases.

For example, Haskell provides only a handful of constructs but allows the
developer to build sophisticated systems.

Though I agree complex platforms are not necessarily complicated.

