10

I was just wondering how the overhead is on a function object.

In an OOP design model, you can spawn up a lot of objects each with their own private functions, but in the case where you have 10,000+, these private function objects, I assume, can make for a lot of overhead.

I'm wondering if there are cases where it would be advantageous enough to move these functions to a utility class or external manager to save the memory taken up by these function objects.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Kevin Wang
  • 3,290
  • 1
  • 28
  • 40
  • 8
    the js engine can recycle identical function objects, so 10,000 function vars in user land can really be 1 function and 10,000 pointers... – dandavis Jun 25 '13 at 22:11
  • 2
    Are you planning on using the utility class? It’ll take up even more memory… anyways, this a) depends on the engine and b) is probably not something to worry about unless you see it become a problem. – Ry- Jun 25 '13 at 22:11
  • My guess is that the interpreters optimize this fairly well, but I'm sure it's possible to write terrible code. Is there a way to audit JS memory usage? – landons Jun 25 '13 at 22:11
  • 2
    @landons: chrome has a task manager + profiles + timeline in dev tools to watch JS ram usage. – dandavis Jun 25 '13 at 22:12
  • There are many different JS engines/renderers and all of them have different memory usage techniques. Voted to close as too broad. – Codeman Jun 25 '13 at 22:21
  • @dandavis awesome, that's great to know. If you add this as an answer I will take it. – Kevin Wang Jun 25 '13 at 22:30
  • @dandavis: I thought so too, but I tried and indeed **total** sharing is not done even for functions that are not closures. I didn't check with the specs but probably it's required by the standard that every time you evaluate a function literal a new different object is returned anyway. Of course the code itself can be shared (and I bet it is in all decent implementations) but some **small** amount of memory for each function is needed. – 6502 Jun 26 '13 at 07:31
  • @6502: yeah, enough memory to store a pointer, which according to the answer below, is 21 bytes. since unicode strings use two bytes per char, even just "function(){}" itself uses 24 bytes un-parsed, more than observed. abigger function would be a better test... also, closures won't matter here since the function object is distinct from the activation object. finally, the ecma3 spec explicitly allowed recycling, but i don't recall seeing anything one way or the other in 5. – dandavis Jun 26 '13 at 07:56
  • 1
    @dandavis: Can you provide a pointer to the specs where recycling of the full function object is permitted? That would make things like `x = function(){...}; x.foo = [];` a no-no and ECMA docs are for me as readable as an encrypted file. – 6502 Jun 26 '13 at 08:27
  • @6502 The [specs](http://es5.github.io/#x13) are pretty clear: " The production *FunctionExpression* [...] is evaluated as follows: Return the result of creating a **new Function object** ..." But that would not stop smart engines from working around this. Slightly related: http://ariya.ofilabs.com/2012/07/lazy-parsing-in-javascript-engines.html – user123444555621 Jun 26 '13 at 17:13
  • @dandavis - I believe my original test was inaccurate. Please see the edit I made below. – Travis J Jun 26 '13 at 17:46
  • @TravisJ: well, there's a difference between Function and function, so you aren't testing the same thing as outlined in the OP or as in your first test; of course a constructor gives a new object when the new keyword is used, but it's not the same as a function declaration or even a function expression. – dandavis Jun 26 '13 at 17:57
  • @dandavis - Function constructs an anonymous function. `var a = Function("");typeof(a);"function"`. – Travis J Jun 26 '13 at 18:00
  • @6502: yes, read the whole page 72 of the 3rd edition, http://www.ecma-international.org/publications/files/ECMA-ST-ARCH/ECMA-262,%203rd%20edition,%20December%201999.pdf which includes the language: "in practice an implementation may detect when the differences in the [[Scope]] properties of two or more joined Function objects are not externally observable and in those cases reuse the same Function object rather than making a set of joined Function objects" – dandavis Jun 26 '13 at 18:03
  • @TravisJ: according to the ecma3 spec "Two uses of FunctionBody obtained from a call to the Function constructor 15.3.1 and 15.3.2) are never equated." (page 72) that means that function expressions and declarations are recycled but Function()s are not. – dandavis Jun 26 '13 at 18:04

3 Answers3

11

This is how Chrome handles functions, and other engines may do different things.

Let's look at this code:

var funcs = [];
for (var i = 0; i < 1000; i++) {
    funcs.push(function f() {
        return 1;
    });
}
for (var i = 0; i < 1000; i++) {
    funcs[0]();
}

http://jsfiddle.net/7LS6B/4/

Now, the engine creates 1000 functions.

The individual function itself takes up almost no memory at all (36 bytes in this case), since it merely holds a pointer to a so-called SharedFunctionInfo object, which is basically a reference to the function definition in your source code*. This is called lazy parsing.

Only when you run it frequently does the JIT kick in, and creates a compiled version of the function, which requires more memory. So, funcs[0] takes up 256 bytes in the end:

Heap analyzer screenshot

*) This is not exactly true. It also holds scope information and the function's name and other metadata, which is why it has a size of 592 bytes in this case.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
user123444555621
  • 148,182
  • 27
  • 114
  • 126
  • Is the JIT-compiled function shared as well (does `funcs[1]` run the same compiled code or does it need to get compiled on its own)? – Bergi Jun 26 '13 at 19:36
  • @Bergi Good point. I don't know what happens with a single run. However, if you run `funcs[1]` a 1000 times (while `funcs[0]`'s compiled version is cached), then the analyzer shows that `funcs[1]` is getting added a reference to `funcs[0]` (not directly to the compiled version). Not sure how that works, but looks like there's some magic involved there – user123444555621 Jun 26 '13 at 20:03
  • 3
    The 36 bytes for function identity (72 under 64-bit node) is actually a lot and in fact the deal breaker. It is 3 times more than normal object overhead (12 bytes or 24 bytes on 64-bit). Assuming node64, and if you create 200 objects, that each have 25 methods and you have 20 requests per second, you create ~7 megabytes of literal garbage per second and put insane pressure on the GC and waste a lot of a server potential. And of course on the client side if you make games or rich GUI apps you will have terrible UX from the pauses. – Esailija Sep 19 '13 at 08:37
