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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

The generated code for function.sent seems excessive #15521

Open
1 task
dead-claudia opened this issue Mar 27, 2023 · 4 comments 路 May be fixed by #15992
Open
1 task

The generated code for function.sent seems excessive #15521

dead-claudia opened this issue Mar 27, 2023 · 4 comments 路 May be fixed by #15992

Comments

@dead-claudia
Copy link

馃捇

  • Would you like to work on this feature?

What problem are you trying to solve?

I'm looking through the generated code for function.sent, and it's generating a lot of unnecessary functions that'd get lazily compiled anyways (and thus wouldn't benefit anything).

Describe the solution you'd like

Let's take this relatively simple source:

export function *foo() {
  console.log("1", function.sent)
  yield
  console.log("2", function.sent)
}

Here's how it's currently compiled as of v7.21.3:

function _skipFirstGeneratorNext(fn) { return function () { var it = fn.apply(this, arguments); it.next(); return it; }; }
export function foo() {
  return _foo.apply(this, arguments);
}
function _foo() {
  _foo = _skipFirstGeneratorNext(function* () {
    let _functionSent = yield;
    console.log("1", _functionSent);
    _functionSent = yield;
    console.log("2", _functionSent);
  });
  return _foo.apply(this, arguments);
}

Here's how I feel it should be compiled:

function _skipFirstGeneratorNext(g) { g.next(); return g; }
export function foo() {
  return _skipFirstGeneratorNext(_foo.apply(this, arguments));
}
function *_foo() {
  let _functionSent = yield;
  console.log("1", _functionSent);
  _functionSent = yield;
  console.log("2", _functionSent);
}

Note the following:

  1. No functions generated at runtime. (This dramatically reduces the runtime overhead.)
  2. Only one extra function needed per transpiled generator, reducing code size.
  3. No references to immutable functions, making inlining analysis simpler.

The helper's small enough, it might even be worth eliminating entirely for only a very slight increase in size (we're talking single digits in toy examples, wouldn't even be noticed in the real world) compared to both a simpler implementation and a possible mild increase in perf-sensitive code (due to the .next() call being made monomorphic):

export function foo() {
  let g = _foo.apply(this, arguments);
  g.next();
  return g;
}
function *_foo() {
  let _functionSent = yield;
  console.log("1", _functionSent);
  _functionSent = yield;
  console.log("2", _functionSent);
}

Here are the tradeoffs with that alternative approach:

  • Slight increase in size that is noticeable pre-compression but nearly washes away post-compression.
  • Provides a slightly simpler transpiler implementation.
  • In perf-sensitive code, the .next() call becomes obviously monomorphic, speeding up construction very slightly. (I doubt this would show up outside microbenchmarks, though.) Once engines can monomorphize calls post-inlining (I don't believe any can currently), this very slight perf advantage won't exist anymore.
Longer example with 5 generators

This is written to be a little less academic and a little more real-world. You could imagine this being part of a streaming micro-framework built on top of generators. I provided gzip results of each as well to give an idea how it compresses.

Source:

  • Minified: 658 bytes
  • Min+gzip: 261 bytes
export function *echo() {
  while (true) {
    yield function.sent
  }
}
export function *map(iter, f) {
  let success = true
  try {
    while (true) {
      f(iter.next(function.sent))
      yield
    }
  } catch (e) {
    success = false
    iter.throw(e)
  } finally {
    if (success) iter.return()
  }
}
export function *recover(iter, f) {
  let success = true
  try {
    let sent = function.sent
    while (true) {
      iter.next(sent)
      try {
        sent = yield
      } catch (e) {
        sent = f(e)
      }
    }
  } catch (e) {
    success = false
    iter.throw(e)
  } finally {
    if (success) iter.return()
  }
}
export function *makeObserver(onNext, onThrow, onReturn) {
  let success = true
  try {
    while (true) {
      onNext(function.sent);
      yield;
    }
  } catch (e) {
    success = false
    onThrow(e)
  } finally {
    if (success) onReturn()
  }
}
export function *throttle(iter, ms) {
  let timer
  let success = true
  try {
    try {
      while (true) {
        let sent = function.sent
        timer = setTimeout(() => iter.next(sent), ms)
        yield
      }
    } finally {
      if (timer) clearTimeout(timer)
    }
  } catch (e) {
    success = false
    iter.throw(e)
  } finally {
    if (timer) clearTimeout(timer)
    if (success) iter.return()
  }
}

// Hypothetical compression to 658 bytes, 261 after piping through `gzip`
// Generated by translating `function.sent` to/from `function_sent` and running
// the translated form through `npx terser --module -cm`
export function*echo(){for(;;)yield function.sent}export function*map(t,e){let n=!0;try{for(;;)e(t.next(function.sent)),yield}catch(e){n=!1,t.throw(e)}finally{n&&t.return()}}export function*recover(t,e){let n=!0;try{let r=function.sent;for(;;){t.next(r);try{r=yield}catch(t){r=e(t)}}}catch(e){n=!1,t.throw(e)}finally{n&&t.return()}}export function*makeObserver(t,e,n){let r=!0;try{for(;;)t(function.sent),yield}catch(t){r=!1,e(t)}finally{r&&n()}}export function*throttle(t,e){let n,r=!0;try{try{for(;;){let r=function.sent;n=setTimeout((()=>t.next(r)),e),yield}}finally{n&&clearTimeout(n)}}catch(e){r=!1,t.throw(e)}finally{n&&clearTimeout(n),r&&t.return()}}

Current compilation result as of v7.21.3:

  • Minified: 1217 bytes (increase of 85% over source)
  • Min+gzip: 375 bytes (increase of 44% over source)
function _skipFirstGeneratorNext(fn) { return function () { var it = fn.apply(this, arguments); it.next(); return it; }; }
export function echo() {
  return _echo.apply(this, arguments);
}
function _echo() {
  _echo = _skipFirstGeneratorNext(function* () {
    let _functionSent = yield;
    while (true) {
      _functionSent = yield _functionSent;
    }
  });
  return _echo.apply(this, arguments);
}
export function map(_x, _x2) {
  return _map.apply(this, arguments);
}
function _map() {
  _map = _skipFirstGeneratorNext(function* (iter, f) {
    let _functionSent2 = yield;
    let success = true;
    try {
      while (true) {
        f(iter.next(_functionSent2));
        _functionSent2 = yield;
      }
    } catch (e) {
      success = false;
      iter.throw(e);
    } finally {
      if (success) iter.return();
    }
  });
  return _map.apply(this, arguments);
}
export function recover(_x3, _x4) {
  return _recover.apply(this, arguments);
}
function _recover() {
  _recover = _skipFirstGeneratorNext(function* (iter, f) {
    let _functionSent3 = yield;
    let success = true;
    try {
      let sent = _functionSent3;
      while (true) {
        iter.next(sent);
        try {
          sent = _functionSent3 = yield;
        } catch (e) {
          sent = f(e);
        }
      }
    } catch (e) {
      success = false;
      iter.throw(e);
    } finally {
      if (success) iter.return();
    }
  });
  return _recover.apply(this, arguments);
}
export function makeObserver(_x5, _x6, _x7) {
  return _makeObserver.apply(this, arguments);
}
function _makeObserver() {
  _makeObserver = _skipFirstGeneratorNext(function* (onNext, onThrow, onReturn) {
    let _functionSent4 = yield;
    let success = true;
    try {
      while (true) {
        onNext(_functionSent4);
        _functionSent4 = yield;
      }
    } catch (e) {
      success = false;
      onThrow(e);
    } finally {
      if (success) onReturn();
    }
  });
  return _makeObserver.apply(this, arguments);
}
export function throttle(_x8, _x9) {
  return _throttle.apply(this, arguments);
}
function _throttle() {
  _throttle = _skipFirstGeneratorNext(function* (iter, ms) {
    let _functionSent5 = yield;
    let timer;
    let success = true;
    try {
      try {
        while (true) {
          let sent = _functionSent5;
          timer = setTimeout(() => iter.next(sent), ms);
          _functionSent5 = yield;
        }
      } finally {
        if (timer) clearTimeout(timer);
      }
    } catch (e) {
      success = false;
      iter.throw(e);
    } finally {
      if (timer) clearTimeout(timer);
      if (success) iter.return();
    }
  });
  return _throttle.apply(this, arguments);
}

// Compressed with `npx terser --module -cm` to 1217 bytes, 375 after piping through `gzip`
function t(t){return function(){var n=t.apply(this,arguments);return n.next(),n}}export function echo(){return n.apply(this,arguments)}function n(){return(n=t((function*(){let t=yield;for(;;)t=yield t}))).apply(this,arguments)}export function map(t,n){return r.apply(this,arguments)}function r(){return(r=t((function*(t,n){let r=yield,e=!0;try{for(;;)n(t.next(r)),r=yield}catch(n){e=!1,t.throw(n)}finally{e&&t.return()}}))).apply(this,arguments)}export function recover(t,n){return e.apply(this,arguments)}function e(){return(e=t((function*(t,n){let r=yield,e=!0;try{let i=r;for(;;){t.next(i);try{i=r=yield}catch(t){i=n(t)}}}catch(n){e=!1,t.throw(n)}finally{e&&t.return()}}))).apply(this,arguments)}export function makeObserver(t,n,r){return i.apply(this,arguments)}function i(){return(i=t((function*(t,n,r){let e=yield,i=!0;try{for(;;)t(e),e=yield}catch(t){i=!1,n(t)}finally{i&&r()}}))).apply(this,arguments)}export function throttle(t,n){return l.apply(this,arguments)}function l(){return(l=t((function*(t,n){let r,e=yield,i=!0;try{try{for(;;){let i=e;r=setTimeout((()=>t.next(i)),n),e=yield}}finally{r&&clearTimeout(r)}}catch(n){i=!1,t.throw(n)}finally{r&&clearTimeout(r),i&&t.return()}}))).apply(this,arguments)}

My suggestion with helper:

  • Minified: 933 bytes (increase of 41% over source, improvement of 23% over v7.21.3)
  • Min+gzip: 332 bytes (increase of 27% over source, improvement of 11% over v7.21.3)
function _skipFirstGeneratorNext(g) { g.next(); return g; }
export function echo() {
  return _skipFirstGeneratorNext(_echo.apply(this, arguments));
}
function *_echo() {
  let _functionSent = yield;
  while (true) {
    _functionSent = yield _functionSent;
  }
}
export function map(_x, _x2) {
  return _skipFirstGeneratorNext(_map.apply(this, arguments));
}
function *_map(iter, f) {
  let _functionSent2 = yield;
  let success = true;
  try {
    while (true) {
      f(iter.next(_functionSent2));
      _functionSent2 = yield;
    }
  } catch (e) {
    success = false;
    iter.throw(e);
  } finally {
    if (success) iter.return();
  }
}
export function recover(_x3, _x4) {
  return _skipFirstGeneratorNext(_recover.apply(this, arguments));
}
function *_recover(iter, f) {
  let _functionSent3 = yield;
  let success = true;
  try {
    let sent = _functionSent3;
    while (true) {
      iter.next(sent);
      try {
        sent = _functionSent3 = yield;
      } catch (e) {
        sent = f(e);
      }
    }
  } catch (e) {
    success = false;
    iter.throw(e);
  } finally {
    if (success) iter.return();
  }
}
export function makeObserver(_x5, _x6, _x7) {
  return _skipFirstGeneratorNext(_makeObserver.apply(this, arguments));
}
function *_makeObserver(onNext, onThrow, onReturn) {
  let _functionSent4 = yield;
  let success = true;
  try {
    while (true) {
      onNext(_functionSent4);
      _functionSent4 = yield;
    }
  } catch (e) {
    success = false;
    onThrow(e);
  } finally {
    if (success) onReturn();
  }
}
export function throttle(_x8, _x9) {
  return _skipFirstGeneratorNext(_throttle.apply(this, arguments));
}
function *_throttle(iter, ms) {
  let _functionSent5 = yield;
  let timer;
  let success = true;
  try {
    try {
      while (true) {
        let sent = _functionSent5;
        timer = setTimeout(() => iter.next(sent), ms);
        _functionSent5 = yield;
      }
    } finally {
      if (timer) clearTimeout(timer);
    }
  } catch (e) {
    success = false;
    iter.throw(e);
  } finally {
    if (timer) clearTimeout(timer);
    if (success) iter.return();
  }
}

// Compressed with `npx terser --module -cm` to 933 bytes, 332 after piping through `gzip`
function t(t){return t.next(),t}export function echo(){return t(e.apply(this,arguments))}function*e(){let t=yield;for(;;)t=yield t}export function map(e,n){return t(r.apply(this,arguments))}function*r(t,e){let r=yield,n=!0;try{for(;;)e(t.next(r)),r=yield}catch(e){n=!1,t.throw(e)}finally{n&&t.return()}}export function recover(e,r){return t(n.apply(this,arguments))}function*n(t,e){let r=yield,n=!0;try{let l=r;for(;;){t.next(l);try{l=r=yield}catch(t){l=e(t)}}}catch(e){n=!1,t.throw(e)}finally{n&&t.return()}}export function makeObserver(e,r,n){return t(l.apply(this,arguments))}function*l(t,e,r){let n=yield,l=!0;try{for(;;)t(n),n=yield}catch(t){l=!1,e(t)}finally{l&&r()}}export function throttle(e,r){return t(i.apply(this,arguments))}function*i(t,e){let r,n=yield,l=!0;try{try{for(;;){let l=n;r=setTimeout((()=>t.next(l)),e),n=yield}}finally{r&&clearTimeout(r)}}catch(e){l=!1,t.throw(e)}finally{r&&clearTimeout(r),l&&t.return()}}

My suggestion with no helper:

  • Minified: 971 bytes (increase of 45% over source, improvement of 20% over v7.21.3)
  • Min+gzip: 339 bytes (increase of 30% over source, improvement of 10% over v7.21.3)
export function echo() {
  let g = _echo.apply(this, arguments);
  g.next();
  return g;
}
function *_echo() {
  let _functionSent = yield;
  while (true) {
    _functionSent = yield _functionSent;
  }
}
export function map(_x, _x2) {
  let g = _map.apply(this, arguments);
  g.next();
  return g;
}
function *_map(iter, f) {
  let _functionSent2 = yield;
  let success = true;
  try {
    while (true) {
      f(iter.next(_functionSent2));
      _functionSent2 = yield;
    }
  } catch (e) {
    success = false;
    iter.throw(e);
  } finally {
    if (success) iter.return();
  }
}
export function recover(_x3, _x4) {
  let g = _recover.apply(this, arguments);
  g.next();
  return g;
}
function *_recover(iter, f) {
  let _functionSent3 = yield;
  let success = true;
  try {
    let sent = _functionSent3;
    while (true) {
      iter.next(sent);
      try {
        sent = _functionSent3 = yield;
      } catch (e) {
        sent = f(e);
      }
    }
  } catch (e) {
    success = false;
    iter.throw(e);
  } finally {
    if (success) iter.return();
  }
}
export function makeObserver(_x5, _x6, _x7) {
  let g = _makeObserver.apply(this, arguments);
  g.next();
  return g;
}
function *_makeObserver(onNext, onThrow, onReturn) {
  let _functionSent4 = yield;
  let success = true;
  try {
    while (true) {
      onNext(_functionSent4);
      _functionSent4 = yield;
    }
  } catch (e) {
    success = false;
    onThrow(e);
  } finally {
    if (success) onReturn();
  }
}
export function throttle(_x8, _x9) {
  let g = _throttle.apply(this, arguments);
  g.next();
  return g;
}
function *_throttle(iter, ms) {
  let _functionSent5 = yield;
  let timer;
  let success = true;
  try {
    try {
      while (true) {
        let sent = _functionSent5;
        timer = setTimeout(() => iter.next(sent), ms);
        _functionSent5 = yield;
      }
    } finally {
      if (timer) clearTimeout(timer);
    }
  } catch (e) {
    success = false;
    iter.throw(e);
  } finally {
    if (timer) clearTimeout(timer);
    if (success) iter.return();
  }
}

// Compressed with `npx terser --module -cm` to 971 bytes, 339 after piping through `gzip`
export function echo(){let e=t.apply(this,arguments);return e.next(),e}function*t(){let t=yield;for(;;)t=yield t}export function map(t,r){let n=e.apply(this,arguments);return n.next(),n}function*e(t,e){let r=yield,n=!0;try{for(;;)e(t.next(r)),r=yield}catch(e){n=!1,t.throw(e)}finally{n&&t.return()}}export function recover(t,e){let n=r.apply(this,arguments);return n.next(),n}function*r(t,e){let r=yield,n=!0;try{let l=r;for(;;){t.next(l);try{l=r=yield}catch(t){l=e(t)}}}catch(e){n=!1,t.throw(e)}finally{n&&t.return()}}export function makeObserver(t,e,r){let l=n.apply(this,arguments);return l.next(),l}function*n(t,e,r){let n=yield,l=!0;try{for(;;)t(n),n=yield}catch(t){l=!1,e(t)}finally{l&&r()}}export function throttle(t,e){let r=l.apply(this,arguments);return r.next(),r}function*l(t,e){let r,n=yield,l=!0;try{try{for(;;){let l=n;r=setTimeout((()=>t.next(l)),e),n=yield}}finally{r&&clearTimeout(r)}}catch(e){l=!1,t.throw(e)}finally{r&&clearTimeout(r),l&&t.return()}}

Describe alternatives you've considered

Leaving it as-is. If it ain't broke, don't fix it. This is just an optimization suggestion anyways.

Documentation, Adoption, Migration Strategy

Nothing really to document or migrate to - this is just an output optimization.

@babel-bot
Copy link
Collaborator

Hey @dead-claudia! We really appreciate you taking the time to report an issue. The collaborators on this project attempt to help as many people as possible, but we're a limited number of volunteers, so it's possible this won't be addressed swiftly.

If you need any help, or just have general Babel or JavaScript questions, we have a vibrant Slack community that typically always has someone willing to help. You can sign-up here for an invite.

@nicolo-ribaudo
Copy link
Member

With #15922 we now generate this code:

var _foo;
export function foo() {
  return (_foo = _foo || babelHelpers.skipFirstGeneratorNext(function* () {
    let _functionSent = yield;
    console.log("1", _functionSent);
    _functionSent = yield;
    console.log("2", _functionSent);
  })).apply(this, arguments);
}

@liuxingbaoyu
Copy link
Member

Thank you for your detailed report, the changes mentioned here I will consider including in the next PR. :)

@dead-claudia
Copy link
Author

I just realized: my desugaring isn't correct when side-effecting default parameters are present. Those will have to be moved after the first yield.

So, that will need accounted for.

@liuxingbaoyu liuxingbaoyu linked a pull request Sep 22, 2023 that will close this issue
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants