C#中的缓存友好性

时间:2014-04-03 23:51:54

标签: c# c++ list caching vector

我刚刚看到Herb Sutter在2014年建设大会上发表了一篇非常有趣的演讲,名为"现代C ++:您需要了解的内容"。这是谈话视频的链接:http://channel9.msdn.com/Events/Build/2014/2-661

讨论的主题之一是std::vector如何对缓存友好,主要是因为它确保其元素在堆中相邻,这对空间局部性有很大影响,或者至少是#39;我认为我已经理解了;即使是插入和移除物品也是如此。巧妙地使用std::vector可以通过利用缓存来显着提升性能。

我想尝试使用C#/ .Net,但是如何确保我的集合中的对象在内存中都相邻?

对C#和.Net上缓存友好性资源的任何其他指针也表示赞赏。 :)

3 个答案:

答案 0 :(得分:3)

在C#中实现此目的的唯一方法似乎是使用值类型,这意味着使用结构而不是类。然后使用List将对象存储在连续的内存中。您可以在此处阅读有关结构与类的更多信息:Choosing Between Class and Struct

答案 1 :(得分:2)

对于GC管理的语言,您往往会失去对每个对象存储在内存中的位置的显式控制。您可以控制内存访问模式,但如果您无法控制正在访问的内存地址,则会变得有点无用。最重要的是,每个对象往往带来一个不可避免的间接开销和类似虚拟指针(在引擎盖下)的东西,以允许动态调度,反射等。它类似于必须存储指针的等价物(引用)对象,并且必须与每个对象实例间接工作,同时还存储模拟指针,以允许运行时类型信息,如反射和虚拟分派所需的内容。

因此,即使您连续存储一个对象引用数组,也只能使对象的类比指针缓存友好,以便顺序访问。每个对象的内容仍然可能分散在内存中,当您将内存区域加载到缓存行中时,只会在数据被驱逐之前使用一个对象的数据,从而导致缓存丢失。

它实际上是C ++这样的语言的第一吸引力:它让你仍然可以使用OOP同时控制将在内存中分配所有内容的位置,以及所有内存使用的确切内存,并让它您从虚拟调度,RTTI等相关的开销中选择退出(实际上是默认情况下),同时仍然使用对象和泛型等。与此同时,我个人对C#和Java等语言的最大吸引力在于每个对象(如反射)可以获得的内容,每个对象都带有每个对象的开销,但如果您的代码有足够的用途,这是合理的成本。

使用纯旧数据类型(C#中包含struct):

也就是说,我已经看到用C#和Java编写的非常高效的代码与C和C ++相同,但关键的区别在于他们避免使用代码的一小部分代码真的是性能关键。例如,我看到了一个交互式Java光线跟踪器,它使用单路径跟踪,速度非常快,因为它正在做的蛮力性质。然而,关键的区别在于,虽然大多数光线跟踪器是使用漂亮的面向对象的代码编写的,但对于性能关键部分(BVH,网格表示和存储在叶子中的三角形),它避免了对象并且只使用了大数组{ {1}}和int[]。这个性能关键的代码很漂亮" raw"甚至更多" raw"比同等优化的C ++代码(它看起来更接近C或Fortran),但它只需要光线跟踪器的几个关键区域。

当您对性能关键区域使用普通旧数据类型的数组时,您可以获得对内存的充分控制,从而产生重大影响,因为如果数组是由GC管理的,并不是那么重要可能偶尔在GC循环后从一个存储位置移动到另一个存储位置(例如:在第一个GC循环后离开Eden空间)。这并不重要,因为数组是作为整体移动的。结果,索引1处的元素仍然位于元素0和元素2的旁边。对于数组的缓存友好顺序处理而言,重要的是数组中的每个元素都紧挨着另一个元素。内存,甚至在Java和C#中,只要您使用POD数组(在我上次检查时在C#中包含float[]),您就具有该级别的控制权。

因此,当您编写性能关键代码时,请确保您的设计留有足够的喘息空间来改变事物存储方式的表示,如果设计在未来变得瓶颈,可能会远离对象。作为一个基本示例,对于structs对象(实际上是像素的集合),您可以避免将像素存储为单个对象,并且您绝对不希望公开抽象Image对象供客户直接使用。相反,您可以将像素集合表示为Pixel接口后面的普通旧整数或浮点数组,而单个图像可能代表一百万像素。这将允许对图像进行缓存友好的顺序处理。

避免使用Image来处理大量的事情。

不要过多地使用new来处理这些事情。为性能关键区域批量分配:一个new用于表示图像中一百万像素的整数百万个整数,例如,不是一次调用new一次分配一个像素到你控制之外的记忆中的位置。除了缓存友好性之外,如果在C#中将每个像素分配为单独的对象,则存储用于动态调度和反射的模拟指针等所需的内存开销通常会大于整个像素本身,使内存使用量增加一倍或三倍一个像素。

在性能关键区域设计批量,均匀的处理。

如果您正在编写围绕OOP和继承而不是ECS和鸭子类型的视频游戏,那么经典继承示例通常过于细化:new继承Dog,{{1}继承Mammal。相反,如果你要在每一帧的游戏循环中处理大量哺乳动物,我建议改为Cat继承MammalCats继承MammalsDogs成为一个抽象的容器,而不是一次代表一个哺乳动物的东西。这将为您的设计提供足够的喘息空间,以便在您尝试处理抽象Mammals的抽象Mammals时,非常有效地处理许多狗一次非常有效的原始数据通过Dogs接口间接地为具有多态性的狗做事。

无论您是使用C或C ++还是Java或C#还是其他任何内容,此建议实际上都适用。要编写缓存友好的代码,您必须从留下足够呼吸空间的设计开始,以便在将来根据需要优化其数据表示和访问模式,并且理想情况下使用分析器。最糟糕的情况是最终会出现一个 design ,它累积了许多依赖关系,这是一个瓶颈,就像对Mammals对象或Mammals接口的许多依赖关系一样精细化以进一步优化,无需重写和重新设计整个软件。因此,避免大规模依赖于过度细化的设计,这些设计不会留下任何喘息的空间来进一将依赖关系从类比Pixel重定向到类比IPixel,从类比Pixel转换为类比Image,您将能够优化到您的心脏的内容没有昂贵的重新设计。

答案 2 :(得分:1)

您可以使用List,它连续存储项目(如std :: vector)。 有关详细信息,请参阅this answer