【问题标题】:Lifespan of JS closure context objects?JS 闭包上下文对象的寿命?
【发布时间】:2021-07-03 00:27:01
【问题描述】:

背景

我正在尝试将 elixir 的 actor 模型 语言原语移植到 JS 中。我想出了一个解决方案(在 JS 中)来模拟 receive elixir 关键字,使用“接收器”函数和生成器。

这是一个简化的实现和演示,向您展示这个想法。

API

type ActorRef: { send(msg: any): void }
type Receiver = (msg: any) => Receiver
/**
 * `spawn` takes a `initializer` and returns an `actorRef`.
 * `initializer` is a factory function that should return a `receiver` function.
 * `receiver` is called to handle `msg` sent through `actorRef.send(msg)`
 */
function spawn(initializer: () => Receiver): ActorRef

演示

function* coroutine(ref) {
  let result
  while (true) {
    const msg = yield result
    result = ref.receive(msg)
  }
}

function spawn(initializer) {
  const ref = {}
  const receiver = initializer()
  ref.receive = receiver
  const gen = coroutine(ref)
  gen.next()

  function send(msg) {
    const ret = gen.next(msg)
    const nextReceiver = ret.value
    ref.receive = nextReceiver
  }

  return { send }
}

function loop(state) {
  console.log('current state', state)
  return function receiver(msg) {
    if (msg.type === 'ADD') {
      return loop(state + msg.value)
    } else {
      console.log('unhandled msg', msg)
      return loop(state)
    }
  }
}

function main() {
  const actor = spawn(() => loop(42))
  actor.send({ type: 'ADD', value: 1 })
  actor.send({ type: 'BLAH', value: 1 })
  actor.send({ type: 'ADD', value: 1 })
  return actor
}

window.actor = main()

关注

以上模型有效。但是我有点担心这种方法对性能的影响,我不清楚它创建的所有闭包上下文对内存的影响。

function loop(state) {
  console.log('current state', state) // <--- `state` in a closure context  <─┐    <─────┐
  return function receiver(msg) {     // ---> `receiver` closure reference  ──┘          │
    if (msg.type === 'ADD') {                                                            │
      return loop(state + msg.value)  // ---> create another context that link to this one???
    } else {
      console.log('unhandled msg', msg)
      return loop(state)
    }
  }
}

loop 是返回“接收者”的“初始化器”。为了保持内部状态,我将它(state 变量)保存在“接收器”函数的闭包上下文中。

当前接收者收到消息后,可以修改内部状态,并将其传递给loop,递归创建一个新接收者替换当前接收者。

显然,新的接收者也有一个新的闭包上下文来保持新的状态。在我看来,这个过程可能创建了一个阻止 GC 的深层链接上下文对象链?

我知道闭包引用的上下文对象可以在某些情况下被链接。如果它们是链接的,那么它们显然不会在最里面的闭包被释放之前被释放。据this articleV8优化在这方面很保守,图片不好看。

问题

如果有人能回答这些问题,我将不胜感激:

  1. loop 示例是否创建了深度链接的上下文对象?
  2. 在这个例子中上下文对象的生命周期是什么样的?
  3. 如果当前示例没有,这种receiver 创建receiver 机制是否最终会在其他情况下创建深度链接的上下文对象?
  4. 如果对问题 3 回答“是”,您能否举一个例子来说明这种情况?

跟进 1

@TJCrowder 的后续问题。

闭包是词法的,因此它们的嵌套遵循源代码的嵌套。

说得好,这很明显,但我错过了????

只是想确认我的理解是正确的,举一个不必要的复杂例子(请多多包涵)。

这两个在逻辑上是等价的:

// global context here

function loop_simple(state) {
  return msg => {
    return loop_simple(state + msg.value)
  }
}

// Notations:
// `c` for context, `s` for state, `r` for receiver.
function loop_trouble(s0) { // c0 : { s0 }
  // return r0
  return msg => {   // c1 : { s1, gibberish } -> c0
    const s1 = s0 + msg.value
    const gibberish = "foobar"
    // return r1
    return msg => { // c2 : { s2 } -> c1 -> c0
      const s2 = s1 + msg.value
      // return r2
      return msg => {
        console.log(gibberish)
        // c3 is not created, since there's no closure
        const s3 = s2 + msg.value
        return loop_trouble(s3)
      }
    }
  }
}

但是对内存的影响是完全不同的。

  1. 步入loop_troublec0被创建持有s0;返回r0 -&gt; c0
  2. 进入r0,创建c1,持有s1gibberish,返回r1 -&gt; c1
  3. 步入r1,创建c2,持有s2,返回r2 -&gt; c2

我相信在上述情况下,当r2(最里面的箭头函数)被用作“当前接收者”时,实际上不仅仅是r2 -&gt; c2,而是r2 -&gt; c2 -&gt; c1 -&gt; c0,所有三个上下文对象都被保留(如果我在这里已经错了,请纠正我。

问题:哪种情况是真的?

  1. 之所以保留所有三个上下文对象,仅仅是因为我故意在其中放入了 gibberish 变量。
  2. 或者即使我删除了gibberish,它们也会被保留。也就是说s1 = s0 + msg.value的依赖就足够链接c1 -&gt; c0了。

跟进 2

因此,作为“容器”的环境记录始终被保留,因为容器中包含的“内容”可能因引擎而异,对吧?

一种非常幼稚的未优化方法可能会盲目地将所有局部变量以及argumentsthis 包含到“内容”中,因为规范没有说明任何关于优化的内容。

更聪明的方法可以是查看嵌套函数并检查确切需要什么,然后决定将什么包含到内容中。这在article I linked 中被称为“促销”,但这条信息可以追溯到 2013 年,恐怕它可能已经过时了。

您是否有更多关于此主题的最新信息要分享?我对 V8 如何实现这种策略特别感兴趣,因为我目前的工作严重依赖于电子运行时。

【问题讨论】:

  • P.S.我保证向提供有用信息的人提供 500 赏金。目前我还不能开始。
  • 下面的答案没用吗?
  • @T.J.Crowder 是的!非常感谢!我正忙着给你写一个后续问题,请耐心等待。

标签: javascript garbage-collection closures v8


【解决方案1】:

注意:此答案假设您使用的是strict mode。你的 sn-p 没有。我建议总是使用严格模式,通过使用 ECMAScript 模块(自动处于严格模式)或将 "use strict"; 放在代码文件的顶部。 (如果你想使用松散模式,我必须更多地考虑arguments.callee.caller 和其他类似的怪物,我没有在下面。)

  1. 循环示例是否创建了深度链接的上下文对象?

不深,不。对loop 的内部调用不会将这些调用创建的上下文链接到对其进行调用的上下文。重要的是函数loop 是在哪里创建的,而不是从哪里调用它的。如果我这样做:

const r1 = loop(1);
const r2 = r1({type: "ADD", value: 2});

这会创建两个函数,每个函数都关闭创建它的上下文。该上下文是对loop 的调用。该调用上下文链接到声明 loop 的上下文——您的 sn-p 中的全局上下文。对loop 的两次调用的上下文不相互链接。

  1. 在这个例子中上下文对象的生命周期是什么样的?

只要引用它的接收器函数被保留(至少在规范方面),它们中的每一个都会被保留。当接收器函数不再有任何引用时,它和上下文都符合 GC 条件。在我上面的示例中,r1 不保留 r2r2 不保留 r1

  1. 如果当前示例没有,此接收器创建接收器机制是否最终会在其他情况下创建深度链接的上下文对象?

很难排除一切,但我不这么认为。闭包是词法的,所以它们的嵌套遵循源代码的嵌套。

  1. 如果对问题 3 回答“是”,您能否举一个例子来说明这种情况?

不适用


注意:在上面,我使用了与您在问题中相同的方式使用“上下文”,但可能值得注意的是,保留的是 environment record,它是调用创建的执行上下文的一部分一个函数。闭包不保留执行上下文,环境记录是。但区别是非常小的一个,我提到它只是因为如果您深入研究规范,您会看到这种区别。


你的后续行动 1:

c3 没有被创建,因为没有闭包

c3 被创建,只是在调用结束后它没有被保留,因为它没有关闭。

问题:哪种情况是真的?

两者都没有。无论是否存在gibberish 变量或s0 参数或s1 变量等,都会保留所有三个上下文(c0c1c2)。 context 不必为了存在而具有参数或变量或任何其他绑定。考虑:

// ge = global environment record

function f1() {
    // Environment record for each call to f1: e1(n) -> ge
    return function f2() {
        // Environment record for each call to f2: e2(n) -> e1(n) -> ge
        return function f3() {
            // Environment record for each call to f3: e3(n) -> e2(n) -> e1(n) -> ge
        };
    };
}

const f = f1()();

即使e1(n)e2(n)e3(n) 没有参数或变量,它们仍然存在(在上面它们至少有两个绑定,一个用于arguments,一个用于@987654350 @,因为那些不是箭头函数)。在上面的代码中,只要f 继续引用f1()() 创建的f3 函数,e1(n)e2(n) 都会被保留。

至少,规范是这样定义的。理论上,这些环境记录可以被优化掉,但这是 JavaScript 引擎实现的一个细节。 V8 在某个阶段进行了一些闭包优化,但大部分都放弃了,因为(据我了解)它在执行时间上的成本比它在内存减少方面所弥补的要多。 但即使他们在优化时,我认为他们优化的是环境记录的内容(删除未使用的绑定之类的东西),而不是它们是否继续存在。 见下文,我发现一篇 2018 年的博文表明他们确实有时会完全忽略它们。


再跟进2:

所以环境记录作为“容器”总是被保留...

在规范方面,是的;这不一定是引擎真正的作用。

...容器中包含的“内容”可能因引擎而异,对吧?

是的,规范要求的只是行为,而不是你如何实现它。从上面链接的environment records 部分:

环境记录是纯粹的规范机制,不需要对应于 ECMAScript 实现的任何特定工件。

...但那条信息可以追溯到 2013 年,恐怕它可能已经过时了。

我想是的,是的,尤其是因为从那时起 V8 就有了changed engines entirely,用 Ignition 和 TurboFan 替换了 Full-codegen 和 Crankshaft。

您是否有更多关于此主题的最新信息要分享?

不是真的,但我确实找到了this V8 blog post from 2018,它说他们在某些情况下会“省略”上下文分配。所以肯定会有一些优化。

【讨论】:

  • @TJCrowder 嘿,我发布了后续问题。请您看一看好吗?
  • @hackape - 我已添加到上述内容的末尾以回答后续问题。编码愉快!
  • 非常丰富的答案!这里有后续2请看。感谢您的宝贵时间,很抱歉挤压了您的大脑。如果没有什么可以添加的。有空时我会给你赏金。目前 SO 不允许我这样做。
  • @hackape - 再次更新。 :-) 不要担心赏金(除非你已经把它放在问题上,但我认为你没有)。编码愉快!
  • 非常感谢!我知道你可能不需要那个赏金,但承诺就是承诺,我需要为我自己的缘故兑现它。快乐编码:-D
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-03-21
  • 2016-01-12
  • 2013-10-14
  • 1970-01-01
相关资源
最近更新 更多