在编写模拟时,我的伙伴说他喜欢尝试编写足够小的程序以适应缓存。这有什么实际意义吗?据我所知,缓存比RAM和主内存快。是否可以指定您希望程序从缓存运行或至少将变量加载到缓存中?我们正在编写模拟,因此任何性能/优化都会带来巨大的好处。
如果您知道任何解释CPU缓存的好链接,请指出我的方向。
答案 0 :(得分:30)
答案 1 :(得分:11)
以下是Christer Ericsson(战神I / II / III成名)对缓存/内存优化的真正好paper的链接。这已经有几年了,但它仍然非常重要。
答案 2 :(得分:7)
乌尔里希·德雷普(Ulrich Drepper)What Every Programmer Should Know About Memory是一篇有用的论文,它将告诉你关于缓存的更多信息。 Hennessey非常彻底地涵盖了它。 Christer和Mike Acton也写了很多关于这个的好东西。
我认为你应该比指令缓存更担心数据缓存 - 根据我的经验,dcache misses更频繁,更痛苦,更有用。
答案 3 :(得分:5)
更新:2014年1月13日 根据这位资深芯片设计师的说法,缓存未命中现在是代码性能的绝对主导因素,因此我们基本上可以追溯到80年代中期和快速286芯片的负载,存储,整数的相对性能瓶颈算术和缓存未命中。
A Crash Course In Modern Hardware by Cliff Click @ Azul 。 。 。 。
---我们现在回到您定期安排的计划---
有时,一个例子比描述如何做某事要好。本着这种精神,这是一个特别成功的例子,说明我如何更改一些代码以更好地利用芯片缓存。这是在不久前在486 CPU上完成的,后者迁移到第一代Pentium CPU。对性能的影响是相似的。
示例:下标映射
以下是我用于将数据拟合到具有通用实用程序的芯片缓存中的技术示例。
我有一个双重浮动载体,长度为1,250个元素,这是一个具有很长尾巴的流行病学曲线。曲线的“有趣”部分只有大约200个唯一值,但我不希望双侧if()测试弄乱CPU的管道(因此长尾,可以用作最极端的下标)值蒙特卡罗代码会吐出来的,我需要在代码中“热点”内的十几个其他条件测试的分支预测逻辑。
我确定了一个方案,我使用8位整数的向量作为双向量的下标,我缩短为256个元素。微小的整数在零之前的128之前都具有相同的值,在零之后的128之前具有相同的值,因此除了中间的256个值之外,它们都指向双向量中的第一个或最后一个值。
对于双打,存储要求缩小为2k,对于8位下标则为1,250字节。这缩小了10,000字节,降至3,298。由于程序在这个内循环中花费了90%或更多的时间,因此2个向量永远不会被推出8k数据高速缓存。该计划立即使其表现翻了一番。在为100多万抵押贷款计算OAS值的过程中,该代码达到了1000亿次。
由于曲线的尾部很少被触及,很可能只有微小的int矢量的中间200-300个元素实际上保存在缓存中,160-240个中间双精度代表1/8的感兴趣的百分比。在一个下午完成的程序中,我花了一年多时间进行优化,这是一个显着的性能提升。
我同意Jerry,因为我的经验也是如此,将代码倾向指令缓存并不像优化数据缓存那么成功。这是我认为AMD的常见缓存不如英特尔独立的数据和指令缓存有用的原因之一。 IE:你不希望指令占用缓存,因为它不是很有帮助。在某种程度上,这是因为CISC指令集最初是为了弥补CPU和内存速度之间的巨大差异而创建的,除了80年代后期的异常外,这几乎总是如此。
我用来支持数据缓存并扼杀指令缓存的另一种最受欢迎的技术是在结构定义中使用大量的bit-ints,并且通常使用尽可能小的数据大小。要屏蔽4位int以保存一年中的月份,或者用9位来保存一年中的某一天等,需要CPU使用掩码来屏蔽位正在使用的主机整数,这会缩小数据,有效地增加了缓存和总线大小,但需要更多指令。虽然这种技术产生的代码在综合基准测试中表现不佳,但在用户和进程竞争资源的繁忙系统上,它的工作效果非常好。
答案 4 :(得分:4)
大多数情况下,这将作为占位符,直到我有时间做这个主题正义,但我想分享我认为是一个真正具有开创性的里程碑 - 在新的英特尔Hazwell微处理器中引入专用位操作指令。
当我在StackOverflow上编写一些代码来反转4096位阵列中的位时,显而易见的是,在引入PC之后30多年,微处理器只是没有太多关注或资源到位,而且我希望会改变。特别是,我很想看到,对于初学者来说,bool类型在C / C ++中变成了一个实际的位数据类型,而不是它当前的荒谬浪费字节。
更新日期:2013年12月29日
我最近有机会优化环形缓冲区,以毫秒级粒度跟踪系统上512个不同资源用户的需求。有一个定时器,每毫秒触发一次,它加上最新切片资源请求的总和,并减去第1,000个时间片的请求,包括现在1000毫秒的资源请求。
头部,尾部向量在内存中彼此相邻,除非首先是头部,然后尾部包裹并在阵列的开始处重新开始。然而,(滚动)摘要切片位于一个固定的,静态分配的数组中,该数组并不特别接近其中任何一个,甚至没有从堆中分配。
考虑到这一点,并研究代码,一些细节引起了我的注意。
进入的需求同时被添加到Head和Summary切片中,在相邻的代码行中彼此相邻。
当计时器触发时,Tail被从摘要切片中减去,结果保留在摘要切片中,正如您所期望的那样
当计时器触发时调用的第二个函数使服务环的所有指针都提前。特别是.... Head覆盖了Tail,从而占据了相同的内存位置 新Tail占用了接下来的512个内存位置,或者包裹了
用户希望管理的需求数量更灵活,从512到4098,或者更多。我觉得这样做最强大,最笨拙的方法是将1000个时间片和摘要片一起分配为一个连续的内存块,这样,摘要片最终会变成不同的长度是不可能的。比其他1000个时间片。
鉴于上述情况,我开始怀疑,如果不是将摘要切片保留在一个位置,而是让它在头部和尾部之间“漫游”,我是否能获得更多性能,所以它总是如此在Head附近添加新的需求,并在计时器触发时紧挨着Tail,并且必须从摘要中减去Tail的值。
我做到了这一点,但后来在这个过程中发现了一些额外的优化。我更改了计算滚动摘要的代码,以便将结果留在Tail中,而不是Summary切片。为什么?因为下一个函数正在执行memcpy()以将Summary切片移动到刚刚被Tail占用的内存中。 (奇怪但是真的,Tail引领头部直到环绕它结束时)。通过在Tail中保留求和的结果,我不必执行memcpy(),我只需将pTail分配给pSummary。
以类似的方式,新的Head占用了现在陈旧的Summary slice的旧内存位置,所以我再次将pSummary分配给pHead,并将memset的所有值归零。
引导到环的末尾(实际上是鼓,512轨道宽)是Tail,但我只需将其指针与常量pEndOfRing指针进行比较以检测该条件。可以在其前面为所有其他指针分配向量的指针值。 IE:我只需要1:3指针的条件测试来正确包装它们。
初始设计使用字节整数来最大化缓存使用率,但是,我能够放宽这个约束 - 满足用户处理每用户每毫秒更高资源数的请求 - 使用无符号短路和STILL 双倍性能,因为即使有3个相邻的512个无符号短路向量,L1高速缓存的32K数据高速缓存也可轻松容纳所需的3,720个字节,其中2/3个字节位于刚才使用的位置。只有当Tail,Summary或Head包装的是3个中的1个时,由8MB L3cache中的任何重要“步骤”分隔。
此代码的总运行时内存占用量低于2MB,因此它完全由片上高速缓存运行,即使在具有4个内核的i7芯片上,也可以运行此过程的4个实例而不会降低性能完全,总吞吐量随着5个进程的运行而略有上升。这是关于缓存使用的Opus Magnum。
答案 5 :(得分:2)
大多数C / C ++编译器更喜欢优化大小而不是“速度”。也就是说,由于缓存效应,较小的代码通常比展开的代码执行得快。
答案 6 :(得分:0)
如果我是你,我会确保我知道代码的哪些部分是热点,我将其定义为
如果您有这样的热点,那么它应该适合缓存。我不知道你怎么告诉它这样做,但我怀疑它是自动的。