繁体   English   中英

蹦床递归堆栈溢出

[英]Trampoline recursion stack overflow

我有这个递归函数sum ,它计算传递给它的所有数字的总和。

 function sum(num1, num2, ...nums) { if (nums.length === 0) { return num1 + num2; } return sum(num1 + num2, ...nums); } let xs = []; for (let i = 0; i < 100; i++) { xs.push(i); } console.log(sum(...xs)); xs = []; for (let i = 0; i < 10000; i++) { xs.push(i); } console.log(sum(...xs)); 

如果仅向其传递“很少”的数字,则它工作正常,否则将导致call stack溢出。 因此,我尝试对其进行一些修改并使用trampoline以便它可以接受更多参数。

 function _sum(num1, num2, ...nums) { if (nums.length === 0) { return num1 + num2; } return () => _sum(num1 + num2, ...nums); } const trampoline = fn => (...args) => { let res = fn(...args); while (typeof res === 'function') { res = res(); } return res; } const sum = trampoline(_sum); let xs = []; for (let i = 0; i < 10000; i++) { xs.push(i); } console.log(sum(...xs)); xs = []; for (let i = 0; i < 100000; i++) { xs.push(i); } console.log(sum(...xs)); 

虽然第一个版本不能处理10000个数字,但第二个版本却不能。 但是,如果我将100000个数字传递给第二个版本,则会再次出现call stack overflow错误。

我要说的是100000确实不是那么大(这里可能是错误的),并且看不到任何可能导致内存泄漏的失控闭包。

有谁知道这是怎么回事?

另一个答案指出了函数参数数量的限制,但是我想谈谈您的蹦床实现。 我们正在运行的长时间计算可能要返回一个函数。 如果使用typeof res === 'function' ,则不再可能将函数计算为返回值!

相反,请使用某种唯一标识符对蹦床变体进行编码

const bounce = (f, ...args) =>
  ({ tag: bounce, f: f, args: args })

const done = (value) =>
  ({ tag: done, value: value })

const trampoline = t =>
{ while (t && t.tag === bounce)
    t = t.f (...t.args)
  if (t && t.tag === done)
    return t.value
  else
    throw Error (`unsupported trampoline type: ${t.tag}`)
}

在继续之前,我们先获取一个示例函数来修复

const none =
  Symbol ()

const badsum = ([ n1, n2 = none, ...rest ]) =>
  n2 === none
    ? n1
    : badsum ([ n1 + n2, ...rest ])

我们将向其添加range数字以查看其是否有效

const range = n =>
  Array.from
    ( Array (n + 1)
    , (_, n) => n
    )

console.log (range (10))
// [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]

console.log (badsum (range (10)))
// 55

但是它可以应付大联盟吗?

console.log (badsum (range (1000)))
// 500500

console.log (badsum (range (20000)))
// RangeError: Maximum call stack size exceeded

到目前为止,在浏览器中查看结果

 const none = Symbol () const badsum = ([ n1, n2 = none, ...rest ]) => n2 === none ? n1 : badsum ([ n1 + n2, ...rest ]) const range = n => Array.from ( Array (n + 1) , (_, n) => n ) console.log (range (10)) // [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ] console.log (badsum (range (1000))) // 500500 console.log (badsum (range (20000))) // RangeError: Maximum call stack size exceeded 

1000020000之间的某个地方,我们的badsum函数不足为奇地导致堆栈溢出。

除了将函数重命名为goodsum我们只需要使用蹦床的变体对返回类型进行编码

const goodsum = ([ n1, n2 = none, ...rest ]) =>
  n2 === none
    ? n1
    ? done (n1)
    : goodsum ([ n1 + n2, ...rest ])
    : bounce (goodsum, [ n1 + n2, ...rest ])

console.log (trampoline (goodsum (range (1000))))
// 500500

console.log (trampoline (goodsum (range (20000))))
// 200010000
// No more stack overflow!

您可以在浏览器中查看此程序的结果。 现在我们可以看到,由于该程序运行缓慢,因此递归和蹦床都不会出错。 不过请放心,我们稍后会修复该问题。

 const bounce = (f, ...args) => ({ tag: bounce, f: f, args: args }) const done = (value) => ({ tag: done, value: value }) const trampoline = t => { while (t && t.tag === bounce) t = tf (...t.args) if (t && t.tag === done) return t.value else throw Error (`unsupported trampoline type: ${t.tag}`) } const none = Symbol () const range = n => Array.from ( Array (n + 1) , (_, n) => n ) const goodsum = ([ n1, n2 = none, ...rest ]) => n2 === none ? done (n1) : bounce (goodsum, [ n1 + n2, ...rest ]) console.log (trampoline (goodsum (range (1000)))) // 500500 console.log (trampoline (goodsum (range (20000)))) // 200010000 // No more stack overflow! 

额外地调用trampoline可能会很烦人,仅当您查看goodsum时,并不会立即知道那里donebounce在做什么,除非在您的许多程序中这是非常普遍的惯例。

我们可以使用通用loop函数更好地编码循环意图。 为循环提供了一个函数,该函数在调用recur时将对其进行调用。 它看起来像一个递归调用,但真正recur正在建设一个值, loop堆栈中的安全的方式处理。

我们提供给loop的函数可以具有任意数量的参数,并且具有默认值。 这也是方便,因为我们现在可以避开昂贵的...解构,并通过简单的使用索引参数传播i初始化为0 该函数的调用者无法在循环调用之外访问这些变量

