我有两种方法可以编程相同的功能。
方法1:
doTheWork(int action)
{
for(int i = 0 i < 1000000000; ++i)
{
doAction(action);
}
}
方法2:
doTheWork(int action)
{
switch(action)
{
case 1:
for(int i = 0 i < 1000000000; ++i)
{
doAction<1>();
}
break;
case 2:
for(int i = 0 i < 1000000000; ++i)
{
doAction<2>();
}
break;
//-----------------------------------------------
//... (there are 1000000 cases here)
//-----------------------------------------------
case 1000000:
for(int i = 0 i < 1000000000; ++i)
{
doAction<1000000>();
}
break;
}
}
假设函数doAction(int action)
和函数template<int Action> doAction()
由大约10行代码组成,这些代码将在编译时进行内联。调用doAction(#)
在功能上与doAction<#>()
等效,但非模板doAction(int value)
比template<int Value> doAction()
慢一些,因为在参数中可以在代码中完成一些很好的优化值在编译时已知。
所以我的问题是,在模板化函数的情况下,所有数百万行代码都会填充CPU L1缓存(以及更多)(因此会大大降低性能),或者只执行{{1}行当前正在运行的循环内部是否已缓存?
答案 0 :(得分:2)
这取决于实际代码大小 - 10行代码可能很少或很多 - 当然在实际的机器上。
然而,方法2猛烈地违反了这几十年的经验法则:说明便宜,内存访问不是。
可扩展性限制
您的优化通常是线性的 - 您可能会减少10%,20%甚至30%的执行时间。达到缓存限制是高度非线性的 - 就像在“碰壁砖”非线性一样。
一旦您的代码大小明显超过第二/第三级缓存的大小,方法2将失去大量时间,因为以下对高端消费者系统的估计显示:
10667MB/s
峰值内存带宽的DDR3-1333,75000 MIPS
为每个指令提供10667MB / 75000M = 0.14字节的中断 - 更大,主内存无法跟上CPU。
典型的x86指令大小是2..3个字节,在1..2个周期内执行(现在,授予,这不一定是相同的指令,因为x86指令被拆分。仍然......) 典型的x64指令长度更大。
您的缓存有多大帮助?
我发现了以下数字(不同的来源,所以很难比较):
i7 Nehalem L2缓存(256K,> 200GB / s带宽)几乎可以跟上x86指令,但可能不适用于x64。
此外,仅当
时,您的L2缓存才会完全启动鉴于此,您可能会更早失去,特别是在具有较小缓存的CPU /板上。
答案 1 :(得分:1)
L1指令高速缓存仅包含最近获取的指令或预计将来执行的指令。因此,第二种方法不能仅仅因为代码存在而填充L1高速缓存。您的执行路径将导致它加载表示正在运行的当前循环的模板实例化版本。当您转到下一个循环时,它通常会使最近最少使用的(LRU)缓存行无效,并将其替换为您接下来要执行的操作。
换句话说,由于两种方法的循环特性,L1缓存在两种情况下都会表现出色,并且不会成为瓶颈。