CPU高效算法?

时间:2015-12-19 19:54:00

标签: c performance memory-management cpu-usage cpu-speed

我在涉及以性能为中心的编程方面做得相当多。通常,我所教授和了解的大部分技术都与保存RAM有关。

话虽如此,我最近在这里解答了这个问题Memory efficient AI objects for physics game

我被告知的地方:

  

通常是CPU在内存耗尽之前耗尽速度

我们做了一些测试,似乎打包/拆包确实可以节省内存,但肯定会降低性能。

但正如我所说,大多数典型的表演规则'我已经看过,处理保存RAM。

例如,程序速度的一个主要议题是动态内存分配,它也专注于RAM保护。

我想知道的是:什么使代码CPU高效?像C这样的低级语言是否具有更高的CPU效率灵活性?如果是这样,为什么/如何?

为简单起见,我们不要讨论有关汇编语言的讨论,因为它们超出了这个问题的范围。

4 个答案:

答案 0 :(得分:4)

<强>概述

首先,当你超越明显的算法效率低下时,你想要找到一个不错的分析器。分析器有几个好处:

  1. 显示精确测量值(花费时间,缓存未命中,分支错误预测等)。
  2. 追逐您的热门热点往往会迅速加速您的学习过程,并直接了解导致微观层面瓶颈的原因(例如:内存层次结构和分支)。
  3. 会让你成为更好的优先顺序。它还会教你什么代码不需要优化,这意味着你可以专注于其他指标,如生产力(可维护性,安全性等)。
  4. 对我而言#2实际上是一个很大的问题。在我掌握了一个分析器之前,我并没有真正开始学习很多这样的东西。通过处理一个实际的,相当大的项目并查找中间出现的事情,你可以学到很多东西。以同样的方式,当你追逐一个接一个的热点并研究它存在的原因时,掌握一个分析器,学习微观效率和计算机体系结构会更容易。

    内存优化

    除此之外,可能超出算法复杂性的第一件事(关于可伸缩性而不是一些绝对的性能感)是内存效率

      

    警告:这有点过于简单,不会涉及编译器设计主题,如寄存器分配和堆栈溢出,甚至是内存层次结构的非常详细的描述。< / EM>

    机器和操作系统的工作方式是以分层形式的内存建立的,范围从绝对最快但最小的内存(寄存器)到绝对最慢和最大(磁盘)。

    当访问内存时,系统会将速度较慢的内存加载到更快的内存中。例如,操作系统可能将内存从辅助存储设备分页到4千字节块中的物理内存(DRAM)。

    [4 kilobyte chunk][*4 kilobyte chunk][4 kilobyte chunk][...]
    // '*' indicates the chunk that's loaded in.
    

    当您请求在对齐的4 KB块周围的任何位置访问虚拟内存时,系统将在该块中寻呼到DRAM。然而,我们还没有完成。通常在我们可以做任何事情之前,我们必须从DRAM加载到CPU缓存中,CPU缓存本身被分成一个层次结构。在这些情况下,内存可能会加载到64字节对齐的缓存行块中,如下所示:

    [64-byte chunk][64-byte chunk][*64-byte chunk][...]
    

    ...所以内存访问最终会从DRAM加载到CPU缓存中。当您请求在这些64字节块中的一个中访问DRAM中的内存时,整个64字节块将加载到CPU缓存中。

    然后CPU缓存本身被划分为层次结构(尽管通常都使用相同的缓存行大小),并且内存向下移动到更快但更小的CPU缓存(最快为L1)。最后但并非最不重要的是,在执行算术之前,L1高速缓存中的内存被加载到一个寄存器中,对于通用CPU寄存器,该寄存器的大小可能是64位。在这种情况下,我们最终将我们的CPU高速缓存内存放在64字节的高速缓存行中:

    [64 bits][64 bits][64 bits][*64 bits][64 bits][...]
    

    所以最后在我们向最小和最快的内存工作之后,我们在寄存器上做了一些算术指令,然后通常将结果移回到层次结构中。

    现在这有点粗略,我最终可能会在以后变得尴尬。然而要记住的是,CPU从较慢,较大的区域获取内存,以对齐的块中的较快,较小的区域。它通过连续的少数人抓住记忆。这样做的希望是,在最终被驱逐之后,你最终会多次访问该内存块(空间/时间局部性)。

    内存优化

    记住这一点,从代码中获得最大的性能通常需要开始优先考虑内存布局和访问(当然除了算法和数据结构)。如果没有有效的内存访问,最快的算术指令几乎无济于事。

    值得记住的一件事是连续数组。连续排列的数据,以连续模式访问,对于这种存储器层次结构是理想的。这是因为计算机可能会抓住一大块旧内存(页面,缓存行),然后我们会逐步浏览它并访问整个块,同时在驱逐之前它会以更快的形式存储。

    使用“之前的数据”

    最糟糕的情况是,当你最终加载一大块旧内存只是为了使用它的一小部分,然后让系统在我们使用其余部分之前将其逐出。这样的场景可以显示在链接结构中,例如链表和树(缺少内存分配器,以便为它们提供更连续的表示),我们可能最终为一个节点周围的内存区域加载一块内存,只访问内部的一个节点。它然后逐出它。

    另一种情况显示在托管语言中,每个用户定义的类型必须单独分配(例如:通过垃圾收集器),但聚合到基于数组的列表结构中。在这种情况下,即使我们正在存储这些对象的数组,每个对象实际上都是通过引用(如指针)来表示,该引用指向内存中的其他位置。

    这可能是使用C或C ++等语言的最令人信服的理由之一。它们允许用户定义的类型连续聚合,也可以在堆栈上分配(具有大量的时间局部性)。

    <强> TL; DR

    如果您想了解有关这些科目的更多信息,我建议您调查参考地点。这篇文章也是必须的:http://lwn.net/Articles/250967/

    最后但并非最不重要的一点是,如果我允许一个无耻的插件来获得赏金问题,我花了很多时间来回答:What is the most efficient way to represent small values in a struct?

    但无论如何,首先要抓住一个分析器并开始追逐热点。它是最快的学习方式,也是最有效的优化方式。

    <强>更新

    Jenz的智慧建议&#39;很好的回答也让我想要包含免责声明,因为算法效率仍然是首要担心的问题。在处理最次优的算法时,我一直在处理那些谈论缓存效率和多线程的类型,以及那些无效的优先级。微观优化或并行化一百万个元素的泡沫远不是一个明显的例子。

    许多内存优化技术倾向于最直接帮助的是那些顺序的情况,除了触摸每个元素之外别无选择(无法达到线性复杂度)。例如,一个需要处理每个粒子的粒子模拟器,一个必须影响每个像素的图像处理算法,涉及大量矩阵的矩阵乘法。在这些情况下,由于我们必须处理每个元素,因此无法在算法上跳过大部分工作并仍然得到相同的结果。在那些时候,内存优化技术可以变得比并行化更有效,并且可以为您提供更多的并行化。

    然而,数据结构和算法的核心也存在内存效率问题。由于内存效率的原因,数组的快速排序仍然倾向于在实际场景中击败合并排序。有些情况下,线性算法可能会击败线性算法,前提是前者的内存效率要高得多。

    内存分配器

    我之前提到了像树和链接列表这样的链接结构的缓存不友好性,但是假设每个节点都是针对通用分配器(并且可能不是一次全部)分配的。实际上可以开始实际上使得甚至像单链接列表这样的结构更加适用的事情之一是使用内存分配器,该分配器使其节点返回他们通常缺少的空间局部性。因此,有一些方法可以挖掘您的数据结构并利用内存分配器,并使它们更有效,而无需实际使用全新的。

    还有像展开列表这样的数据结构经常被忽略,因为它们没有提供链表上的算法优势。然而,从内存效率的角度来看,它们提供了更大的好处,并且在我们有两个具有类似算法复杂性但存储器布局大不相同的数据结构的情况下,获胜者往往是具有更高效的内存布局和访问模式的数据结构。展开的列表将元素数组链接在一起而不是单个元素,并且空间局部性再次强烈支持连续的基于数组的表示。

    然而,任何微优化都会降低代码的直接性和可维护性。因此,优化的关键一般是优先级,并且分析器可以至少在某种程度上帮助您保持检查(从生产力的角度来看,分析器具有向您显示优化你可能会试图尝试)。

答案 1 :(得分:2)

什么使代码CPU高效?

代码中较少的指令,较少的分支和最小的变量使用导致较少的cpu资源使用。所有这些都可以通过为您的逻辑应用有效算法并减少不必要的代码来有效地实现。尝试从内存中减少更多I / O,这需要更多时间来访问。

像C这样的低级语言是否具有更高的CPU效率灵活性?

CPU的工作只是执行指令,您只能在软件中控制以最小化指令。 CPU效率与指令数量成正比。 C被设计为使用相对简单的编译器进行编译,以提供对内存的低级访问,提供有效映射到机器指令的语言结构,并且需要最少的运行时支持。因此,C对于以前用汇编语言编写的许多应用程序很有用,例如在系统编程中。

答案 2 :(得分:1)

一般性问题值得一般回答:

  

所有优化都是缓存练习。

特别是在今天的多级缓存架构上。

请注意愚蠢的想法,您需要做的就是将级别1指令缓存中的代码和1级数据缓存中的所有数据塞入,以便有效地计算O(N 2 )算法,因为通过在大桌子中进行O(1)查找来进行繁重的生活和呼吸练习的天才。

换句话说,RAM和磁盘空间很便宜。使用它们对您有利。

答案 3 :(得分:1)

只要一种语言具有相当不错的编译器,它生成的代码应该与其他代码大致相同。

不同语言的问题是它们会诱使你做一些花费额外时间的事情。 像C ++一样诱使你使用new,因为它很容易, 并且有各种各样的容器类可以很容易地做出花哨的东西。 如果你在C工作,那么做一些花哨的东西要麻烦很多,所以猜猜你做了什么 - 除非你真的必须这样做而且你不付出代价性能

很有可能认为高级语言的所有优点都是免费的,或者至多可以忽略不计, 但实际上它们可以相互繁殖,如this example shows。 您仍然可以使用高级语言,但如果您知道如何进行性能调整,则可以获得其高级功能的好处,而无需支付您不需要的费用。