调用 isSpaceAscii 时的性能问题

Performance issue when calling isSpaceAscii

我尝试从标准库调用 isSpaceAscii,但性能比我自己的过程差。

重现代码:

import strutils
import std/monotimes
import stats

template timeIt(tag: string, iter: untyped, body: untyped) =
  var st: RunningStat
  for i in countup(1, iter):
    let t0 = getMonoTime().ticks
    body
    let t1 = getMonoTime().ticks
    let d = t1 - t0
    st.push(d.float64)
  echo tag, ": ", st.min

proc isSpace(c: char): bool =
  result = c in Whitespace

when isMainModule:
  # check eqaulity
  for i in 1..255:
    let c = char(i)
    doAssert isSpace(c) == isSpaceAscii(c)

  timeIt "isSpaceAscii", 1000:
    for i in 1..255:
      let c = char(i)
      discard isSpaceAscii(c)

  timeIt "isSpace", 1000:
    for i in 1..255:
      let c = char(i)
      discard isSpace(c)

基准测试结果:

$ nim compile -d:release --verbosity:0 --hints:off --run test.nim
isSpaceAscii: 380.0
isSpace: 20.0

编译器版本:

$ nim -V
Nim Compiler Version 1.4.2 [Linux: amd64]

为什么 isSpaceisSpaceAscii 快?

基准测试很难,因为您并不总是在衡量您认为自己在衡量的东西。

您看到的明显差异是因为 isSpace 循环不执行任何操作,并且与 isSpace 函数位于同一编译单元中,因此编译器可以对其进行优化离开 as you can see on godbolt

如果您改为使用 -d:release -d:lto 编译,编译器将执行 link 时间优化,并将优化两个版本。

$ nim c -d:release -d:lto -r test.nim
isSpaceAscii: 16
isSpace: 16

我们只是在测量循环开销。

要真正比较 isSpaceAscii 和 isSpace,就编译器而言,它们需要做实际工作。

import std/[strutils,monotimes,stats]

template timeIt(tag: string, iters: int, body: untyped) =
  var st: RunningStat
  when declared(warmup): #BUG
    for i in 1..iters:
      body
  for i in 1..iters:
    let t0 = getMonoTime().ticks
    body
    let t1 = getMonoTime().ticks
    st.push((t1-t0).float64)
  echo tag,": ", st.min

proc isSpace(c:char):bool = c in Whitespace

template badloop(procname: untyped) =
  for i in 1..255:
    let c = char(i)
    discard procname(c)

template goodloop(procname: untyped) =
  var x: int
  for i in 1..255:
    let c = char(i)
    if procname(c): inc x

when isMainModule:
  let nruns = 1000
  for i in 1..255:
    doAssert isSpace(i.char) == isSpaceAscii(i.char)

  timeit "isSpaceAscii, good",nruns:
    goodloop(isSpaceAscii)

  timeit "isSpace, good",nruns:
    goodloop(isSpace)

  timeit "isSpaceAscii, bad",nruns:
    badloop(isSpaceAscii)

  timeit "isSpace, bad",nruns:
    badloop(isSpace)

结果:

$ nim -d:release -d:lto -r test.nim
isSpaceAscii, good: 439.0
isSpace, good: 382.0
isSpaceAscii, bad: 17.0
isSpace, bad: 17.0

接近了,但好像还是有出入,这是怎么回事? T̶h̶e̶ ̶c̶p̶u̶ ̶h̶a̶s̶ ̶w̶a̶r̶m̶e̶d̶ ̶u̶p̶ ̶b̶y̶ ̶t̶h̶e̶ ̶t̶i̶m̶e̶ ̶i̶t̶ ̶g̶e̶t̶s̶ ̶t̶o̶ ̶t̶h̶e̶ ̶s̶e̶c̶o̶n̶d̶ ̶t̶e̶s̶t̶,̶ ̶a̶n̶d̶ ̶i̶t̶ ̶g̶o̶e̶s̶ ̶f̶a̶s̶t̶e̶r̶.̶ (Edit due to a bug -d:warmup didn't change the code,差异是由于编译器做出了不同的优化选择。)

再试一次,t̶u̶r̶n̶i̶n̶g̶̶o̶n̶̶t̶h̶e̶̶w̶a̶r̶m̶u̶p̶̶l̶o̶o̶p̶我们已经添加到timeit,这次全力以赴使用-d:danger

$ nim c -d:danger -d:lto -d:warmup -r test.nim
isSpaceAscii, good: 237.0
isSpace, good: 234.0
isSpaceAscii, bad: 15.0
isSpace, bad: 15.0

差不多。

编辑 好像是为了突出微基准测试的不可测性,我对热身部分的看法完全错了。我应该写 when defined(warmup),由于那个错误,我的“预热”代码实际上从未 运行。事实上,由于我们用了 最快的 时间,所以前几 运行 秒已经足够预热了。

从那时起,我已经 运行 了多个版本的代码,结果差异太大,无法得出更多结论,除了可能:

  • 编译器的优化选择变化无常
  • 基准测试很难