【问题标题】:optimizing calling an array of function pointers优化调用函数指针数组
【发布时间】:2019-11-16 08:49:48
【问题描述】:

我有以下循环调用数组中的所有函数指针:

for(auto f : program) {
   f();
}

我想对此进行优化。到目前为止,我已经尝试了两种方法:

  1. 尾递归
  2. JITting 线程代码

这里是完整的测试代码:https://coliru.stacked-crooked.com/a/d639f024b1222c54

我的机器(iMac Pro 8-Core)上的计时结果是:

naive: 0.530534
tail recursion: 0.265192
JIT threaded: 0.125106

当然,所有函数都必须修改以方便尾递归,但这没关系。在代码清洁度方面不太令人愉快的是将所有内容放在一个函数中并使用计算 goto 之类的东西(实际上,我也尝试过,计算 goto 只比我机器上的尾递归快一点。)

我可以在没有 JITting 的情况下比尾递归做得更好吗?(在 iOS 上,不允许 JITting)

请注意,函数不能重新排序。

【问题讨论】:

  • 您是否保证所有测试的内存访问模式相同?我想所有的缓存获取和分页都发生在第一次调用这些函数时,这会极大地影响结果。如果您最后测试天真,您的计时结果会发生很大变化吗?或者可能从您的计时结果中排除第一轮执行(因为第一轮将包括分页未命中和 CPU 缓存未命中。)
  • @Wyck 好点。在我的机器上,我使用 XCTest,每个测试运行 10 次。第一次运行稍慢,但不是特别重要。我怀疑这是因为每个测试都进行了 100m 的函数调用。

标签: c++ c optimization jit


【解决方案1】:

是的。我们实际上可以在没有 JITting 的情况下击败线程代码​​。

测试代码包含 100 个可能的函数。我编写了一个小程序来为 100x100 的函数数组生成代码,这些函数调用这 100 个函数的对。优化器将原始的 100 内联到对中。我们现在有:

naive: 0.534162
tail recursion: 0.269307
JIT threaded: 0.124608
pairs: 0.085922

通过分析常见的函数调用序列,而不是生成所有可能的对,可以将该技术推广到实际情况。

这可以与尾递归相结合,以实现更快的调度。

【讨论】:

  • 这令人印象深刻,即使最终毫无用处。还使用了我得到的这个页面的时间:naive: 1.040995 tail recursion: 0.698340 tail recursion: 0.130868 JIT threaded: 0.870852。第一个尾递归是你的,比 JIT 更快(你也测量 jitting 本身吗?)。第二个尾递归只是按顺序调用函数。它的速度要快得多,它比你的 pair 方法快 2-3,这对我来说,比在内存中排序函数的代码可能是最好的主意。所以让函数可重定位,通过调用 order 对它们进行排序,然后就可以了。
  • @RadosławCybulski 计时来自我的机器。正如测试代码所暗示的,函数调用顺序在编译时是未知的,这意味着它们无法在内存中排序。此外,函数可能会被多次调用,因此不存在这样的排序顺序。
  • 当然可以对代码进行排序。如果函数代码是可重定位的,那么您可以按照与创建跳转调用相同的原则将其简单地移动到内存中(本质上不是编写跳转调用,而是编写整个函数体)。您甚至可以多次编写函数体(复制它们),因为它们必须非常短(否则它们的运行时间将超过任何调用时间损失)。
  • @RadosławCybulski 当然,好的,更复杂的抖动。那时还不如只使用 LLVM。这里的要点是在无需 JIT 的情况下实现更快的调度。我会尽量在问题中更清楚地说明这一点。
  • @RadosławCybulski 复制时如何确定函数的结尾?结语之后可能会有数据,对吧?
猜你喜欢
  • 2012-03-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-11-04
  • 1970-01-01
  • 2014-02-16
相关资源
最近更新 更多