Proposal: Function static variables and blocks

A proposal to add a static keyword for variable declaration in functions.
Proposal repository: yehoryatskevych/proposal-function-static-variables (github.com)

The problem

Allocation in JavaScript can hardly be called "fast". JavaScript is a great language, and its features significantly reduce development time, making it the best language for prototyping and fast development. However, it is still an interpreted language, which takes a lot of time for native calls and allocations. Therefore, it would be great to have more options for manual code optimization.

Let's consider an example with a function that just creates a 3D vector using some extra parameter:

function task(i) {
    new Vector3(i + 1.13, i + 5.231, i + 7.1247);
}

Every time this function is called, JavaScript allocates a new object for the Vector3 instance. We can easily measure the execution time of this function by calling it 10^8 times:

const ITERATIONS_NUM = Math.pow(10, 8);

console.time('task');
for (let i = 0; i < ITERATIONS_NUM; ++i) {
    task(i);
}
console.timeEnd('task');

The result:

task: 920.739ms

I don't think that's a good result. So let's test again, but move the vector allocation to the global scope:

let vec = new Vector3(0, 0, 0);

function task(i) {
    vec.set(i + 1.13, i + 5.231, i + 7.1247);
}

const ITERATIONS_NUM = Math.pow(10, 8);

console.time('task');
for (let i = 0; i < ITERATIONS_NUM; ++i) {
    task(i);
}
console.timeEnd('task');

The result:

task: 78.667ms

Looks much better, even though we are setting the value every call. But is it reasonable to put all variables into the global context? I don't think so, especially if your goal is to write high-performance code with many functions that need to be optimized. So you can use closure with an Immediately Invoked Function Expression (IIFE), like this:

const task = (() => {
    let vec = new Vector(0, 0, 0);
    return function task(i) {
        vec.set(i + 1.13, i + 5.231, i + 7.1247);
    }
})();

However, we are immediately confronted with the fact that we can't use function declarations, and hoisting no longer works for us. So we have to declare these functions before we use them, and we already get restrictions that slow down development speed and interfere with the desired structuring of the code, especially when there are a lot of such functions. Using such optimizations in methods becomes almost impossible.

The solution

One solution to this problem could be local static variables, as implemented in some other C-like low-level languages. These variables would allow us to optimize memory allocation and function execution time by allocating and initializing the variables only once after the first function call. Subsequent function calls would use the already allocated variable with the last value. It's similar to global variables that can only be accessed from the specific function in which they are declared.

Here is an example of how it could be used:

function task(i) {
    static let vec = new Vector(0, 0, 0);
    vec.set(i + 1.13, i + 5.231, i + 7.1247);
}

and a more complex example of how it should work:

function func() {
    static let isFirstCall = true;
    static let counter = 0;

    if (isFirstCall) {
        isFirstCall = false;
        console.log('Static variables initialized!');
    }

    console.log("Counter:", counter);

    counter++;
}

func(); // OUT: "Static variables initialized!", "Counter: 0"
func(); // OUT: "Counter: 1"
func(); // OUT: "Counter: 2"

This code proposal introduces a highly useful feature for functions with high execution rates and constants that require complex operations for initialization. It allows functions to store their own state for subsequent executions and remain independent of the global context. With this feature, functions can efficiently handle their own state persistence and eliminate the need for global variables or external dependencies.

To make it more consistent with the current static implementation and more flexible it also can be added with the support of class-static-block proposal.

function func() {
    static let counter = 0;

    static {
        console.log('Static variables initialized!');
    };

    console.log("Counter:", counter);

    counter++;
}

func(); // OUT: "Static variables initialized!", "Counter: 0"
func(); // OUT: "Counter: 1"
func(); // OUT: "Counter: 2"

Additional benefits

In addition to helping with optimization, it can also be useful to store some function states independently from global scope for example for util/helper functions that need to have some state and pre-allocation, refactoring is also made easier due to the elimination of the need to move variables and "helpers" for the "helper".

function funtionWithComplexPreallocation(value) {
    static const magic = 6816;
    static const magicOffset = 285615417;
    static const magicRatio = complexOperation(magic);
    return magicOffset + (value * magicRatio);   
}

or

function funtionWithComplexPreallocation(value) {
    static const magic = 6816;
    static const magicOffset = 285615417;
    static let magicRatio;

    static {
        for (let i = 0; i < 1024; ++i) {
            magicRatio = Math.sqrt(magicRatio) * magic;
        }
    }

    return magicOffset + (value * magicRatio);   
}

Previous discussion: C-style static variables

3 Likes

Note that you could also write:

function task (i) {
  task.vec ??= new Vector(0, 0, 0);
  task.vec.set(i + 1.13, i + 5.231, i + 7.1247);
}

which should probably be almost as fast as your IIFE example

I've sometimes found myself slightly wanting a feature like this, but mostly for use-cases like "I'm making an id-retrieving function that keeps an internal state of the last ID used in order to decide the next one", or that sort of thing.

Though, for me, do expressions (if they ever come) would be a good enough solution. Not sure that it would satisfy you though, since you're saying that you'd still like function hosting and what-not, which do expressions can't provide, and they're technically not much better than IIFEs, just less clunky.

No, it is not reasonable to put those variables in the global scope. However, you really should use modules, and it is totally reasonable to declare such variables in the module scope. Just don't export them.

const declarations are still hoisted, though yes, you need to initialise them before you use them. This is usually not a problem in well-structured code that only calls functions from the script entrypoint, not during module initialisation.
And really, I would not want to rely on hoisting for initialising static variables, having the order of evaluation of static blocks depend on the order of calls to the functions seems like a nightmare to me.

Any thoughts how we can combine two approaches with my Lazy inlined constants ?