4

First of all, it's common to place methods in the object constructor prototype, so they'll be shared among all instances of a given object:

function MyObject() {
    ....
}

MyObject.prototype.do_this = function() {
   ...
}

MyObject.prototype.do_that = function() {
   ...
}

Also note that a "function object" is a constant code-only block or a closure; in both cases the size is not related to the code:

x = [];
for (var i=0; i<1000; i++) {
    x.push(function(){ ... });
}

The size of each element of the array is not going to depend on the code size, because the code itself will be shared between all of the function object instances. Some memory will be required for each of the 1000 instances, but it would be roughly the same amount required by other objects like strings or arrays and not related to how much code is present inside the function.

Things would be different if you create functions using JavaScript's eval: In that case I'd expect each function to take quite a bit and proportional to code size unless some super-smart caching and sharing is done also at this level.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
6502
  • 112,025
  • 15
  • 165
  • 265
  • You should mention that each function needs to hold references to the scoped vars, so there is some (minor) difference depending on that. – user123444555621 Jun 26 '13 at 07:58
  • @Pumbaa80 - no, the function object doesn't hold that info, which can be late collected as needed anyway since closure prevent garbage collection. – dandavis Jun 26 '13 at 08:00
  • @Pumbaa80: in the example all functions are going to share the same closed over variables because the scope is in Javascript is the whole function. Things would be different for example with `for(var i=0; i<1000; i++){ x.push((function(i){return function(){alert(i)}}))(i)); }` where each closure would reference a different `i` variable. – 6502 Jun 26 '13 at 10:04
  • To be more precise, I'm talking about the [Environment Record](http://es5.github.io/#x10.2.1), which has to be stored alongside the function. Now how much memory does it take? Actually, I'm not aware of the implementation details of any engine, but after thinking about it, I guess there may just be a pointer to some [Lexical Environment](http://es5.github.io/#x10.2) object (in which case there would be no difference between functions). – user123444555621 Jun 26 '13 at 17:07
  • I guess one way to put this simply is: Your JS environment will have a number of function objects equal to the number of times the keyword 'function' is written in your code (or perhaps for some intelligent compilers, less than that) – Katana314 Jun 26 '13 at 18:39
  • @Katana314 Not quite. I created an [answer](http://stackoverflow.com/a/17328532/27862) to summarize my findings. – user123444555621 Jun 26 '13 at 19:15
2

Function objects do in fact take up a lot of space. Objects themselves may not take up much room as shown below but Function objects seem to take up considerably more. In order to test this, I used Function("return 2;") in order to create an array of anonymous functions.

The result was as implied by the OP. That these do in fact take up space.

Created Function

100,000 of these Function()'s created caused 75.4 MB to be used, from 0. I ran this test in a more controlled environment. This conversion is a little more obvious, where it indicates that each function object is going to consume 754 bytes. And these are empty. Larger function objects may surpass 1kb which will become significant very quickly. Spinning up the 75MB was non trivial on the client, and caused a near 4 second lock of the UI.

Here is the script I used to create the function objects:

fs = [];
for(var i = 0; i < 100000; i++ ){
 fs.push(Function("return 2;"));
}

Calling these functions also affects memory levels. Calling the functions added an additional 34MB of memory use.

Called Called

This is what I used to call them:

for( var i = 0; i < fs.length; i++ ){
 for( var a = 0; a < 1000; a++ ){
     fs[i]();
 }
}

Using jsfiddle in edit mode is hard to get accurate results, I would suggest embedding it.

Embedded jsFiddle Demo


These statements are incorrect, I left them to allow the comments to retain context.

Function objects don't take very much space at all. The operating system and memory available are going to be what decides in the end how this memory is managed. This is not going to really impact anything on a scale which you should be worried about.

When loaded on my computer, a relatively blank jsfiddle consumed 5.4MB of memory. After creating 100,000 function objects it jumped to 7.5MB. This seems to be an insignificant amount of memory per function object (the implication being 21 bytes per function object: 7.5M-5.4M / 100k).

memory image

jsFiddle Demo

Travis J
  • 81,153
  • 41
  • 202
  • 273
  • It's not quite that simple: the amount of memory used by function objects will increase as they are compiled in any JITing VM, possibly in multiple steps (depending on whether multiple compiled copies are kept at the same time). – gsnedders Jun 25 '13 at 22:51
  • @gsnedders - "The amount of memory used by function objects will increase as they are compiled". And how long do you think it takes to compile? I took the screenshot above and cropped it for brevity. It remained at the same level of 7.5MB for quite some time until it actually dropped down after several minutes. It never increased. What basis or evidence do you have for your comment? – Travis J Jun 26 '13 at 06:31
  • start calling them. And in that Fiddle you only ever have one function object (f) — you just call [[Construct]] on it 100k times, which will create (non-function) objects. – gsnedders Jun 26 '13 at 12:14
  • @gsnedders - Thank you for these comments. Although calling them did not affect memory levels, you were certainly right about the difference from instantiated functions (plain objects) and function objects. Please see my edit above. – Travis J Jun 26 '13 at 17:44
  • Your answer is only valid for JS engines with [lazy parsing](http://ariya.ofilabs.com/2012/07/lazy-parsing-in-javascript-engines.html) – user123444555621 Jun 26 '13 at 18:04
  • @Pumbaa80 - Then how do you explain no memory difference when calling the function? – Travis J Jun 26 '13 at 18:07
  • @TravisJ Chrome has further optimizations. If you call the function only once, it is *interpreted*. Run it 1000 times to see that there is a difference. http://i.imgur.com/JT5BGuI.png – user123444555621 Jun 26 '13 at 18:29
  • @Pumbaa80 - Interesting. I was able to reproduce your claim and saw a jump of 34MB to 108MB when the function was called 1000 times for all 10,000 functions. Please see my edit – Travis J Jun 26 '13 at 18:33
  • @TravisJ Pumbaa80's point was what half of mine was — JITing VMs won't generate code until a certain point. Note that with something like SpiderMonkey or V8 increasing the number of calls could increase the number of copies of compiled code. – gsnedders Jun 26 '13 at 18:41
  • @gsnedders - Thanks for the clarification. Do you know how to determine that point? – Travis J Jun 26 '13 at 18:42
  • @TravisJ It's hard. It varies on engine — in SpiderMonkey there are fixed call counts after which it happens, in Carakan it is a function of the number of bytecodes that the code contains. For a given function, you should be able to call it a number of times, and at a certain point the time it takes to execute will reduce. – gsnedders Jun 26 '13 at 18:43
  • Please don't use `Function("return 2;")` but just `function() { return 2; }` - it will be easier for the engine to decide that the functions produced in the loop have the same body. – Bergi Jun 26 '13 at 19:07
  • @Bergi - Isn't the point that all of these are different functions with different bodies? Wouldn't allowing them to be similar defeat the purpose? Or are you talking about when they are called? – Travis J Jun 26 '13 at 19:20
  • I wouldn't expect any (normal-sized) application to have 100000 different functions. If your point was to really create different functions (so that you can measure their size better) you should've used `new Function("return "+Math.random()+";")` to avoid clever optimisations :-) – Bergi Jun 26 '13 at 19:25
  • @Bergi - The point was to create different ones. At least, that was how I interpreted the OP's question. Not sure why you would need that many either to be honest. I will try it with `new` :) . Tested, didn't notice any difference. – Travis J Jun 26 '13 at 19:32
  • Function objects at minimum take 36 (+4 for pointer) bytes of memory on 32-bit and 72 (+8 for pointer) bytes memory on 64-bit. Obviously stuff like the function code and possibly closure object is shared (so it doesn't matter if you use new Function). Hell, the spec even says that a uniuqe prototype object must be created for each function identity but in V8 this is created lazily too :). 40/80 bytes just per identity is too much for anything but irrelevant CRUD UI apps anyway - there is no way to have performant code if you put such pressure on GC. – Esailija Sep 19 '13 at 08:49
  • Functions in JS have such semantics that would make the implementation code pretty crazy if you tried to avoid creating unique identities. 2 different function identities (even if created form same code) can have properties attached at any time, can be `new`'d any time that must create objects with unique prototype object and so on. ES6 provides a `const function` which has such semantics that it is feasible to implement it in a way that only 1 unique object is created. – Esailija Sep 19 '13 at 08:51