如何减少因子循环的执行时间和周期数?和/或代码大小?

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何减少因子循环的执行时间和周期数?和/或代码大小?相关的知识,希望对你有一定的参考价值。

基本上我很难让执行时间低于它,以及减少时钟周期和内存大小。有谁知道如何做到这一点?代码工作正常我只想稍微改变它。

写了一个工作代码,但不想弄乱代码,但也不知道要做什么改动。

; Calculation of a factorial value using a simple loop

; set up the exception addresses
THUMB
AREA RESET, CODE, READONLY
EXPORT  __Vectors
EXPORT Reset_Handler
__Vectors 
DCD 0x00180000     ; top of the stack 
DCD Reset_Handler  ; reset vector - where the program starts

AREA 2a_Code, CODE, READONLY
Reset_Handler
ENTRY
start   
MOV r1,#0    ; count the number of multiplications performed 
MOV r2,#3    ; the final value in the factorial calculation
MOV r3,#1    ; the factorial result will be stored here

; loop r2 times forming the product  
fact
ADD r1,r1,#1  ; find the next multiplicand
MUL r3,r1,r3  ; form the next product - note that MUL r3,r3,r1 gives unpredictable output
CMP r1,r2     ; check if the final value has been reached
BMI fact      ; continue if all products have not been formed

exit    ; stay in an endless loop 
B exit
END

当前结果为:内存大小:0x00000024时钟周期:22总执行时间:1.1微秒

我们正在使用Cortex M3

我只需要减少其中的任何一项,只要它产生不同的结果,代码的更改就可以很小。

答案

代码大小和性能通常是一种权衡。展开循环通常有助于提高性能(至少对于大输入),但需要在循环外部使用额外的逻辑来处理清理等等。


Most of this answer was assuming a higher-performance CPU like Cortex-A9 or Cortex-A53 where software pipelining to create instruction-level parallelism would be helpful. Cortex M3 is scalar and has a single-cycle multiply instruction, making it much simpler to optimize for.

(最初的问题没有指定核心,我期待即使是低端CPU也会有多周期mul延迟。我写完后才发现Cortex-M3数字。)

您的代码可能会遇到整数乘法延迟的瓶颈。与add不同,结果将在下一个周期准备就绪,mul很复杂,需要多个周期才能产生结果。

(除了一些非常慢的时钟芯片,就像显然Cortex-M3有一个1周期的mul指令。但该指令的Cortex-M0/M0+/M23 are available with a choice为1个周期或32个周期性能!慢迭代=较小的硅。)


乘法执行单元本身通常是流水线的,因此多个独立的乘法可以同时处于飞行状态,但是你的阶乘循环需要每个乘法结果作为下一次迭代的输入。 (仅适用于性能更高的内核,而不是Cortex-M系列。慢速皮质-M芯片上的32周期乘法是迭代的,可能不是流水线的,因此在运行时无法启动另一个乘法,并且没有任何好处暴露任何指令级并行性,除了减少循环开销。)

请注意,乘法是关联的:1 * 2 * 3 = 3 * 2 * 1,所以我们可以从n倒数,因为@ ensc的答案指出。或者(1*2) * (3*4) = 1*2*3*4

我们可以改为与1 * 2 * ... * (n/2)并行执行n/2+1 * n/2+2 * n/2+3 * ... * n,在这两个依赖链上交错工作。或者我们可以将1 * 3 * 5 * ... * n2 * 4 * 6 * ... n-1交错,在一个循环中执行n -= 2并从中计算n+1。 (然后在最后,你将这两个产品相乘)。

这显然需要更多的代码大小,但可以帮助提高性能。


当然,查找表是另一种解决方法。如果您只关心不会溢出32位结果的输入,那么这是一个非常小的表。但这具有相当大的成本。


即使在有序CPU(其中指令执行必须以程序顺序开始),也可以允许诸如高速缓存未命中加载或乘法之类的长时间运行的指令无序地完成,例如,一些add指令可以在启动mul之后但在mul结果被写回之前运行。甚至在早期的mul延迟的阴影下开始另一个独立的mul指令。

我搜索了一些ARM性能数据,以便了解一下典型的情况。

