Javascript 性能优化 - Duff 装置 (Duff's device)

705 阅读3分钟

在 JavaScript 程序中,作为性能优化的一部分,循环优化是很重要的一部分。因为循环在编程中随处可见,是常见的结构。而循环展开就是对循环进行优化的一种可考虑的方式。

Duff 装置(Duff's device)是实现循环展开(loop unrolling)的一种方法。

我们知道,每次循环之前或之后都需要检查是否满足条件,来决定是否继续执行循环体。使用循环处理大数据集时,这种检查会有较明显的性能开销。循环展开通过每次迭代执行一批循环体,来减少迭代次数,继而减少检查次数同时减少阻塞,从而达到减少条件检查带来的性能开销的目的。

要处理迭代次数不能被展开的循环增量整除的情况,在汇编语言中有一种常见技术是直接跳到展开的循环体的中间来处理其余部分。Tom Duff 在 C 语言中利用 do-while 循环和 switch 语句实现了这种技术。后来 Jeff Greenberg 用 JavaScript 实现了 Duff 装置。

// credit: Jeff Greenberg for JS implementation of Duff's Device
let iterations = Math.ceil(items.length / 8)
let startAt = item.length % 8
let i = 0do {
  switch (startAt) {
    case 0: process(items[i++])
    case 7: process(items[i++])
    case 6: process(items[i++])
    case 5: process(items[i++])
    case 4: process(items[i++])
    case 3: process(items[i++])
    case 2: process(items[i++])
    case 1: process(items[i++])
  }
  startAt = 0
} while (--iterations)

Duff 装置背后的基本思想是,每次循环最多允许8次调用process()。循环的迭代次数是由数据集总数除以 8 来确定。因为不是所有的数字都能被 8 整除,所以用 startAt 变量保存余数,并指示在第一次循环中会调用 process() 多少次。如果有数据集有 12 条,那么第一次循环运行将调用process() 4 次,然后第二次(以及之后的每次)循环运行将调用process() 8 次,总共两次循环运行,而不是 12 次。

Duff 装置算法还有一个更快的版本,它移除了 switch 语句。

// credit: Speed up Your Site (New Riders, 2003)
let iterations = items.length % 8
let i = items.length - 1while (iterations--) {
  process(items[i--])
}
​
iterations = Math.floor(items.length / 8)
do {
  process(items[i--])
  process(items[i--])
  process(items[i--])
  process(items[i--])
  process(items[i--])
  process(items[i--])
  process(items[i--])
  process(items[i--])
} while (--iterations)

在这个实现中,剩余的计算部分提取出来在一个单独的初始化循环中处理。当处理掉了额外的部分,继续执行每次调用 8 次 process() 的主循环。这个方法几乎比原始的 Duff 装置实际快上 40%。

针对大数据集使用循环展开可以节省很多时间,但对于小数据集,额外的开销可能得不偿失。一般如果迭代的次数不超过 1000,使用 Duff 装置对性能提升并不明显。当迭代次数超过 1000 时,迭代次数越多,性能提升越明显。例如,在500000 次的迭代中,Duff 装置的执行时间比常规循环减少了 70%。

如果有必要,我们也可以将 Duff 装置封装为函数。

function duffDevice(items, process) {
  let iterations = items.length % 8
  let i = 0
  while (iterations--) {
    process(items[i++])
  }

  iterations = Math.floor(items.length / 8)
  do {
    process(items[i++], i, items)
    process(items[i++], i, items)
    process(items[i++], i, items)
    process(items[i++], i, items)
    process(items[i++], i, items)
    process(items[i++], i, items)
    process(items[i++], i, items)
    process(items[i++], i, items)
  } while (--iterations)
}

const nums = Array.from({ length: 1000 }).map((_, i) => i)

duffDevice(nums, function (item, index, arr) {
  console.log(item)
})

参考资料: