我的想法是给出一个优雅的代码示例,它将演示指令缓存限制的影响。我编写了以下代码,使用模板元编程创建了大量相同的函数。
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),运行时间仍随着功能量的增加而增加。这可能与分支预测效率有关,但由于在这种情况下每个函数调用都减少为无条件跳转,我不确定是否应该存在任何分支惩罚。
蓝色曲线表现完全不同。对于小范围,运行时间是恒定的,并且之后增加对数。然而对于更大的范围,曲线似乎再次接近恒定的渐近线。两种曲线的定性差异究竟如何解释?
我目前正在使用带有g++ -std=c++11 -ftemplate-depth=65536
的GCC MinGW Win32 x86 v.4.8.1,并且没有编译器优化。
任何帮助将不胜感激。我也对如何改进实验本身感兴趣。提前谢谢!
答案 0 :(得分:1)
首先,让我说我真的很喜欢你如何解决这个问题,这对于故意代码膨胀是一个非常巧妙的解决方案。但是,您的测试可能仍有几个问题 -
您还可以测量预热时间。你没有显示你在哪里放置时间检查,但如果它只是在内部循环周围 - 那么第一次直到达到范围/ 2你仍然会享受前一次外部迭代的预热。相反,只测量热性能 - 多次运行每次内部迭代(在中间添加另一个循环),并且仅在1-2轮之后获取时间戳。
您声称已经测量了多个缓存级别,但您的L1缓存只有32k,这是图表结束的位置。即使假设它按“范围”计算,每个函数大约是21个字节(至少在我的gcc 4.8.1上),所以你最多只能达到256KB,这只会刮掉你的L2的大小。
您没有指定您的CPU型号(i7现在市场上至少有4代,Haswell,IvyBridge,SandyBridge和Nehalem)。差异非常大,例如自Sandybrige以来具有复杂存储规则和条件的额外uop-cache。你的基线也很复杂,如果我没记错,P4有一个跟踪缓存,这也可能导致各种性能影响。如果可能,您应该检查一个选项以禁用它们。
不要忘记TLB - 即使它在这样一个严密组织的代码中可能不起作用,唯一的4k页面的数量不应超过ITLB(128个条目),甚至之前如果您的操作系统没有充分展开物理代码页以避免ITLB冲突,您可能会开始发生冲突。