在我的汇编程序中,我试图计算方程(2^0+2^1)*2^2)+2^3)*2^4)+2^5)

  • 本文关键字:汇编程序 方程 计算 math assembly x86
  • 更新时间 :
  • 英文 :


在我的80x86汇编程序中,我试图计算(2^0+2^1)*2^2)+2^3)*2^4)+2^5)。。。(2^n),其中每个偶数指数前面都有一个乘法,每个奇数指数前面都是一个加号。我有代码,但我的结果与期望的结果不断偏离。当n加上5时,结果应该是354,但我得到了330。

如有任何建议,我们将不胜感激。

.586
.model flat
include io.h
.stack 4096
.data
number dword ?
prompt byte "enter the power", 0
string byte 40 dup (?), 0
result byte 11 dup (?), 0
lbl_msg byte "answer", 0
bool dword ?
runtot dword ?
.code
_MainProc proc
input prompt, string, 40
atod string
push eax

call power

add esp, 4
dtoa result, eax
output lbl_msg, result
mov eax, 0
ret
_MainProc endp
power proc
push ebp
mov ebp, esp
push ecx
mov bool, 1     ;initial boolean value
mov eax, 1
mov runtot, 2   ;to keep a running total
mov ecx, [ebp + 8]
jecxz done
loop1:
add eax, eax        ;power of 2
test bool, ecx      ;test case for whether exp is odd/even
jnz oddexp          ;if boolean is 1
add runtot, eax     ;if boolean is 0
loop loop1
oddexp:
mov ebx, eax        ;move eax to seperate register for multiplication
mov eax, runtot     ;move existing total for multiplication
mul ebx             ;multiplication of old eax to new eax/running total
loop loop1
done:
mov eax, runtot     ;move final runtotal for print
pop ecx
pop ebp
ret


power endp

end

您使用静态变量和分支使代码过于复杂。

这些是2的幂,您可以(也应该)只左移n,而不是实际构造2^n并使用mul指令。

add eax,eax是乘以2(也就是左移1)的最佳方式,但目前还不清楚为什么要对EAX中的值这样做。它要么是乘法结果(您可能应该在mul之后将其存储回runtot),要么是偶数迭代后左移1的结果。

如果您试图创建一个2^i变量(使用强度降低优化,每次迭代偏移1,而不是偏移i),那么您的错误是在oddexp块中使用mul及其设置来破坏EAX。

正如Jester所指出的,如果第一个loop loop1失败,它将失败为oddexp:。当你在做环尾复制时,一定要考虑到如果环真的在那里结束,每个尾的穿透会在哪里。


拥有一个名为bool的静态变量也没有意义,该变量包含一个1,您只将其用作test的操作数。这向人类读者暗示,面具有时需要改变;CCD_ 16作为检查低位是否为零/非零的一种方式要清楚得多。

您也不需要runtot的静态存储,只需使用一个寄存器(就像EAX一样,您希望最终得到结果)。32位x86有7个寄存器(不包括堆栈指针)。


这就是我的做法。未经测试,但我通过展开2简化了很多。然后奇数/偶数的测试就结束了,因为交替模式被硬编码到循环结构中。

我们在循环中增加并比较/分支两次,所以展开并没有消除循环开销,只是将其中一个循环分支更改为if() break,它可以从中间离开循环。

这是而不是写这篇文章最有效的方式;循环中间的增量和早出检查可以通过从n向下计数另一个计数器来优化,并且如果剩余的步骤少于2步,则离开循环。(然后在后记中整理)

;; UNTESTED
power proc   ; fastcall calling convention: arg: ECX = unsigned int n
; clobbers: ECX, EDX
; returns: EAX
push  ebx           ; save a call-preserved register for scratch space
mov   eax, 1        ; EAX = 2^0   running total / return value
test  ecx,ecx
jz    done
mov   edx, ecx      ; EDX = n
mov   ecx, 1        ; ECX = i=1..n loop counter and shift count
loop1:                  ; do{   // unrolled by 2
; add 2^odd power
mov   ebx, 1
shl   ebx, cl         ; 2^i         ; xor   ebx, ebx; bts   ebx, ecx
add   eax, ebx        ; total += 2^i
inc   ecx
cmp   ecx, edx
jae   done            ; if (++i >= n) break;
; multiply by 2^even power
shl   eax, cl       ; total <<= i;  // same as total *= (1<<i)
inc   ecx           ; ++i
cmp   ecx, edx
jb    loop1         ; }while(i<n);
done:
pop  ebx
ret

我没有检查添加奇数幂阶是否会在另一位中产生进位。我认为它不是,所以将它实现为bts eax, ecx(设置位i)可能是安全的。实际上是OR而不是ADD,但只要该位之前被清除,这些都是等效的。

