Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optimise dict equality check on JavaScript #738

Merged
merged 1 commit into from
Dec 3, 2024

Conversation

richard-viney
Copy link
Contributor

The dict equality check now returns false as soon as any item doesn't match, rather than continuing to unnecessarily check remaining items.

@richard-viney richard-viney marked this pull request as ready for review November 13, 2024 19:56
src/dict.mjs Outdated Show resolved Hide resolved
src/dict.mjs Outdated Show resolved Hide resolved
@richard-viney
Copy link
Contributor Author

I'll look into adding ES6-style iterators and see what the performance is like.

@richard-viney richard-viney marked this pull request as draft November 14, 2024 18:32
@richard-viney richard-viney force-pushed the js-optimise-dict-equality branch from b5ffd1f to 1821ff0 Compare November 20, 2024 22:06
@richard-viney
Copy link
Contributor Author

The fastest ES6 iterator I came up with iterates at about half the speed of the forEach() approach. Maintaining the iterator state is slightly involved, and a few extra temporary objects get created, which is probably what makes it slower.

Here's the code:

/**
 * @template K,V
 * @param {undefined | Node<K,V>} root
 * @returns {IterableIterator<[K, V]>}
 */
function iterator(root) {
  // Return an empty iterator if there's no root node
  if (root === undefined) {
    return [][Symbol.iterator]();
  }

  // Stack of nodes in the trie that tracks the iterator's current position
  const nodes = [[root?.array, 0]];

  // Quick access to the current node being iterated
  let currentNodeItems = nodes[0][0];
  let currentNodeItemIndex = nodes[0][1];

  return {
    next() {
      while (true) {
        // If the current node is fully iterated then pop it off the stack
        if (currentNodeItemIndex >= currentNodeItems.length) {
          nodes.pop();

          // Finish when the final node is popped off
          if (nodes.length === 0) {
            return { done: true };
          }

          currentNodeItems = nodes[nodes.length - 1][0];
          currentNodeItemIndex = nodes[nodes.length - 1][1];
        } else {
          const item = currentNodeItems[currentNodeItemIndex];

          // Move the current node to its next item
          currentNodeItemIndex++;

          if (item === undefined) {
            continue;
          } else if (item.type === ENTRY) {
            return { value: [item.k, item.v], done: false };
          } else {
            nodes[nodes.length - 1][1] = currentNodeItemIndex;

            // Iterate into this child node
            nodes.push([item.array, 0]);

            currentNodeItems = item.array;
            currentNodeItemIndex = 0;
          }
        }
      }
    },

    [Symbol.iterator]() {
      return this;
    },
  };
}

It may be hard/impossible to match the speed of forEach() with an ES6-style iterator here.

I don't know of any 3rd-party uses of Dict.forEach() in JS FFI code that currently return false from the callback function and therefore would be broken by this change, but it can't be definitively ruled out. Should we make it a separate function?

@richard-viney richard-viney marked this pull request as ready for review November 20, 2024 22:36
@richard-viney richard-viney force-pushed the js-optimise-dict-equality branch 4 times, most recently from ab07691 to e10f6aa Compare November 28, 2024 12:37
@lpil
Copy link
Member

lpil commented Nov 28, 2024

I agree, that's too much of a performance hit.

Returning false is not an option. How about we use throw- that's a procedural way to break out of a loop and it infers no runtime cost for the other uses.

@richard-viney richard-viney force-pushed the js-optimise-dict-equality branch from e10f6aa to 3a46c8f Compare November 28, 2024 20:55
@richard-viney
Copy link
Contributor Author

Great idea, I've switched to throwing a unique symbol on inequality and propagating any other exceptions that may occur.

Note that the prelude is currently silently swallowing all exceptions from an equals() call, so any other exceptions will never be seen:

image

Do you recall why the prelude does that?

@richard-viney richard-viney force-pushed the js-optimise-dict-equality branch 2 times, most recently from ac586b1 to bee3e1f Compare November 28, 2024 21:01
@richard-viney richard-viney force-pushed the js-optimise-dict-equality branch from bee3e1f to af8f71d Compare November 29, 2024 01:49
@lpil
Copy link
Member

lpil commented Dec 3, 2024

Ah right. It's because == should never throw, and we're calling any function called equals, but we don't know what that might do.

The code looks good to me, it is ready to merge? Or does that catch cause a problem?

@richard-viney
Copy link
Contributor Author

Ready to merge 👍

Copy link
Member

@lpil lpil left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!!

@lpil lpil merged commit 2f3b2bb into gleam-lang:main Dec 3, 2024
7 checks passed
@richard-viney richard-viney deleted the js-optimise-dict-equality branch December 4, 2024 00:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants