Javascript Classes v. Closures (2/3)
Performance
Overview
This is part 2 of a series of posts where I explore the tradeoffs between Javascript classes and the closure pattern (known frequently also as the factory class pattern), two patterns that can be used as substitutes for each other. The series is in three parts
- The closure pattern is more lintable than the class pattern.
- The class pattern tends to perform better than the closure pattern.
- The class pattern is more monkey-patchable than the closure pattern.
I will compare both memory efficiency of the two patterns, and then CPU efficiency. But first:
Background: The Object Prototype
To appreciate this post, you should be familiar with Javascript’s object prototype. I recommend this very detailed overview if you are not. Salient details:
- You can make multiple objects with the same prototype using the new keyword.
- You can put methods on this prototype.
- You can call those methods as if they were attached directly to the object, and when you do so, inside the body of the method,
this
will refer to your object.
This snippet which uses a class:
class Friend () {
constructor (greeting) {
this.greeting = greeting
}
greet () {
console.log(this.greeting)
}
}let englishman = new Friend('hullo')
let cowboy = new Friend('howdy')englishman.greet()
cowboy.greet()
can be roughly imagined as
function Friend (greeting) {
return { greeting: greeting }
}function greet () {
console.log(this.greeting)
}let englishman = Friend('hullo')
let cowboy = Friend('howdy')this = englishman; greet()
this = cowboy; greet()
Whereas this snippet that uses a closure
function Friend (greeting) {
function greet () {
console.log(greeting)
}
return { greet: greet }
}let englishman = Friend('hullo')
let cowboy = Friend('howdy')
englishman.greet()
cowboy.greet()
can be roughly imagined as
let englishman = {
greet: function () {
console.log('hullo')
}
}
let cowboy = {
greet: function () {
console.log('howdy')
}
}englishman.greet()
cowboy.greet()
Memory Efficiency
Notice that the ‘rough imagining’ of the class implementation only has one function literal, whereas the ‘rough imagining’ of the closure implementation has two different function literals. Similarly, only one function will live in memory while the class version of the program is running, but two separate functions will live in memory while the closure version of the program is running. Classes “save space” by reusing methods across different instances of the class.
Why not test this empirically, by running some contrived code on my laptop? Here is the contrived code:
$ cat classes.js
'use strict'
class Cow {
constructor (lungCapacity) {
this.lungCapacity = lungCapacity
this.airInLungs = 0
}
getAirInLungs() {
return this.airInLungs
}
breathe () {
this.airInLungs = this.lungCapacity
}
moo () {
let output = 'm'
let air = this.getAirInLungs()
while (air --> 0) { // The 'goes to' operator
output += 'o'
}
this.airInLungs = air
return output
}
}
const herd = []
for (let i = 0; i < 30000; i++) {
const cow = new Cow(i)
cow.index = i
herd.push(cow)
}
console.log(process.memoryUsage())
const start = Date.now()
herd.map(cow => {
cow.breathe()
cow.moo()
})
console.log('Finished mooing in ' + (Date.now() - start) / 1000 + ' seconds')
and its closure-based analog:
$ cat closures.js
'use strict'
function Cow (lungCapacity) {
let airInLungs = 0
function breathe () {
airInLungs = lungCapacity
}
function getAirInLungs () {
return airInLungs
}
function moo () {
let output = 'm'
let air = getAirInLungs()
while (air --> 0) { // The 'goes to' operator.df
output += 'o'
}
airInLungs = air
return output
}
return {breathe:breathe, moo:moo}
}
const herd = []
for (var i = 0; i < 30000; i++) {
const cow = Cow(i)
cow.index = i
herd.push(cow)
}
console.log(process.memoryUsage())
const start = Date.now()
herd.map(function(cow) {
cow.breathe()
cow.moo()
})
console.log('Finished mooing in ' + (Date.now() - start) / 1000 + ' seconds')
Now let’s run them!
$ nvm use 6
Now using node v6.9.1 (npm v3.10.8)
$ node classes.js
{ rss: 29143040, heapTotal: 11571200, heapUsed: 6064928 }
Finished mooing in 3.147 seconds
$ node closures.js
{ rss: 41410560, heapTotal: 29396992, heapUsed: 15649208 }
Finished mooing in 5.315 seconds
Looks like the closures version used substantially more memory. That was to be expected based on the analysis above: “classes save space by reusing methods across instances”. Why the boost in speed though?
CPU Performance
It turns out this is a little more complicated a question. I recommend reading a back-and-forth between Marijn Haverbeke (author of Eloquent Javascript) and Vyacheslav Egorov (a V8 developer at Google).
First, as Haverbeke muses, there are some reasons to expect that closures might be quicker. In principle, they are simpler, in a sense. The number of closed variables in the scope of a closure is fixed. They take up a fixed space in memory. Whereas, the number (and type) of properties on an object is not fixed. They can be changed at any time. So if you had many many instances of a closure, you might expect the runtime to be able to deal with them very efficiently, because it has stronger guarantees about their uniformity than it would if they were instances of a class.
I will admit I do not fully grok Egorov’s very technical response, but the understanding that I do get is, basically, v8 rocks at doing classes. Despite how you do not have a strict guarantee that instances of a class will be uniform, the runtime is nevertheless very good at sorting them into uniform “hidden classes” anyway, and taking advantage of any uniformity that does exist. So any uniformity advantage of closures is effectively negated.
Furthermore, the V8 runtime engine is able to optimize frequently called methods. Instances of classes share methods, so if you call the same method on a bunch of instances of the class, that method will be frequently called and V8 is able to optimize it and effectively use ‘inline caching’. For closures, on the other hand, each closure has its own copy of the method. So if you call the method of the same name on a bunch of instances of a closure — it’s not actually the same method. They are all different methods. So V8 won’t optimize it as effectively.
In principle, it’s possible, says Egorov, for V8 to be a little bit smarter and do static analysis on the code to perform some of these optimizations for closures when feasible. But — at least at the time of his writing — nothing like this has really been implemented.
In the meantime, classes tend to be more performant than closures both in terms of memory and CPU. Of course, in much code, performance is of lesser concern than readability and maintainability. But, since the two patterns are equivalent in many ways, the performance aspects may be something that tips the balance for you.
Make sure to stay tuned for the next and final post in the series. I’ll discuss how classes are more monkey-patchable — and thus sometimes more testable — than closures.
Update:
A previous version of this post contained a mistake which slightly biased the results against the ‘closure’ version. The code and the results have been updated to correct this. Great thanks to achen2345 of reddit!
I’ve made available a browser-based version of these examples, so that you can run the tests in your own browser and see how performance is affected. Let’s get those cows to mooing!
—
If you enjoy thinking hard about Javascript, consider joining our team of classy developers at Livestream.