这里的最后一个优点是, goodsum的读者可以清楚地看到循环编码,并且不再需要显式的done标签。 函数的用户也不必担心调用trampoline ,因为它已经为我们loop

const goodsum = (ns = []) =>
  loop ((sum = 0, i = 0) =>
    i >= ns.length
      ? sum
      : recur (sum + ns[i], i + 1))

console.log (goodsum (range (1000)))
// 500500

console.log (goodsum (range (20000)))
// 200010000

console.log (goodsum (range (999999)))
// 499999500000

这是我们的looprecur对。 这次,我们使用标记模块扩展了{ tag: ... }约定

const recur = (...values) =>
  tag (recur, { values })

const loop = f =>
{ let acc = f ()
  while (is (recur, acc))
    acc = f (...acc.values)
  return acc
}

const T =
  Symbol ()

const tag = (t, x) =>
  Object.assign (x, { [T]: t })

const is = (t, x) =>
  t && x[T] === t

在浏览器中运行以验证结果

 const T = Symbol () const tag = (t, x) => Object.assign (x, { [T]: t }) const is = (t, x) => t && x[T] === t const recur = (...values) => tag (recur, { values }) const loop = f => { let acc = f () while (is (recur, acc)) acc = f (...acc.values) return acc } const range = n => Array.from ( Array (n + 1) , (_, n) => n ) const goodsum = (ns = []) => loop ((sum = 0, i = 0) => i >= ns.length ? sum : recur (sum + ns[i], i + 1)) console.log (goodsum (range (1000))) // 500500 console.log (goodsum (range (20000))) // 200010000 console.log (goodsum (range (999999))) // 499999500000 

额外

我的大脑被卡在变形装置中几个月了,我很好奇是否可以使用上面介绍的loop功能实现堆栈安全unfold

下面,我们看一个示例程序,该程序生成直到n的整个和序列。 可以将其视为显示为达到上述goodsum计划答案的工作。 总计n总和是数组中的最后一个元素。

这是一个很好的unfold案例。 我们可以直接使用loop编写此代码,但是这样做的目的是扩展unfold的极限,因此

const sumseq = (n = 0) =>
  unfold
    ( (loop, done, [ m, sum ]) =>
        m > n
          ? done ()
          : loop (sum, [ m + 1, sum + m ])
    , [ 1, 0 ]
    )

console.log (sumseq (10))
// [ 0,   1,   3,   6,   10,  15,  21,  28,  36, 45 ]
//   +1 ↗ +2 ↗ +3 ↗ +4 ↗ +5 ↗ +6 ↗ +7 ↗ +8 ↗ +9 ↗  ...

如果我们使用不安全的unfold实现,则可能会导致堆栈崩溃

// direct recursion, stack-unsafe!
const unfold = (f, initState) =>
  f ( (x, nextState) => [ x, ...unfold (f, nextState) ]
    , () => []
    , initState
    )

console.log (sumseq (20000))
// RangeError: Maximum call stack size exceeded

在玩了一点之后,确实可以使用我们的堆栈安全loopunfold进行编码。 使用push效果清理... spread语法也使事情变得更快

const push = (xs, x) =>
  (xs .push (x), xs)

const unfold = (f, init) =>
  loop ((acc = [], state = init) =>
    f ( (x, nextState) => recur (push (acc, x), nextState)
      , () => acc
      , state
      ))

用栈安全unfold ,我们sumseq函数现在请客

console.time ('sumseq')
const result = sumseq (20000)
console.timeEnd ('sumseq')

console.log (result)
// sumseq: 23 ms
// [ 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ..., 199990000 ]

在下面的浏览器中验证结果

 const recur = (...values) => tag (recur, { values }) const loop = f => { let acc = f () while (is (recur, acc)) acc = f (...acc.values) return acc } const T = Symbol () const tag = (t, x) => Object.assign (x, { [T]: t }) const is = (t, x) => t && x[T] === t const push = (xs, x) => (xs .push (x), xs) const unfold = (f, init) => loop ((acc = [], state = init) => f ( (x, nextState) => recur (push (acc, x), nextState) , () => acc , state )) const sumseq = (n = 0) => unfold ( (loop, done, [ m, sum ]) => m > n ? done () : loop (sum, [ m + 1, sum + m ]) , [ 1, 0 ] ) console.time ('sumseq') const result = sumseq (20000) console.timeEnd ('sumseq') console.log (result) // sumseq: 23 ms // [ 0, 1, 3, 6, 10, 15, 21, 28, 36, 45, ..., 199990000 ] 

浏览器对函数可以接受的参数数量有实际限制

您可以更改sum签名以接受数组而不是可变数量的参数,并使用解构使语法/可读性保持与所拥有的相似。 这可以“修复” stackoverflow错误,但是速度很慢:D

function _sum([num1, num2, ...nums]) { /* ... */ }

即:如果遇到最大参数数量的问题,那么递归/蹦床方法可能会太慢而无法使用...

另一个答案已经说明了您的代码存在的问题。 这个答案表明蹦床对于大多数基于数组的计算来说足够快,并且提供了更高的抽象水平:

 // trampoline const loop = f => { let acc = f(); while (acc && acc.type === recur) acc = f(...acc.args); return acc; }; const recur = (...args) => ({type: recur, args}); // sum const sum = xs => { const len = xs.length; return loop( (acc = 0, i = 0) => i === len ? acc : recur(acc + xs[i], i + 1)); }; // and run... const xs = Array(1e5) .fill(0) .map((x, i) => i); console.log(sum(xs)); 

如果基于蹦床的计算导致性能问题,那么您仍然可以将其替换为裸循环。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM