嵌入式ARM算法移植与优化

2023-05-16

目录

一、算法优化指导思想

1.算法优化基本原则

2.算法优化方法

 二、编译器优化

1,函数内联

2,消除公共子表达式

3,循环展开

4,GCC优化选项

5,ARMCC优化选项

三、ARM内存系统优化

1,数据cache优化

2,循环分块

3,内部循环优化

4,结构对齐

5,综合相关性影响

6,优化指令cache的使用

7,优化L2与外部cache的使用

8,优化TLB使用

9,数据终止优化

10,预取一个内存块的访问

四、代码级优化

1,循环终止

2,循环融合

3,减少栈和堆的使用

4,变量选择

5,指针别名

6,除法和模

7,外部数据

8,内联与嵌入式汇编

9,复杂寻址模式

10,对齐访问

11,链接优化


一、算法优化指导思想

1.算法优化基本原则

        A 等效原则:优化前后程序实现的功能一致;

        B 有效原则:优化后要比优化前运行速度快或占用存储空间小,或二者兼有;

        C 经济原则:优化程序要付出较小的代价,取得较好的结果。

2.算法优化方法

        算法优化参考流程如下图所示:

  算法性能评估指标

  综合性能测试评估基本指标:时间,内存,CPU占用率,功耗

  函数优化大致方向:

        A消除冗余计算

        B消除,优化选择,跳转分支

        C低效率函数优化

        D内存访问效率优化

 二、编译器优化

1,函数内联

       当一个函数被调用时,会有一定的开销。如果它必须重用R14,被调用的函数必须在堆栈上存储自己的返回地址。根据过程调用标准,也可能需要指令将参数放入适当的寄存器并将寄存器推到堆栈上。当函数结束时返回到原始执行点时,可能会有开销,再次需要一个分支(以及相应的指令管道清理),并可能从堆栈中取出寄存器。当函数只包含少量指令,并且这些函数代表了总运行时的大量内容时,这种函数调用开销就会变得非常大。而且,执行分支会使用分支预测器资源,这会影响整个程序的性能。函数内联通过将对函数的调用替换为函数本身的实际代码副本(称为内联放置代码)来消除这种开销。

      如果函数只在一个地方被调用,那么关键代码路径的内联总是值得优化的。另外需要考虑的是,内联可以帮助实现其他优化。显然,增加函数被调用的次数将增加函数的内联副本的数量,这将增加代码大小的成本。

       GCC只在每个编译单元内执行内联。inline关键字可以用来请求特定函数必须在任何可能的地方进行内联,即使是在其他文件中。GCC文档提供了更多的细节,以及如何将其与static和extern结合使用。在考虑缓存优化时,我们将更详细地了解内联。

2,消除公共子表达式

       另一个简单的源码级优化是在后面的表达式中重用已经计算好的结果。当使用优化命令行开关打开时,这种常见的子表达式消除会自动执行,可以使代码更小、更快。然而,编译器不一定会识别出所有的情况,有时手工完成会更有用。

例:
    i = a * b + c;
    j = a * b * d;
编译器可以像例-2中那样处理这段代码。但必须注意的是,只有在a和b都不是易失性的情况下,它才能这样做。
    tmp = a * b;
    i = tmp + c;
    j = tmp * d;
这减少了指令计数和周期计数。

3,循环展开

      每次循环迭代都有相应的开销。每个条件循环必须在每次迭代中包含一个循环结束的判断。此外,还有一个分支指令迭代遍历循环,这可能需要更多周期来执行。我们可以通过部分或完全展开循环来避免这个问题。

循环展开前:
    for (i = 0; i < 10; i++)
    {
        x[i] = i;
    }
循环展开后:
    x[0] = 0;
    x[1] = 1;
    x[2] = 2;
    x[3] = 3;
    x[4] = 4;
    x[5] = 5;
    x[6] = 6;
    x[7] = 7;
    x[8] = 8;
    x[9] = 9;

      当代码以这种方式编写时,我们删除了比较和分支指令,并有一个存储和添加的序列。这显然比原始代码大,但执行起来要快得多。

      通常,循环展开通常被认为可以提高程序的速度,但代价是代码大小的增加(非常短的循环除外)。然而,在实践中,在许多硬件平台上可能并不总是如此。在许多系统中,访问外部内存需要大量的周期,并提供指令缓存。循环代码通常能够很好地装入缓存。代码在第一次循环迭代时被取到缓存中,然后直接从缓存中执行。展开循环可能意味着代码只执行一次,而且因为代码比较大,所以不能很好地缓存。这种情况更可能发生在只执行一次的函数上。无论是否展开循环,频繁执行的循环都可能被缓存。另一个需要考虑的问题是,现代ARM处理器通常包括分支预测逻辑,这种逻辑可以通过在实际计算条件之前预测分支是否会被执行来隐藏管道刷新的影响。在某些情况下,可以折叠分支指令,这样它就不需要实际的处理器周期来执行。

4,GCC优化选项

      GCC有一系列优化级别,以及启用或禁用特定优化的单独选项。总体编译器优化级别由命令行选项-On控制,其中n是所需的优化级别,如下所示:

        O0:没有进行任何优化。每个源代码命令与可执行文件中的相应指令直接相关。这为源代码级调试提供了最清晰的视图。

        O1:这样就可以实现最常见的优化形式,不需要对大小和速度进行决策,包括函数内联。它通常可以比O0生成更快的编译,因为生成的文件更小。

        O2:这支持额外的优化,例如指令调度。同样,不会使用具有速度与大小关系的优化。

        O3:这可以进行额外的优化,比如激进的函数内联,因此可以以牺牲图像大小为代价提高速度。此外,这个选项允许-ftree-vectorize -导致编译器尝试从标准C或c++自动生成NEON代码。

        -funroll-loops:此选项独立于on选项,并启用循环展开。循环展开可能会增加代码大小,并不是在所有情况下都有好处。操作系统。

        -Os:这选择了试图最小化图像大小的优化,甚至以速度为代价。

5,ARMCC优化选项

        armcc编译器使您能够编译C和c++代码。它是一个优化编译器,具有一系列命令行选项,使您能够控制优化级别。命令行选项提供了优化级别的选择,如下所示:

-Ospace:此选项指示编译器执行优化以减少映像大小,但可能会增加执行时间。

-Otime:该选项指示编译器执行优化,以减少执行时间,但可能会增加镜像大小。 -O0:关闭大多数优化。它提供了最好的调试视图和最低级别的优化。

-O1:删除未使用的内联函数和静态函数。关闭会严重降低调试视图性能的优化。如果与——debug一起使用,该选项将提供一个令人满意的调试视图,并具有良好的代码密度。

-O2(默认)。高的优化。如果与——debug一起使用,调试视图可能不太令人满意,因为目标代码到源代码的映射并不总是清晰的。

-O3:执行与-O2相同的优化,但是与-O2相比,生成代码中的空间和时间优化更倾向于空间或时间优化。也就是说:-O3 -Otime的目标是生成比-O2 -Otime更快的代码,但可能会增加映像大小。-O3 -Ospace的目标是生成比-O2 -Ospace更小的代码,但性能可能会降低。

三、ARM内存系统优化

        编写最适合系统的代码是编程艺术的关键部分。它要求您理解编译器和底层硬件将如何执行代码行中描述的任务。如果你可以用较少的外部内存来完成这项工作,你可以通过将所有东西都保存在芯片上来节省功耗。此外,通过减少访问外部内存的频率,可以改善系统的性能,使软件运行得更快,或者使处理器的时钟运行得更慢或更短,从而节省功耗。

1,数据cache优化

       在大多数Cortex-A系列处理器中,内存访问命中缓存和不命中缓存之间存在显著的性能差距。缓存丢失可能需要数十个周期来解析。缓存在几个周期内命中返回数据,编译器通常可以以一种隐藏延迟的方式调度指令。因此,对于大多数算法来说,确保缓存缺失最小化是最重要的可能优化。最重要的改进是那些影响1级缓存的改进。

        考虑数据缓存丢失的问题。对于使用大于可用缓存大小的数据集的代码段,优化尤其重要。理解数据在内存中的排列以及它如何与数据缓存访问相对应是很重要的。代码的结构必须确保最大限度地重用已经加载到缓存中的数据。正是这种数据局部性原则,即在程序执行期间,在空间和时间上对同一缓存线的访问集中的程度,提供了最佳的性能。

2,循环分块

        循环平铺将循环迭代划分为更小的块,从而促进了数据缓存的重用。大数组被划分为更小的块(tile),这些块将访问的数组元素与缓存大小匹配。说明这种方法的经典例子是一个大的矩阵向量乘积。

        考虑两个方阵a和b,大小都是1024 1024。例显示了计算矩阵向量乘积的代码。这要求您将每个数组中的每个元素与另一个数组中的每个元素相乘

for (i = 0; i < 1024; i++)
    for (j = 0; j < 1024; j++)
        for (k = 0; k < 1024; k++)
            result[i][j] = result[i][j] + a[i][k] * b[k][j];

        在这种情况下,矩阵a的内容是按顺序访问的,但矩阵b在内部循环中逐行前进。因此,您很可能会遇到每次乘法操作的缓存丢失。

        很明显,计算结果矩阵中每个元素的加法的顺序不会改变结果,忽略了溢出等因素的影响。可以以提高缓存命中率的方式重写代码。在这个例子中,矩阵b的元素按如下方式访问(0,0),(1,0),(2,0)(1023,0),(0,1),(1,1)(1023,1)。元素按(0,0)、(0,1)等顺序存储在内存中。对于单词大小的元素,这意味着元素(0,0),(0,1)(0,7)将存储在同一个缓存行中。为了简单起见,我们假设矩阵的起始地址与缓存线对齐。

        因此,元素(0,0)、(0,1)、(0,2)等将在同一条缓存线上;当你将(0,0)加载到缓存中时,你也会得到(0,1…7)。在内部循环完成的时候,很可能这个缓存线将被逐出。

        如果您修改代码,使中间循环的两次(或实际上是四次,或八次)迭代在执行内部循环时立即执行,如例17-7所示,您可以做出很大的改进。类似地,您也可以展开外层循环两次(或四次或八次)。

for (io = 0; io < 1024; io += 8)
	for (jo = 0; jo < 1024; jo += 8)
		for (ko = 0; ko < 1024; ko += 8)
			for (ii = 0, rresult = &result[io][jo],ra = &a[io][ko]; 
				ii < 8;ii++, rresult += 1024, ra += 1024)
				for (ki = 0, rb = &b[ko][jo];ki < 8; ki++, rb += 1024)
					for (ji = 0; ji < 8; ji++)
						rresult[ji] += ra[ki] * rb[ji];

         现在有六个嵌套循环。外层循环的步骤为8,表示1级缓存的每行中存储了8个int大小的元素。还引入了一些额外的优化。“ji”和“ki”的顺序颠倒了,只有一个表达使用“ki”,但有两个使用“ji”。此外,您可以通过从内部循环中删除公共表达式来进行优化。在C语言中,所有的指针访问都可能导致混叠,因此,通过使用result、ra和rb访问数组元素,数组索引速度加快。

3,内部循环优化

        在许多程序中,都存在嵌套循环,一个非常简单的例子就是在一个二维数组中逐行执行项的代码。对于相当复杂的代码,有时可以通过重新安排循环来获得更好的性能。最好将迭代次数较少的循环作为外部循环,将迭代次数最高的循环作为最内部的循环。

        这有两个潜在的优势。一个是编译器可能会展开内部循环。更重要的是,对于复杂的循环来说,嵌套循环的大小足够大,可能不会在同一时间全部保存在一级缓存中,此更改将提高总体缓存命中率。一些编译器可以在更高级别的优化时自动进行此更改。例如,GCC 4.4添加了switch- floon -interchange来实现这一点。

4,结构对齐

        结构元素的有效放置和对齐并不是影响缓存效率的数据结构的唯一方面。当代码有一个大的工作集时,重要的是要有效地使用可用的缓存空间。为此,可能需要重新安排数据结构。

       通常有跨越多个缓存线的数据结构,但是程序在任何特定时间只使用结构的少数部分。如果有很多这种类型的对象,可以尝试拆分结构,使其适合于缓存线。例如,可以将一个结构数组拆分为两个或多个较小结构的数组。这只有在对象本身对齐到缓存边界时才有意义。例如,考虑这样一种情况:您有一个非常大的64字节结构的实例数组(比缓存大小大得多)。在这个结构中,有一个字节大小的量,还有一个常用的函数,该函数遍历数组,只查看这个字节大小的量。这个函数将使缓存的使用效率低下,因为您将不得不加载整个缓存线路来读取8位值。如果这些8位的值被存储在它们自己的数组中(而不是作为一个更大的结构的一部分),每个缓存行填充将得到32或64个值。

        支持非对齐访问,但与对齐访问相比会占用额外的周期。因此,出于性能原因,移除或减少非对齐访问是明智的。

5,综合相关性影响

        正如我们所看到的,ARM L1缓存通常是4路的集合关联,而L2缓存通常是8路或16路的集合关联。如果数据中有超过四个位置属于同一个缓存集,就会出现性能问题,因为即使缓存的其他部分可能未被使用,也会出现重复的缓存丢失。ARM L1缓存使用物理地址而不是虚拟地址,所以对于在User模式下操作的程序员来说,要处理这个问题是很困难的。

        这个问题的一个特别常见的原因是安排数据,使其处于2的幂的边界上。如果缓存大小是16KB,那么每种方式的大小都是4KB。如果您有多个排列在4KB的边界上的数据块,那么对每个块的第一次访问将进入第0行。如果代码访问了几个这样的块中的第一行,那么即使总共只使用了5条缓存线,你也会得到缓存丢失。非对齐访问可能会增加这种可能性,因为每次访问可能需要两条而不是一条缓存线。

6,优化指令cache的使用

        C程序员不能直接控制代码如何使用指令缓存。分支指令之间的代码是线性的,这种顺序访问模式可以有效地使用缓存。核心的分支预测逻辑将试图最小化分支导致的延迟,因此您几乎无法提供帮助。您的主要目标是减少代码占用空间。ARM编译器和GCC在-O2和-O3上启用了许多编译器优化,用于处理循环优化和函数内联。如果代码占整个程序执行的很大一部分,那么这些优化将提高性能。特别是,函数内联有多个潜在的好处。显然,它可以通过在函数调用和退出时删除分支,以及可能的堆栈使用时删除分支,从而减少分支惩罚。同样重要的是,它使编译器能够优化更大的代码块,从而更好地优化值范围传播和消除未使用的代码。

        然而,针对速度优化的修改增加了代码大小,实际上会因为缓存问题而降低性能。较大的代码不太可能装入L1缓存(或者实际上是L2缓存),额外的缓存线填充所带来的性能损失可能会超过优化的任何好处。通常最好使用armcc -Ospace或gcc Os选项来优化代码密度而不是速度。显然,使用Thumb代码也可以提高代码密度和缓存效率。

        关于函数内联有一些有趣的决定,在某些情况下,人类的判断可以改进编译器的判断。一个只从一个地方调用的函数如果内联,总是会带来好处。有人可能认为内联非常小的函数总是有好处,但事实并非如此。从很多地方调用的一个小函数的实例很可能在指令缓存中被多次重用。如果重复内联相同的函数,则更有可能导致缓存丢失,并从缓存中驱逐其他可能有用的代码。Cortex-A系列处理器中的分支预测逻辑是高效的,无条件函数调用和返回所消耗的周期很少,比填充缓存线要少得多。您可能希望使用GCC函数属性noinline或always_inline来控制这种情况。

        这是一个普遍问题,并不是内联函数所特有的。每当使用条件执行时,如果它是不平衡的,即表达式往往导致一个结果,而不是另一个结果,那么管道中就有可能出现错误的静态分支预测和冒泡(指令执行的延迟)。通常更好的做法是对条件块进行排序,这样经常执行的代码是线性的,而很少执行的代码必须分支到,除非实际使用,否则不会预取。与freorder-blocks优化选项一起使用的GCC属性__builtin_expect可以帮助解决这个问题。

        处理器的性能监视器块(和OProfile)可以用来度量代码中的分支预测率。这里有两个影响。正确的分支预测通过避免管道刷新节省了时钟周期,但是使用更少的跳过代码的条件分支,可以使更多的程序适合L1缓存,从而提高性能。

7,优化L2与外部cache的使用

        所有关于使用L1缓存的优化也适用于L2缓存访问。最好的性能来自于一个比L2缓存小的工作数据集,并且数据被使用了不止一次;缓存只使用一次的数据几乎没有什么好处,除了可能产生更多最优的总线访问。如果数据集大于缓存大小,则可以考虑与L1缓存中描述的技术类似的技术。但是,对于外部缓存还有一个需要考虑的问题,那就是它们可能与其他内核共享,因此单个处理器的有效大小可能小于实际大小。此外,当编写在许多ARM家族上运行的泛型代码时,很难优化L2缓存的使用。这种缓存的存在并没有得到保证,而且它的大小在不同的系统之间会有很大的差异。

8,优化TLB使用

        一般来说,转译后备缓冲区(参见第9章)的优化使用范围要比优化缓存访问小得多。要点是尽量减少使用的页面数量少(这显然给了TLB)和使用大型MMU映射(supersections或部分优先于4 kb页),因为这降低了单个转换表走的成本(一个外部内存访问,而不是两个),也意味着更大的内存在一个单独的TLB条目中表示(也给出更少的TLB错过)。然而实际上,像Linux这样的操作系统到处使用4KB的页面,所以主要的优化技术可以是单独的很少的频繁访问的代码和数据访问代码和数据(例如异常处理代码可以移动到一个不同的页面),并试图限制经常访问的页面的数量低于处理器硬件支持的最大数量。主要的优化是尝试处理每个页面的多个缓存线的数据,因此L1缓存是限制因素,而不是TLB条目。

9,数据终止优化

        在Linux上下文中,描述了在第一次访问内存页面时,以及第一次写入该页面时,页面错误如何产生数据中止。这意味着内核中止处理程序被调用以采取适当的操作,这有一定的性能开销。简单地说,您可以通过使用更少的页面来减少这个开销。同样,使代码更小的代码优化也会有所帮助,这将减少数据空间的大小。

10,预取一个内存块的访问

        ARM Cortex-A系列处理器包含复杂的缓存系统,并支持推测和无序执行,从而隐藏与内存访问相关的延迟。但是,对外部内存系统的访问通常非常慢,因此仍然会有一些损失。如果可以在需要指令或数据之前将它们预取到缓存中,就可以隐藏这种延迟。

        ARM处理器使用PLD指令为数据的预加载提供支持。PLD指令是一种提示,使您能够在应用程序实际读取或写入数据之前请求将数据加载到数据缓存中。PLD操作可能产生一个缓存行填充或数据缓存缺失,独立于加载和存储指令的执行,而核心继续执行其他指令。如果得到支持和正确使用,可编程逻辑器件可以通过隐藏内存访问延迟来显著提高性能。还有一个PLI指令,它使您能够提示处理器,在不久的将来可能会从某个特定地址加载指令。这可能导致处理器将指令预加载到它的缓存中。

        除了这个由程序员发起的预取之外,核心还可能支持自动数据预取。本质上,内核可以检测到一系列对内存的顺序访问。当它这样做的时候,它会在程序实际使用它们之前,自动猜测地请求下面的缓存线。

        在许多系统中,使用memset()或memcpy()函数初始化或移动内存块要消耗大量的周期。优化后的ARM库通常会使用Store Multiple指令来实现这些功能,每个Store都与缓存线的边界对齐。

四、代码级优化

      分析工具使您能够识别可以从优化中获益的代码段或函数,以及不同的编译器选项如何对我们的代码进行编译器优化。现在我们将考虑各种各样的源代码修改,这些修改可以在ARM上生成更快或更小的代码。

1,循环终止

        对于已经被分析器识别的循环,使用在0(零)处结束的整数循环计数器可能是合适的,而不是从0(零)开始。这是因为用于更新循环计数器的ADD或SUB指令可以免费使用与0的比较,而与非0值的比较通常需要一个显式的CMP指令。

Replace a loop that counts up to a terminating value:
    for (i = 1; i<= total; i++)
with one that counts down to zero:
    for (i = total; i != 0; i--)
This will remove a CMP instruction from each iteration of the loop.

        为循环计数器使用int(32位)变量也是一种良好的实践。这是因为ARM本身是32位机器。它的ADD汇编语言指令在两个32位寄存器上操作。如果它执行的ADD(或其他数据处理操作)数量较小,编译器可能会插入额外的指令来处理溢出

2,循环融合

        这是多种可能的循环技术之一,您或优化编译器都可以使用这种技术。它本质上意味着合并具有相同迭代计数且没有相互依赖关系的循环。

for (i = 0; i < 10; i++)
{
    x[i] = 1;
}
for (j = 0; j < 10; j++)
{
    y[j] = j;
}
很明显,这可以优化为:
for (i = 0; i < 10; i++)
{
    x[i] = 1;
    y[j] = j;
}

        值得一提的是,这种方法有时会导致性能下降,这取决于缓存的关联性和被访问数据的地址,因为缓存的影响(如抖动)。

3,减少栈和堆的使用

        通常,通过代码尽量减少内存使用是一个好主意。ARM处理器有一个寄存器集,它为编译器保存变量提供了一组相对有限的资源。当所有寄存器都使用当前活动变量分配时,额外的变量将溢出到堆栈中,导致内存操作和代码执行的额外周期。有很多方法可以帮助你。一个关键的规则是尝试在任何时候限制活动变量的数量。

        寄存器中最多可以传递四个参数给一个函数。附加参数在堆栈上传递。因此,传递四个或更少的参数要比传递五个或更多的参数有效率得多。当然,有问题的ARM寄存器的大小是32位的,因此如果您传递一个64位变量,它将占用我们四个寄存器插槽中的两个。出于类似的原因,递归函数通常不会产生有效的处理器寄存器使用。还请记住,非静态c++函数也使用一个带有this指针的参数槽。

4,变量选择

        ARM整数寄存器是32位大小的,因此在使用32位大小的变量时最容易产生最佳代码,因为这避免了提供额外的代码来处理32位结果溢出8位或16位大小的变量的情况。

Consider the following code:
    unsigned int i, j, k;
    i = j+k;
The compiler would typically emit assembly code similar to:
    ADD R0, R1, R2
如果这些变量是short(16位)或char(8位),编译器必须确保结果不会溢出半字或字节。对于带符号的半字(short),同样的代码可能如例17-10所示。
    ADD R0, R1, R2
    SXTH R0, R0
Or for unsigned halfwords as in Example 17-11.
    ADD R0, R1, R2
    BIC R0, R0, #0x10000
这具有将结果裁剪到定义大小的效果

        尽管编译器有时可以处理循环计数器变量的不正确类型说明等问题,但通常最好首先使用正确的类型。

5,指针别名

        如果一个函数有两个指针pa和pb,它们的值相同,我们说两个指针相互别名。这引入了指令执行顺序的约束。如果两个写访问以程序顺序发生,它们必须在处理器上以相同的顺序发生,并且不能被重新排序。这也是写后读,或读后写的情况。对别名的两次读访问可以安全地重新排序。因为C中的任何指针都可以别名任何其他指针,所以编译器必须假设通过这些指针访问的内存区域可以重叠,这就防止了许多可能的优化。c++支持更多的优化,因为如果指针参数指向不同的类型,它们将不会被视为可能的别名。

        C99引入了restrict关键字,该关键字指定一个特定的指针参数不能别名任何其他参数。如果您知道指针不重叠,那么使用这个关键字向编译器提供这个信息可以产生显著的改进。然而,滥用它会导致不正确的程序功能。restrict关键字限定了指针而不是被指向的对象。这并不是ARM架构特有的考虑。使用GCC时,可以通过在编译标志中添加-std= C99来启用C99标准。

        在不能用C99编译的代码中,可以使用__restrict或__restrict__来启用关键字作为GCC扩展。

考虑以下简单的代码序列:
void foo(unsigned int *ptr1, unsigned int *ptr2, unsigned int *i)
{
    *ptr1 += *i;
    *ptr2 += *i;
}
指针可能指向相同的内存位置,这导致编译器生成的代码效率较低。在这个例子中,它必须从内存中读取值*i两次,每次添加一次,因为它不能确定改变*ptr1的值不会改变*i的值。
如果函数被声明为:
void foo(unsigned int *restrict ptr1, unsigned int *restrict ptr2, unsigned int
*restrict i)
这意味着编译器可以假设这三个指针可能不指向相同的位置,并相应地进行优化。您必须确保指针不重叠。

6,除法和模

        并不是所有的ARM处理器都有硬件支持除法。对于这些处理器,C除法通常会调用一个库例程,对于32位整数的除法需要运行几十个周期,

        注意:即使在硬件上除法也比乘法慢。在性能关键的代码中,如果可能的话,几乎总是值得替换的。这必须以代码可维护性为代价。

        在可能的情况下,必须避免或从循环中删除除法。用一个固定的除数(即在编译时已知的除数)来除法比用两个变量除法要快。在这种情况下,编译器可以用移位-乘对替换除法。32 32乘以固定常数,然后右移以调整最重要的子。

模运算是另一种需要注意的情况,因为这也将使用除法库例程。
The code
    minutes = (minutes + 1) % 60;
将在没有硬件分割的机器上运行得更快, if coded as
    if (++minutes == 60) minutes=0;
用两个循环的add和compare代替对库函数的调用

7,外部数据

        访问外部变量需要处理器执行一系列的加载指令,通过基指针获取变量的地址,然后读取实际的变量值。如果多个变量被定义为一个结构的成员,它们可以共享一个基指针,从而节省周期和指令。因此,在同一个结构中定义变量是一种良好的实践。

8,内联与嵌入式汇编

        在某些情况下,它可以是一个有价值的优化使用汇编代码,除了c .这里的一般原则是对你在高级语言代码,使用一个分析器,以确定哪些部分将产生最大的好处如果优化,然后检查compiler-produced汇编代码寻找可能的改进。

        如果某个代码段被标识为性能瓶颈,则不要立即查阅汇编语言手册。在考虑使用汇编代码之前,应该首先寻求对算法的改进,然后尝试编译器优化。即便如此,性能低下的原因往往是缓存丢失和内存访问延迟,而不是实际的程序集代码。

        ARM编译器、GCC和大多数其他C编译器使用s标志来告诉编译器生成汇编代码输出。fverbose-asm命令行选项在gcc中也很有用。使用——interleave选项,ARM编译器可以生成交叉的源代码和汇编程序。

9,复杂寻址模式

        通常最好避免复杂的寻址模式。如果用于加载或存储的地址需要复杂的计算,则不可能进行双下发指令。只有使用基寄存器加偏移量(由寄存器或立即值指定),并可选地向左移动一个立即值为2的寻址模式是快速的。其他不太常用的寻址模式可以通过分割成两个可能是双重发出的指令来更快地执行。

For example:
    MOV R2, R1 LSL#3; LDR R2,[R0, R2]
can be faster than
    LDR R2, [R0, R1 LSL #3]
LDRH和LDRB没有额外的惩罚,但是LDRSH和LDRSB有一个单一的循环负载-使用惩罚,但是没有早期转发路径,并且如果后续指令使用所加载的值,可能会导致额外的延迟。

10,对齐访问

        与已对齐的loads相比,未对齐的ldr有额外的循环惩罚,但是跨缓存线的未对齐的ldr有许多额外的循环惩罚。一般来说,与loads相比,存储不太可能使系统陷入停顿。由于合并写缓冲区,STRB和STRH的性能与STR相似。因为在装载/存储单元中有四个槽,超过四个连续挂起的装载总是会导致管道停止。

11,链接优化

        一些代码优化可以在链接中执行,而不是在构建的编译阶段,例如,未使用的部分消除和链接器反馈。可以跨多个C文件进行多文件优化,可以删除未使用的部分。类似地,多文件编译使编译器能够跨多个文件而不是单个文件执行优化。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

嵌入式ARM算法移植与优化 的相关文章

  • Makefile详解——从入门到精通

    转自ChinaUnix xff0c 作者gunguymadman xff0c 陈皓 链接地址 xff1a http www chinaunix net jh 23 408225 html 陈皓 CSDN 博客 xff1a http blog
  • FreeRTOS事件标志组与事件标志位使用

    事件标志位 事件位用于指示事件是否发生 也叫做事件标志位 可用位表示 xff1a 当信息收到且准备好处理时 xff0c 设置为1 xff1b 当没有收到信息且等待处理时 xff0c 设置为0 事件标志组 事件标志组是一组事件位 xff0c
  • 结合图片看常用串口通信UART

    结合图片看常用串口通信UART UART是一种通用串行数据总线 xff0c 用于异步通信 该总线双向通信 xff0c 可以实现全双工传输和接收 在嵌入式设计中 xff0c UART用于主机与辅助设备通信 xff0c 如汽车音响与外接AP之间
  • 浅谈面向对象设计思想,以及在Linux内核中的体现

    面向对象编程 xff08 OOP xff09 xff0c 是一种设计思想或者架构风格 OO语言之父Alan Kay xff0c Smalltalk的发明人 xff0c 在谈到OOP时是这样说的 xff1a OOP应该体现一种网状结构 xff
  • STM32F4移植FreeRTOS

    在之前的文章 xff1a FreeRTOS全解析 2 源码结构与移植 中我们已经讲解过FreeRTOS移植的方法 xff0c 并且给出了一个在ARM9上移植的例子 xff0c 今天再来看一个例子 xff1a 板子的芯片为STM32F407
  • 彻底掌握FreeRTOS中的任务

    FreeRTOS是个操作系统 xff0c FreeRTOS的任务 task 其实就是像我们电脑中的一个独立程序 xff0c 表现在源码中 xff0c 其实就是一个函数 本文从学会使用任务 task 到理解逐步深入 参考资料 xff1a Ma
  • FreeRTOS任务之调度器中的三种调度算法

    本文主要讲解FreeRTOS调度器中的三种调度算法 xff1a 基于时间片的抢占式调度 不带时间片的抢占式调度和协同调度 前导文章 xff1a FreeRTOS全解析 3 任务 xff08 task xff09 参考资料 xff1a Mas
  • 彻底理解FreeRTOS中的队列(Queue)

    队列 xff08 Queue xff09 提供了任务与任务之间通信的机制 在这样的场景 xff1a 一个或多个其他的任务产生数据 xff0c 主任务要依次处理数据 xff0c 队列就显得非常有用了 参考资料 xff1a Mastering
  • 简单路由器DIY

    一 关于路由器 路由器 xff08 Route xff09 是一种负责寻径的系统 xff0c 在互连网络中从多条路径中寻找通讯量最少的一条网络路径提供给用户通信 它使用寻径协议来获得网络信息 xff0c 采用基于 34 寻径矩阵 34 的寻
  • DOS那一代的程序员现在都干嘛呢?

    亿友论坛 DOS那一代的程序员现在都干嘛呢 xff1f xff08 几年前的老帖子 xff09 作者 xff1a 包子夹蛋 发布时间 xff1a 2005 5 10 14 01 00 DOS那一代的程序员现在都干嘛呢 xff1f xff08
  • switch case的用法

    switch case的用法如下 1 Switch用在编程中 xff0c 如C语言中它经常跟Case一起使用 xff0c 是一个判断选择代码 其功能就是控制流程流转的 直线翻译 xff1a switch语句 xff0c 即 切换 语句 xf
  • Rust嵌入式:只使用寄存器为STM32点灯;从查手册开始

    为什么是 只使用寄存器 网上很多教程都是直接调用对应芯片的HAL库 xff0c 让初学者认为操作都被Rust嵌入式开源组封装好了 xff0c 直接调就好 这在目前Rust嵌入式生态还不成熟的情况下可能是个误解 实际上Rust嵌入式有自己独特
  • epoll_wait 中timeout参数的损耗和如何避免

    ccd 100qps xff0c dcc 4wqps
  • ubuntu和windows双系统默认启动顺序

    在安装了双系统后 xff0c 一般的默认启动ubuntu xff0c 但我实际可能使用windows会比较多 xff0c 为解决这个问题 xff0c 必须要更改默认顺序 xff0c 以达到方便我们使用的效果 方法 xff1a 开机界面会有显
  • 深度学习优化算法

    深度学习各类优化器 借用古代炼丹的一些名词 xff0c 我们可以把训练模型中的数据比做炼丹药材 xff0c 模型比做炼丹炉 xff0c 火候比做优化器 那么我们知道 xff0c 同样的药材同样的炼丹炉 xff0c 但是火候不一样的话 xff
  • yolov3

    YOLOv3 YOLOv3的总体网络架构图 可根据官方代码中的yolov3 cfg进行一一对应 xff0c 标号 0 0 0 是第一个 convolutional Darknet 53 和DarkNet 19一样 xff0c 同样下采样32
  • SSH远程连接报错:kex_exchange_identification: Connection closed by remote host

    突然出现问题 xff1a kex exchange identification Connection closed by remote host 解决方法 删除known hosts xff0c 重启终端即可
  • Dronekit——Introduction

    Dronekit Introduction 参考 xff1a http python dronekit io about overview html 关于Dronekit DroneKit Python允许开发人员创建 在板载协同计算机上运
  • Run dronekit scripts by SITL on Linux 执行dronekit 脚本在Linux系统下模拟飞行

    参考 xff1a http ardupilot org dev docs setting up sitl on linux html Overview The SITL simulator allows you to run Plane C
  • SLAM从入门到放肆——SLAM简述

    SLAM 同时定位与建图 xff08 simultaneous localization and mapping xff0c SLAM xff09 是自动驾驶与增强现实领域中常用的技术 主要研究装置通过各种传感器在未知环境中的感知与定位问题

随机推荐

  • 关于NoDriveTypeAutoRun的键值

    最近在写一个修改注册表的驱动 xff0c 只是为了好玩 xff0c 也算是一个恶搞驱动吧 xff01 遇到一个问题 xff0c 花了我差不多两天的时间的 xff0c 详情往下看 写这篇文章 xff0c 为了记住这个教训 xff0c 一个提供
  • 闭包的实现原理和作用、以及内存泄露

    一 闭包的实现原理和作用 xff0c 可以列举几个开发中闭包的实际应用 1 闭包的概念 xff1a 指有权访问另一个函数作用域中的变量的函数 xff0c 一般情况就是在一个函数中包含另一个函数 2 闭包的作用 xff1a 访问函数内部变量
  • PX4 SITL Gazebo 仿真时 libgazebo_multirotor_base_plugin 插件运行时出错

    PX4 SITL Gazebo 仿真时 libgazebo multirotor base plugin 插件运行时出错 问题描述原因分析解决办法总结 问题描述 在 Gazebo 中进行 PX4 的软件在环仿真时 xff0c 执 make
  • Jetson AGX Xavier 正确上手教程

    概览 1 开机1 1 配件1 2 需要准备的器材1 3 连接设备 2 刷机2 1 更换主机的 apt get 源2 2 在主机上安装 SDK ManagerStep 1Step xff12 Step xff13 烧录 OS 镜像安装 SDK
  • Matplotlib绘图显示缺少中文字体-RuntimeWarning: Glyph 8722 missing from current font.

    pyplot 并不默认支持中文显示 xff0c 也没有自带中文字体 xff0c 因此需要自行下载所需字体 xff0c 并修改 rcParams 参数来显示中文 下面以 SimHei xff08 黑体 xff09 字体为例进行说明 说明 xf
  • Win10 系统安装 Linux 子系统教程(WSL2 + Ubuntu 20.04 + Gnome 桌面 )

    Win10 系统安装 Linux 子系统教程 WSL2 43 Ubuntu 20 04 43 Gnome 桌面 xff09 1 WSL 简介1 1 什么是 WSL xff1f 1 2 WSL1 与 WSL2 2 安装 WSL2 1 安装 W
  • Ubuntu 桌面美化教程

    将 Ubuntu 桌面美化成 Mac 风格 xff0c 效果如下 1 美化任务栏 Ubuntu 20 04 默认的任务栏在桌面左侧 xff0c 不使用时会自动隐藏 安装 plank dock 工具可以在桌面底部设置一个常驻任务栏 xff08
  • Git 图解教程

    Git 图解教程 基础篇 git 简介 安装与配置 安装 git 配置 git 创建版本库 跟踪修改 提交修改 比较文件 辅助命令 创建分支 合并分支 git merge git rebase 高级篇 移动提交记录 分离的 HEAD 相对引
  • Doxygen + Graphviz 代码自动化分析

    Doxygen 43 Graphviz 代码自动化分析 目录 1 实际需求2 工具简介3 使用说明4 代码注释规范5 参考 1 实际需求 在开发程序时 xff0c 需要编写对应的说明文档 在阅读现有的项目源代码时 xff0c 需要梳理函数间
  • Microsoft Learn: Docker入门教程

    Microsoft Learn Docker入门教程 1 Introduction2 What is Docker3 How Docker images work4 How Docker containers work5 When to u
  • 将kitti数据集中的velodyne points转换为ROS bag文件

    kitti数据集中包含了相机图像 激光扫描得到的点云信息 高精度GPS测量信息和IMU加速度信息 xff0c 用于移动机器人与自动驾驶方面的研究 如下为数据采集平台示意图 kitti数据集中的激光扫描数据是以二进制文件形式存储的 xff0c
  • 使用 Dockerfile 创建一个简单的容器

    Dockerfile Dockerfile 是用于指导 docker 创建自定义 image 的一系列指令 xff0c 是用于创建 image 的蓝图 现在有一个简单的 node 项目 其中Dockerfile 是后面加的 xff0c 初始
  • 导航英语专业词汇——不停更新

    惯性导航 inertia 惯性INS 惯性导航系统GINS 平台惯性导航系统SINS 捷联惯性导航系统IMU 惯性导航元件gyroscope 陀螺仪accelerometer 加速度计 SLAM 词汇词性意思 导航中用 SLAM词组同步定位
  • 不止于linux SSH 基本用法-正反向代理-内外网穿透

    最近小伙伴们纷纷进了实验室 xff0c 就冒出了一系列关于控制远程机器的问题 xff0c 我觉得我还是有必要科普一下的 约定 本文不讲解 Linux 使用方法 xff0c 只讲解机器之间的通信方法 下文中行首的 local 以及 remot
  • 传感器数据滤波算法

    嵌入式应用中 xff0c 系统获取的传感器数据通常不能够直接供应用使用 xff08 存在噪声干扰 xff1a 低频噪声或高频噪声 xff09 xff0c 一般通过一种或者多种滤波算法结合 xff0c 对原始数据进行滤波处理 xff0c 在保
  • 写了一个生成reStructuredText表格的vim插件

    vim官网上有一个rst table xff0c 不过不太好用 xff0c 最大的问题是对中文的支持有欠缺在 f后 xff0c 中文所在的cell的长度会计算错误 扫了一眼它的源码使用了python写的插件 xff0c 所以我就心血来潮也想
  • Ubuntu 16.04 一系列软件安装命令,包括QQ、搜狗、Chrome、vlc、网易云音乐安装方法

    1 简介 Ubuntu 16 04安装完后 还需要做一些配置才能愉快的使用 包括添加软件源 安装搜狗输入法 Chrome浏览器 网易云音乐 配置快捷键 安装git等等 下面就跟着我来配置吧 just do it 2 版本选择 如果你是小白
  • 从入门到放弃之生产环境基于Kubernetes V1.21.10搭建高可用集群

    1 资源规划 名称系统配置IP组件k8s master01CentOS 7 94核8G 500G存储172 16 97 27kube apiserver lt br gt kube controller manager lt br gt k
  • ros c++ 开发错误“集锦”

    集锦 xff0c 才能经常回放 1 error s getMD5Sum is not a member2 ROS datatype md5sum错误 持续更新 1 error s getMD5Sum is not a member 目前观测
  • 嵌入式ARM算法移植与优化

    目录 一 算法优化指导思想 1 算法优化基本原则 2 算法优化方法 二 编译器优化 1 xff0c 函数内联 2 xff0c 消除公共子表达式 3 xff0c 循环展开 4 xff0c GCC优化选项 5 xff0c ARMCC优化选项 三