为了使asm看起来更像源代码并避免模糊的指令,我用shl实现了1<<i,为total += 2^i生成2^i,而不是在Intelxor ebx,ebx/bts ebx, ecx上更高效。(在Intel Sandybridge系列上,由于x86标志处理遗留行李,可变计数偏移为3 uop:如果计数=0,则标志必须保持不变)。但AMD Ryzen的情况更糟,bts reg,reg是2个单位,而shl reg,cl是1个单位。


更新:i=3在加法时确实产生进位,因此我们不能对这种情况下的位进行OR或BTS运算。但是,通过更多的分支可以进行优化

使用calc:

; define shiftadd_power(n) { local res=1; local i; for(i=1;i<=n;i++){ res+=1<<i; i++; if(i>n)break; res<<=i;} return res;}
shiftadd_power(n) defined
; base2(2)
; shiftadd_power(0)
1 /* 1 */
...

前几个输出是:

n          shiftadd(n) (base2)
0                   1
1                  11
2                1100
3               10100     ; 1100 + 1000 carries
4           101000000
5           101100000     ; 101000000 + 100000 set a bit that was previously 0
6     101100000000000
7     101100010000000     ; increasing amounts of trailing zero around the bit being flipped by ADD

剥离前3次迭代将启用BTS优化,您只需设置比特,而不是实际创建2^n并添加。

我们可以对较大n的i=3的起点进行硬编码,并优化计算n<3情况返回值的代码,而不是剥离它们。我提出了一个基于0b1100位模式右移3、2或0的无分支公式。

还要注意,对于n>=18,最后一个移位计数严格大于寄存器宽度的一半,并且奇数i的2^i没有低位。因此,只有最后的1或2次迭代才能影响结果。它可以归结为奇数n1<<n,或者偶数n0。这简化为(n&1) << n

对于n=14..17,最多设置2个比特。从result=0开始并进行最后3或4次迭代应该足以获得正确的总数。事实上,对于任何n,我们只需要进行最后的k迭代,其中k足以使偶数i的总移位计数>=32。由先前迭代设置的任何比特都被移出。(我没有为这个特殊情况添加分支。)

;; UNTESTED
;; special cases for n<3, and for n>=18
;; enabling an optimization in the main loop (BTS instead of add)
;; funky overflow behaviour for n>31: large odd n gives 1<<(n%32) instead of 0
power_optimized proc
; fastcall calling convention: arg: ECX = unsigned int n <= 31
; clobbers: ECX, EDX
; returns: EAX
mov   eax, 14h      ; 0b10100 = power(3)
cmp   ecx, 3
ja    n_gt_3        ; goto main loop or fall through to hard-coded low n
je    early_ret
;; n=0, 1, or 2  =>  1, 3, 12  (0b1, 0b11, 0b1100)
mov   eax, 0ch      ; 0b1100  to be right-shifted by 3, 2, or 0
cmp   ecx, 1        ; count=0,1,2 => CF,ZF,neither flag set
setbe cl            ; count=0,1,2 => cl=1,1,0
adc   cl, cl        ;                   3,2,0  (cl = cl+cl + (count<1) )
shr   eax, cl
early_ret:
ret
large_n:                ; odd n: result = 1<<n.  even n: result = 0
mov   eax, ecx
and   eax, 1        ; n&1
shl   eax, cl       ; n>31 will wrap the shift count so this "fails"
ret                 ; if you need to return 0 for all n>31, add another check
n_gt_3:
;; eax = running total for i=3 already
cmp   ecx, 18
jae   large_n
mov   edx, ecx      ; EDX = n
mov   ecx, 4        ; ECX = i=4..n loop counter and shift count
loop1:                  ; do{   // unrolled by 2
; multiply by 2^even power
shl   eax, cl       ; total <<= i;  // same as total *= (1<<i)
inc   edx
cmp   ecx, edx
jae   done            ; if (++i >= n) break;
; add 2^odd power.  i>3 so it won't already be set (thus no carry)
bts   eax, edx      ; total |= 1<<i;
inc   ecx           ; ++i
cmp   ecx, edx
jb    loop1         ; }while(i<n);
done:
ret

通过使用BTS在EAX中设置一个位,避免了在中构造1<<i需要额外的暂存寄存器,因此我们不必保存/恢复EBX。所以这是一个小的奖金节省。

请注意,这一次进入主循环的是i=4,它是偶数,而不是i=1。所以我换了加法和移位。

我仍然没有抽出时间将cmp/jae从循环的中间拉出。类似lea edx, [ecx-2]而不是mov的东西会设置循环退出条件,但需要进行检查以在i=4或5时根本不运行循环。对于大计数吞吐量,许多CPU可以每2个时钟维持1个已取+1个未取分支,不会产生比循环承载的dep链(通过eaxecx)更糟糕的瓶颈。但分支预测将有所不同,它使用更多的分支顺序缓冲区条目来记录更多可能的回滚/快速恢复点。

最新更新