Andrei Alexandrescu撰写的“编写快速编码I”约39分钟(link here to youtube)
有一个如何使用差分计时的幻灯片......有人能用这种方法向我展示一些基本代码吗?它只提到了一秒钟,但我认为这是一个有趣的想法。
答案 0 :(得分:1)
Alexsandrescu的slide对于代码来说很简单:
auto start = clock::now();
for( int i = 0; i < 2*n; i++ )
baseline();
auto t2a = clock::now() - start;
start = clock::now();
for( int i = 0; i < n; i++ )
baseline();
// *
for( int i = 0; i < n; i++ )
contender();
auto taplusb = clock::now() - start;
double r = t2a / (2 * taplusb - t2a) // relative speedup
*同步点,可防止在最后两个循环中进行优化。
相对于我一直以来所做的tBaseline / tContender
而言,我对这种以相对方式测量相对速度背后的数学推理更感兴趣。他只是隐约地暗示着“ ...高架噪音(正在)被消除(出去)”,但没有详细解释。
答案 1 :(得分:0)
如果您一直观看到41:40左右,他会在警告首次运行与后续运行(分配器预热等)的陷阱时再次提及它
最好的解决方案是在第一个定时区域之前进行预热。
我认为他是在基准测试程序的单独调用中描述了2n
基准与n
基准+ n
的竞争者。
因此,与其在定时区域之前 进行一些预热,他将基线用作定时区域内的受控预热。这可能使得仅对整个程序计时,例如perf stat
,而不是在程序内部调用时间函数。取决于您的操作系统有多少启动过程的开销以及重复循环的时间。
微基准测试很困难,而且有很多陷阱。值得一提的是,在对优化代码进行基准测试的同时,还要确保重复循环的迭代之间没有优化。 (通常使用内联asm“转义”宏来强制编译器在整数寄存器中具体化一个值,和/或忽略变量的值以打败CSE是很有用的。有时只需添加最终结果即可)每次迭代到最后打印的总和。
这是我第一次听说这种不同的想法。听起来没有比普通的热身赛有用。
如果有任何事情,它将使竞争者看起来比使用被测函数在定时区域之前进行一些预热运行时稍差一些。使用与定时区域相同的功能将为其预热分支预测。还是不行,因为在内联后的预热和主版本将位于不同的地址。在不同地址使用相同模式可能仍然可以帮助现代TAGE预测器,但IDK。
或者如果contender
有任何查找表,则这些表将从预热起在缓存中变热。
在任何情况下,预热都是必不可少的,除非您使重复计数足够长,以使CPU切换到最大加速等所需的时间相形见and。并在您触摸的所有内存中进行分页操作。
如果您计算出的时间/迭代次数与重复次数不一致,那么您的微基准测试就会失效。
也将他的其他建议与一粒盐一起接受。大部分使用有用的方法(例如,即使对于本地临时对象,也更喜欢32位整数,而不仅仅是出于缓存占用的原因而用于数组),但是其中某些原因是错误的。
他关于ALU可以进行2x 32位加法或1x 64位加法的解释仅适用于SIMD:paddd
的向量中为4x 32位int或为paddq
的向量中为2x 64位int add r32, r32
。但是x86标量add r64,r64
具有与add
相同的吞吐量。我认为即使在Pentium 4(Nocona)上也不是真的,尽管P4具有int8_t
具有0.5个周期延迟的时髦的双泵ALU。至少在Prescott / Nocona引入了64位支持之前。
如果需要,在x86-64上使用32位无符号整数可以阻止编译器优化指针增量。如果要在数组索引之前对变量进行32位环绕,则必须保持正确性。
使用16位或8位局部变量匹配数组中的数据有时可以帮助自动矢量化IIRC。在处理uint8_t
或fistp
数组时,Gcc / clang有时会产生真正的脑残代码,将其解压缩为32位,然后重新压缩为8位元素。不过,我忘了我是否每个人都通过使用狭窄的本地人来解决这个问题。 C默认的整数提升使大多数表达式恢复到32位。
他还在https://youtu.be/vrfYLlR8X8k?t=3498声称FP-> int很昂贵。在x86-64上从未如此:FP数学使用SSE / SSE2,该指令的指令会截断转换。在x87数学的糟糕年代,FP-> int 使用的速度很慢,在这种情况下,您必须更改FP舍入模式cvttsd2si
,然后再更改回去,以获得C截断语义。但是SSE恰好包含该常见情况的float
。
他还说double
并不比n
快。标量(div / sqrt除外)是正确的,但是如果您的代码可以自动向量化,那么每条指令将完成两倍的工作,并且指令具有相同的吞吐量。 (SIMD向量中容纳的元素数量是原来的两倍。)
它只是从两个部分中取消了(2 * baseline) / (2*contender)
*基线时间,有效地做了t_2a
=基线/竞争者。
假定时间相加正常(不重叠计算)。 2 * t_ab
= 2 *基准,而n*baseline
= 2 *基准+ 2 *竞争者。减去将取消2 *基线部分,让您拥有2 *竞争者。
诀窍不在数学中,如果在数学上更危险,因为减去两个大数会累积误差。也就是说,如果{{1}}在两次运行中实际上花费了不同的时间(因为您没有完美地控制它),那么它就不会取消,并且会给估计带来误差。