例如,Cortex-A9是较旧的相当常见的高端ARMv7 CPU,它是超标量(每个周期多个指令),具有无序执行。

mul "takes" 2 cycles, and has 4 cycle result latency。他们没有解释非延迟成本的含义。也许这就是执行单元的倒数吞吐量,就像你开始新的独立操作的频率一样。它是一个无序的CPU,因此它无法将其他指令停止2个周期。在NEON SIMD instruction section中,他们解释了看起来像“周期”的数字:

这是特定指令消耗的发布周期数,如果不存在操作数互锁,则是每条指令的绝对最小周期数。

(操作数互锁=如果先前的指令尚未产生结果,则等待输入操作数准备就绪)。

(Cortex-A9确实支持打包整数乘法,所以对于大因子,你可以看看并行4次乘法,每4个周期开始一个向量,使用vmul.32 q1, q1, q2。或者每2个周期使用64位d寄存器2次,但是你会需要更多vadd指令,与乘法不同,vadd.32与128位q reg一样快,与64位向量一样快。因此,如果使用足够的寄存器来隐藏,SIMD可以为Cortex-A9提供两倍的标量乘法吞吐量但是,SIMD可能只对n非常有用,以至于n!会溢出一个32位整数,所以你得到一个模数为2 ^ 32的结果。)


Lower latency ARM multiply instructions:

mul是32x32 => 32位乘法。在Cortex-A9上,它具有2c吞吐量和4c延迟。

muls是拇指模式下的16位指令,除非你不需要破坏标志,否则应该是首选。拇指模式下的mul仅在ARMv6T2及更高版本中可用。)

smulbb是16x16 => 32位有符号乘法,仅读取其输入的低半部分,但在A9上具有1c吞吐量和3c延迟。 (BB =底部,底部。其他组合也可用,以及乘法累积和各种时髦的东西。)

smulxy没有2字节的Thumb版本,所以对于代码大小来说这比muls更糟糕。

不幸的是smulxy在无符号版本中不可用,因此限制了我们可以使用的输入范围到正int16_t,而不是uint16_t

但是如果我们只关心最终的32位结果不会溢出的情况,我们可以安排我们的操作顺序,这样最后一个乘法就有2个相似幅度的输入(两个都是大16位数)。即尽可能接近sqrt(n!)。所以例如赔率和均衡的乘积是合理的,但(n-1)! * n将是最坏的情况,因为这将需要(n-1)!适合16位。实际上最糟糕的情况是从n倒数,所以最后一个是乘以3然后2.我们可以特殊情况下乘以2到左移......


把这些碎片放在一起,注意乘以1是一个无操作(除了smulbb,它将输入截断为16位)。所以我们可以在一个乘以1或2后停止的方式展开,具体取决于输入是奇数还是偶数。

因此,不是知道哪个是奇数,哪个是偶数,我们只需要lo(从n-1开始)和hi(从n开始)。

;; UNTESTED, but it does assemble with the GNU assembler, after sed -i 's/;/@/' arm-fact.S
;; and replacing THUMB with
; .thumb
; .syntax unified
THUMB

;; Input: n in r0.   (n is signed positive, otherwise we return n.)
;; Output: n! in r0.
;; clobbers: r1, r2, r3
;; pre-conditions: n! < 2^31.  Or maybe slightly lower.
fact:
    subs   r3, r0, #3   ; r3 = lo = n-3  (first multiplier for loprod)
    bls   .Ltiny_input
    subs   r2, r0, #2   ; r2 = hi = n-2  (first multiplier for hiprod)
    subs   r1, r0, #1   ; r1 = loprod = n-1
                        ; r0 = hiprod = n

.Lloop:                 ; do 
    smulbb  r0,r0, r2      ; hiprod *= hi
    subs    r2, #2         ; hi -= 2 for next iter
    smulbb  r1,r1, r3
    subs    r3, #2         ; lo -= 2 for next iter
    bgt     .Lloop       ; while((lo-=2) > 0);  signed condition
    ; r3 = 0 or -1, r2 = 1 or 0.  The last multiplies were:
    ;       hiprod *= 2 and loprod *= 1  for even n
    ;   or  hiprod *= 3 and loprod *= 2  for odd n

    ; muls  r0, r1
    smulbb  r0,r0, r1      ; return  hiprod *= loprod

    bx lr    ; or inline this

.Ltiny_input:   ; alternate return path for tiny inputs
    ; r0 = n.   flags still set from  n - 3
    IT eq                  ; GAS insists on explicit IT for thumb mode
    moveq   r0, #6         ; 3! = 6, else n! = n for smaller n=1 or 2.
                           ; 0! = 1 case is not handled, nor are negative inputs
    bx lr

(标签名称中的.L使其成为未在目标文件中显示的本地标签,至少在GAS语法中。如果您使用的是汇编程序,可能不在ARMASM中。)

ARM程序集允许您在与第一个源相同时省略目标,对于某些指令(如subs但不是smulbb)。如果你愿意,你可以每次都像subs r2, r2, #2一样写出来。

您可以使用muls r0, r1作为最终产品,因为最终的hiprodloprod略高。即使hiprod> max int16_t,产品也可能不会溢出。这样可以节省2个字节的代码大小,但在Cortex-A9上增加了1个周期的延迟。 (顺便说一句,ARMv6使用mul d,d, src怪异修复了“不可预测的结果”,并且您的代码使用了32位Thumb2指令,因此它仅适用于ARMv6T2及更高版本。)


产品有2个累加器,在Cortex-A9上每3个周期可以运行2次,这在很大程度上取决于CPU微架构以及它的前端是否可以跟上。在有序ARM中,我担心它能够在乘法结束之前启动其他指令。

sub而不是subs上花费2个额外字节可能会更好,这样我们就可以在分支之前计算几个指令的标志,可能会减少分支误预测惩罚并避免有序CPU上的停顿。 smulbb没有触摸旗帜,所以我们可以先做loprod并让hi的东西不触摸旗帜。

.loop:                  ; do 
    smulbb  r1, r3       ; loprod *= lo
    subs    r3, #2       ; lo -= 2 for next iter, and set flags
    smulbb  r0, r2       ; hiprod *= hi
    sub     r2, #2       ; hi -= 2 for next iter (no flags)
    bgt     .loop       ; while((lo-=2) >= 0);

请注意,我们正在r3读取它们之后修改r2smulbb,避免为有序芯片的数据依赖性创建停顿。


您正在使用Thumb模式并针对代码大小进行优化,因此了解哪种形式的指令可以使用2字节/ 16位编码以及哪些只能用作32位Thumb2编码非常重要。

subs Rd, Rn, #imm can be encoded as a 16-bit Thumb instruction for imm=0..7(立即3位)。或者使用与src和destination相同的寄存器,对于imm = 0..255。所以我的复制和子指令是紧凑的。

除非在IT块内部或使用sub作为操作数,否则非标志设置SP不能是16位指令。

Thumb模式中的谓词指令,如moveq r0, #6,要求汇编器使用IT instruction为下一个最多4条指令引入预测。在ARM模式下,每条指令的前4位信号预测。 (如果不使用后缀,则汇编程序将其编码为ALways,即不是谓词。)

我们可以使用n==0 / cmp r0,#0处理另外4或6个字节的moveq r0, #1情况。如果我们将tst / mov放在同一个IT块中,可能会将其降低到4个字节。 IT不会对实际标记条件进行快照,它会对哪个谓词进行快照,因此IT块内的标志设置指令会对同一块中的后续指令产生影响。 (我认为这是对的,但我不是百分百肯定的)。

tiny_input:    ; r0 = n,  flags set according to n-3
    ITET EQ
    moveq  r0, #6
    cmpne  r0, #0
    moveq  r0, #1

或者有16-bit cbnz有条件地跳

以上是关于如何减少因子循环的执行时间和周期数?和/或代码大小?的主要内容,如果未能解决你的问题,请参考以下文章

魔兽自带地图编辑器触发事件中的循环函数和周期事件怎么设置啊 求高手指点

如何减少 cx_Freeze 编译的 Python 可执行文件大小?

仅在第一个周期中检查条件,在其余周期中执行一些代码

周期计数测量

如何实现滑动窗口或减少这些嵌套循环?

控制并行循环中的线程数并减少开销