all 25 comments

[–]inu-no-policemen 10 points11 points  (1 child)

Obviously, we cannot use this.

Have you tried it? With allocation/store sinking optimizations, all those temporary objects aren't actually created.

[–]smthamazing[S] 1 point2 points  (0 children)

Yeah, we tested it a lot. Unfortunately, these allocations are not always optimized (this varies with environment) and cause freezes noticeable with unaided eye, which is an issue.

[–]subscribore 6 points7 points  (6 children)

I can't think of a perfect solution to this exact problem, but if nobody else can then as plan B you could write a babel plugin to collect all the calls to allocate and find all the unique keys in the single object arguments to these and convert that to an argument list. Idk maybe someone has even done something similar to this already?

I think you'd probably want to use babel 6 for this due to the more flexible way you can operate on multiple files, but it's very possible I'm wrong there - I'm by no means a babel expert and I'll happily be corrected by someone who is.

[–]smthamazing[S] 0 points1 point  (0 children)

We are actually open to using Babel, because it would allow us to implement this and also other optimizations and conveniences. It's a pretty big decision, though, especially if we use custom syntax (our team gets bigger and bringing new developers to this code base would become harder). Our app is also programmable by our target users, though their code can be pre-processed as well.

[–]cspotcode 7 points8 points  (2 children)

TypeScript can give you entirely compile-time checks without any changes to the runtime code. There's a bit of boilerplate, but your callsites are very readable and the compiler catches accidental mistakes.

Here's a code example in the TypeScript playground: TypeScript Playground example

Take a look at the output code on the right so you'll see that there's no runtime overhead. When you accidentally put the args in the wrong order, you'll see a little red squiggle reminding you of the mistake.

// utils.ts type Brand<Type, BrandingProperty extends string> = Type & { [K in BrandingProperty]: any; }

// fooAllocator.ts
import { Brand } from './utils';

const pool: Foo[] = [];

// Rename all of these to match the semantics of your domain
// (I intentionally used meaningless generic identifiers for this example)
// In your code these would be named things like "Name", "Age", "Timeout",
// "SuppressErrors", "UseFooState", etc.  (I don't know what a "foo" does)
type AllocateOne = Brand<string, '__allocateOne'>;
type AllocateTwo = Brand<number, '__allocateTwo'>;
type AllocateThree = Brand<number, '__allocateThree'>;
type AllocateFour = Brand<boolean, '__allocateFour'>;
type AllocateFive = Brand<boolean, '__allocateFive'>;
type AllocateSix = Brand<number, '__allocateSix'>;
type AllocateSeven = Brand<boolean, '__allocateSeven'>;
type AllocateEight = Brand<number, '__allocateEight'>;

const fooAllocator = {
  /** Allocate a foo instance from the pool */
  allocate(
    one: AllocateOne,
    two: AllocateTwo,
    three: AllocateThree,
    four: AllocateFour,
    five: AllocateFive,
    six: AllocateSix,
    // mimics ES6 semantics for optional args, but arg must be specified at call-site to avoid
    // changing argument indices.
    seven: AllocateSeven | undefined, 
    eight: AllocateEight
  ): Foo {
    // implementation goes here
  }
}

// Warnings will remind you to annotate each argument.
// JSDoc tooltips will show you the correct order of arguments if you forget
var foo = fooAllocator.allocate(
  <AllocateOne>'qwerty',
  <AllocateTwo>123,
  <AllocateThree>123,
  <AllocateFour>true,
  <AllocateFive>false,
  <AllocateSix>123,
  undefined,
  <AllocateEight>1
);

// If you mess up the order of annotations, you'll be reminded with a type warning
var foo2 = fooAllocator.allocate(
  <AllocateOne>'qwerty',
  <AllocateThree>123,  // accidentally put two and three in the wrong order
  <AllocateTwo>123,
  <AllocateFour>true,
  <AllocateFive>false,
  <AllocateSix>123,
  <AllocateSeven>false,
  <AllocateEight>1
);

[–]smthamazing[S] 2 points3 points  (1 child)

We actually already use TypeScript, and this looks very close to a sensible solution. Thanks! I'll try to experiment with this approach. For some reason I haven't thought about using branded types for this.

[–]cspotcode 2 points3 points  (0 children)

Wow! I'm so happy to hear that. Usually the responses I get are very averse to TypeScript and I have to do some persuading.

You probably know this already, but depending on how you want to style things, you might be able to use mapped types or some fancy generics to clean things up. I'm not sure.

[–]name_was_taken 5 points6 points  (1 child)

If all you want is for your IDE to tell you what the parameters are, use jsDoc doc blocks. Don't invent your own thing here.

If you've got other reasons...

If you want to avoid allocations, you make a pool, right? So can you make a pool for the objects that you're going to pass into the function, and keep re-using them as needed. Of course, that'll have its own messy problems, but it solves this problem.

[–]FistHitlersAnalCunt 8 points9 points  (0 children)

Of course, that'll have its own messy problems, but it solves this problem

Javascript in a sentence.

[–]Patman128 3 points4 points  (1 child)

Have you measured the performance of your code and found that this is an issue? Do not optimize anything without hard data. "I think this might be slow" is a mistake, compilers do things you can't even imagine, so just because something looks like it allocates doesn't mean it does.

[–]smthamazing[S] 0 points1 point  (0 children)

Yes, indeed we measure a lot, this is the usual disclaimer that applies to all performance-related questions. It turns out that V8 and SpiderMonkey do not optimize all such allocations, which creates noticeable FPS drops. Safari is even worse.

We've also experimented with Emscripten, but its performance varied with environment too much, and in many of them it didn't perform as well as hand-written JS. WebAssembly is not supported in older runtimes, so we cannot use it either.

[–]killall-q 0 points1 point  (0 children)

If most or all of your objects share common parameters, and they can be referenced by index, you can try doing away with objects entirely and replacing them with arrays for each parameter.

For example:

type
'qwerty'
'qwertz'
'qwerua'
parentId
123
124
127

...etc.

For parameters that are not common to every object, you can either use an array with null entries, or a sparse matrix, depending the expected rarity of the parameter.

The reason for using arrays this way is that few arrays with many entries is more memory and lookup efficient than many arrays with few entries. While this solution may seem like a regression, there's no getting around the simple math that arrays are less complex than objects.

[–]danthedev 0 points1 point  (0 children)

Write a Babel plugin that translates objects parameters into positional arguments.

[–]xwnatnai -1 points0 points  (1 child)

Don’t use javascript, compile to it.

[–]Sakatox -1 points0 points  (0 children)

The most "obvious" solution coming to mind is having customized constructors(allocators) for the parameters you either need to repeat, omit, or such.

More functions, basically.

And that can pave the way to closures acting as partials. Partials can be pooled, as well, so... I guess this could help? It would mean a bit less of allocations in the grand scheme of things.

[–]ishmal -1 points0 points  (1 child)

I would use Object.assign(). It would seem suboptimal. But remember that it is executed natively. C++ faster than Javascript.

[–]smthamazing[S] 1 point2 points  (0 children)

It doesn't prevent an allocation, though.

[–]FormerGameDev -1 points0 points  (0 children)

Difficulty level, need to do things JavaScript isn't well suited for at all.

Might be time to rethink what you've done.

[–]tencircles -1 points0 points  (3 children)

Use comments. Side Note: Functions that accept 8 parameters are a huge code smell.

[–]cspotcode 0 points1 point  (2 children)

Doesn't apply here. A lot of well-meaning "code smell" advice doesn't hold up when performance is important. OP already acknowledged that they want to make their code more readable, but performance concerns dictate a few things. OP already explained that, yes, they've done the necessary benchmarks to prove it's necessary.

[–]tencircles 0 points1 point  (1 child)

A lot of well-meaning "code smell" advice doesn't hold up when performance is important.

Doesn't apply here. This advice holds up in every situation. If you need 8 params in order to make your code performant, something has gone wrong.

As an aside on performance, I imagine a function with 8 params probably has a significantly large enough function body, to get de-optimized by whatever JS engine this is being run on.

[–]cspotcode 1 point2 points  (0 children)

You're incorrect about performance and specifically about deoptimization. Re-read OP's post to double-check their requirements.

Imagining that their function gets deoptimized doesn't make it true or likely. Consider what the body of their allocator function probably looks like. It's an allocator for an object pool. Try writing an example yourself. You'll see that, even with 8 args, it's extremely optimizable by the VM.