6

我最初的想法是给出一个优雅的代码示例,以展示指令缓存限制的影响。我使用模板元编程编写了以下代码,它创建了大量相同的函数。

volatile int checksum;
void (*funcs[MAX_FUNCS])(void);

template <unsigned t> 
__attribute__ ((noinline)) static void work(void) { ++checksum; }

template <unsigned t> 
static void create(void) { funcs[t - 1] = &work<t - 1>; create<t - 1>(); }

template <> void create<0>(void) {  }

int main()
{
    create<MAX_FUNCS>();

    for (unsigned range = 1; range <= MAX_FUNCS; range *= 2)
    {
        checksum = 0;
        for (unsigned i = 0; i < WORKLOAD; ++i)
        {
            funcs[i % range]();
        }
    }

    return 0;
}

外部循环使用跳转表来改变要调用的不同函数的数量。WORKLOAD对于每个循环通过,然后测量调用函数所花费的时间。现在结果如何?下图显示了与使用范围相关的每个函数调用的平均运行时间。蓝线显示在 Core i7 机器上测量的数据。红线所示的比较测量是在 Pentium 4 机器上进行的。然而,当谈到解释这些台词时,我似乎有点挣扎......

图表

分段常数红色曲线的唯一跳跃恰好发生在范围内所有函数的总内存消耗超过测试机器上一个缓存级别的容量的地方,该机器没有专用的指令缓存。然而,对于非常小的范围(在这种情况下低于 4),运行时间仍然会随着函数的数量而增加。这可能与分支预测效率有关,但由于在这种情况下每个函数调用都会减少为无条件跳转,所以我不确定是否应该有任何分支惩罚。

蓝色曲线的行为完全不同。运行时间对于小范围是恒定的,然后以对数方式增加。然而,对于更大的范围,曲线似乎再次接近恒定的渐近线。如何准确地解释两条曲线的质量差异?

我目前正在使用 GCC MinGW Win32 x86 v.4.8.1g++ -std=c++11 -ftemplate-depth=65536没有编译器优化。

任何帮助,将不胜感激。我也对如何改进实验本身的任何想法感兴趣。提前致谢!

4

1 回答 1

1

首先,让我说我真的很喜欢你解决这个问题的方式,这是一个非常巧妙的解决故意代码膨胀的解决方案。但是,您的测试可能仍然存在几个可能的问题 -

  1. 您还可以测量预热时间。您没有显示时间检查的位置,但如果它只是在内部循环周围 - 那么第一次到达 range/2 之前,您仍然会享受上一次外部迭代的热身。相反,只测量温暖的性能 - 多次运行每个内部迭代(在中间添加另一个循环),并且仅在 1-2 轮后获取时间戳。

  2. 您声称已经测量了几个缓存级别,但您的 L1 缓存只有 32k,这是您的图表结束的地方。即使假设这以“范围”计,每个函数大约是 21 个字节(至少在我的 gcc 4.8.1 上),所以你最多会达到 256KB,这只是你的 L2 的大小。

  3. 您没有指定您的 CPU 型号(i7 现在市场上至少有 4 代,Haswell、IvyBridge、SandyBridge 和 Nehalem)。差异非常大,例如自 Sandybrige 以来额外的 uop-cache 具有复杂的存储规则和条件。您的基线也使事情变得复杂,如果我没记错的话,P4 有一个跟踪缓存,这也可能导致各种性能影响。如果可能,您应该检查一个选项以禁用它们。

  4. 不要忘记 TLB——尽管它可能在如此紧密组织的代码中没有发挥作用,但唯一 4k 页面的数量不应超过 ITLB(128 个条目),甚至在此之前你可能会开始发生冲突如果您的操作系统没有很好地传播物理代码页以避免 ITLB 冲突。

于 2013-10-06T12:29:10.580 回答