使用profile优化你的javaScript性能

2024-12-22 15:11:13/17天之前

序言

我为什么要写这篇文章? 这可能是因为前段时间看到了一篇文章关于对使用rust,go…语言重构前端基建的态度,作者从几个纬度以及该如何做表达了他的看法,对此我认为是十分有意义的。无论是 riir还是什么其他的,javaScript对于熟悉NodeJs或者front-end的开发人员都是极其友好的,不需要面对未知的segfault

使用Profile评估你的代码

在这我从我的实际项目作为出发点记录下代码优化的差异具体的改动在tar-mini。这篇blog会很长我可能有时间会写下篇关于流的一些优化。 这篇我只会详细地记录encode的优化。

我们可以简单的编写个测试代码。

// encode-test.js
import { F_MODE, TypeFlag, encode } from 'tar-mini'

const filename = 'nonzzz.txt'
const content = new Uint8Array([49, 49, 49, 49])

for (let i = 0; i < 1000; i++) {
  encode({
    name: filename,
    size: content.length,
    uid: 0,
    gid: 0,
    mtime: Math.floor(Date.now() / 1000),
    typeflag: TypeFlag.AREG_TYPE,
    linkname: '',
    devmajor: 0,
    devminor: 0,
    mode: F_MODE,
    uname: 'nonzzz',
    gname: 'admin'
  })
}
node --prof encode-test.js

# 这里会生成一个日志然后我们调用 

node --prof-process 你生成的日志 > encode.txt

然后查看生成的encode.txt 我们能发现在[JavaScript] 这一段我们有大量的TextEncode的开销。 那么这显然是不太对的。因为一开始我们的代码定义如下

const enc = new TextEncode()

const encodeString = enc.encode.bind(enc)

function encode() {
  encodeString()
  // ... multiple call encodeString
}

既然这里的开销和自己的认知不符那么我们就可以借助搜索引擎去检索相关信息不出意外我们得到了关于对TextEncode的描述。issue 既然每次encodeString都会重复创建一块新的Uint8Array那么我们能不能只分配一次呢?答案是肯定的我们既然是编写一个tar header encode那我们可以根据最长的字段来提前分配一段内存。 因此我们只需要分配256 bytename(100) + prefix(155).

const enc = new TextEncode()

function createUT8Encode() {
  const alloc = new Uint8Array(256)
  return function encodeString() {
    const { written } = enc.encodeInto(s, alloc)
    return alloc.subarray(0, written)
  }
}

这样我们就能避免重复创建大量的uint8Array提升性能。然后我们再看看还有什么地方可以优化。我们注意到了chksum函数的开销是有异常的。

function chksum(b: Uint8Array) {
  return b.subarray(0, 512).reduce((acc, cur, i) => {
    if (i >= 148 && i < 156) {
      return acc + Magic.WHITE_SPACE // WHITE_SPACE mean ASCII 32
    }
    return acc + cur
  }, 0)
}

我们在这个函数做了无意义的判断开销。同时根据定义我们不需要这个判断的开销只需要

function chksum(b: Uint8Array) {
  // 148 ~ 156
  let sum = Magic.WHITE_SPACE * 8 // 根据定义148 ~ 156 中间是有 white_space 填充的
  for (let i = 0; i < 148; i++) {
    sum += b[i]
  }
  for (let i = 156; i < 512; i++) {
    sum += b[i]
  }
  return sum
}

在此观察发现剩余的开销是发生在encodeOctal上 根据日志我们发现stringRepeat的速度并不理想在跑过Js perf得知对比

const s = '7777777'

const fastStr = s.slice(0, 3) // fast

const slowStr = '7'.repeat(3) // slow

// fastStr的跑分是slowStr的两倍。

接着我们重构我们的encodeOctal

// past
function encodeOctal(b: number, fixed?: number) {
  const o = b.toString(8)
  if (fixed) {
    if (o.length <= fixed) {
      const fill = '0'.repeat(fixed - o.length)
      return fill + o + ' '
    }
    return '7'.repeat(fixed) + ' '
  }
  return o
}

// after

function encodeOctal(b: number, fixed: number) {
  const o = b.toString(8)
  if (o.length > fixed) { return Magic.SEVENS.slice(0, fixed) + ' ' }
  return Magic.ZEROS.slice(0, fixed - o.length) + o + ' '
}

然后分别对比优化前后的性能差距

bench

基于这个图表做了上述的优化我们提升了4倍多的性能。而这些性能的获取相比riir他是廉价的。我们只需要使用profbench就能轻易的得到一些提升。

CC BY-NC-SA 4.02024-PRESENT © Kanno