Is it more performant to mix types or to keep types?

Tags: , ,



I was wondering, with v8, when storing class attributes, is it more performant to use one variable with mixed types or one variable per type? Consider the following snippets (written in TypeScript for making the difference more clear):

type Type = "number" | "string";
type FooChild = number | string;

class Foo {
    val?: FooChild;
    _type: Type = "number";
    constructor(initialValue: FooChild, initialType: Type) {
        this.val = initialValue;
        this._type = initialType;
    }
    getValue() {
        return this.val;
    }
}

class Bar {
    str?: string;
    val?: number;
    _type: Type = "number";
    constructor(initialValue: FooChild, initialType: Type) {
        if (initialType === "string") this.str = initialValue as string;
        else this.val = initialValue as number;
        this._type = initialType;
    }
    getValue() {
        return this._type === "string" ? this.str : this.val;
    }
}

Afaik both should have advantages or defaults. Using one variable would have the problem that the JS engine might make assumptions on the types, optimize it for that type, and have to deoptimize it is it turns out that it actually also accepts numbers. But using multiple ones make that a lot of junk variables are lying around in each instance taking much space in memory for nothing.

I made this little benchmark to see if one outperforms the other, and it seems that using only one variable is more performant. But I also know that such benchmarks are usually not good at replicating real world situations, so I am seeking confirmation from more knowledgeable programmers.

Answer

(V8 developer here.) Define “more performant”: faster to create such objects, faster to read their properties, faster to modify them? You can often improve one of these operations at the cost of another 🙂

In most cases in practice, I’d expect both approaches to behave similar enough that it doesn’t matter, so you can use what’s simpler.

It’s true that in some cases, an engine can apply certain tricks and shortcuts when a given object property has always had a number as its value, and it’s probably possible (I haven’t tried) to craft a microbenchmark that makes it appear to have significant impact. But if the objects in your use case naturally have to store properties of various types, then modeling the distinction by hand (as your example does) comes with its own performance cost, so there’s a good chance that it won’t pay off.

TL;DR: Don’t worry about it, let the engine do its thing. When you have a full app and are dissatisfied with its performance, profile it (running under realistic conditions) and find the lowest-hanging fruit, i.e. the bottlenecks where you get the biggest impact from investing some effort (and maybe code complexity) into improving things. I am doubtful that teasing one property into two will be one of the more effective optimizations you can do, and it might even hurt overall performance (though I can imagine that in some very specific circumstances it might help).

[Side note: do disregard the microbenchmark you’ve linked, whatever its results are. For one, the way both versions of it assign instance.str = randint(... (or .val, or .bar) is clearly a violation of the class’s contract, where it’s illegal to assign str or val without updating _type. Secondly, before doing anything with the results of such a microbenchmark, you want to make sure that it’s actually measuring what you think it’s measuring. What percentage of the time is being spent in randint? Are the objects actually allocated, or has the optimizing compiler realized that they’re never used and eliminated the allocations entirely? Thirdly, is “one allocation, a thousand assignments with 50% probability of changing the type, zero reads” really representative of the real workload you’re interested in? Never trust a microbenchmark unless you’ve studied in depth what the engine actually ends up doing!]



Source: stackoverflow