看了昨天 @ManjusakaH 转发的 simd 科普之后我才知道原来字符串搜索也可以 simd,我当时就萌生了一个大胆的想法,何不买一根 335mm Φ8mm 的金属棒 何不让 bpf 也用 simd 搜索 http header,目前的实践都是狗屎,我和同事在 6.1 ec2 上编译 kfunc 的惨痛经历仿佛还在昨天😭

任务是从巨型 HTTP/1.1 payload 里搜索你喜欢的 header,比如 Host:
GET /url HTTP/1.1
User-Agent: xxx
X-Forward: xxx
X-Metadata: xxx
[...1000+ bytes...]
Host: xxx


虽然 bpf simd 是做不了的,但可以 SWAR 啊,我用 go 写一遍是这样的
const (
  repeatLF = uint64(0x0a0a0a0a0a0a0a0a)
  ones     = uint64(0x0101010101010101)
  highs    = uint64(0x8080808080808080)
)

func findHostHeader(buf []byte) int {
  cursor := 0
  lineStart := false

  for cursor < len(buf) {
    if lineStart && cursor+5 <= len(buf) &&
      bytes.EqualFold(buf[cursor:cursor+5], []byte("Host:")) {
      return cursor
    }
    lineStart = false

    if cursor%8 == 0 && cursor+8 <= len(buf) {
      word := binary.LittleEndian.Uint64(buf[cursor:])
      x := word ^ repeatLF
      matches := (x - ones) &^ x & highs
      if matches != 0 {
        cursor += bits.TrailingZeros64(matches)/8 + 1
        lineStart = true
      } else {
        cursor += 8
      }
      continue
    }

    // Scan only to the next alignment boundary or the end.
    scanEnd := min((cursor+7)&^7, len(buf))
    for cursor < scanEnd {
      if buf[cursor] == '\n' {
        cursor++
        lineStart = true
        break
      }
      cursor++
    }
  }

  return -1
}

虽然看起来一大坨,但实际理解并不复杂,for 循环里每次 cast 8 字节成 u64 用位运算检查这里是否有 \n,有的话说明是行末,buf[idx:idx+5] 可以用来判定 "Host:";否则不是行末,直接推进 8 字节。

这个 swar 做法虽然打不过 simd,但是两倍性能于逐字节比较还是轻轻松松的,更美妙的事它可以用 bpf 实现出来, microbench 的结果是在所有情况下都是逐字节比较的 3~4 倍,甚至比 kfunc bpf_strnstr 的性能也快 2x 了,因为 kfunc 也是逐字节的,只是少了一些 bpf_loop callback 开销而已
Scenario     Bytes   Byte ns/op   SWAR ns/op   Kfunc ns/op SWAR speedup Kfunc speedup
first           64         76.0         24.0          61.0        3.17x        1.25x
middle         512        522.0        144.0         267.0        3.62x        1.96x
late          1536       2238.0        554.0        1018.0        4.04x        2.20x
absent        2048       3745.0        923.0        1718.0        4.06x        2.18x

http payload parsing 大概占用了 50% 的观测消耗,这部分性能优化 4 倍,根据 Amdahl's law,整体性能提升是 1/((1-0.5) + 0.5/4)) = 1.6 倍,如果之前是观测造成是 latency 是 +7%,这个优化可以把 latency 降到 +7%/1.6 = +4.4%,这也是符合观测结果的,嘻嘻,无敌。
 
 
Back to Top