我想计算一个以2为底的指数序列。例如,如果给定3,则程序将计算(2 ^ 3 + 2 ^ 2 + 2 ^ 1 + 2 ^ 0)。问题是我无法计算与之相乘的指数。
我尝试了shl
来计算指数,并且尝试了循环。均未产生正确的结果,下面是我的最新尝试。
.586
.MODEL FLAT
INCLUDE io.h
.STACK 4096
.DATA
Exponent DWORD ?
Prompt BYTE "Enter an exponent", 0
Base DWORD 2
string BYTE 40 DUP (?)
resultLbl BYTE "The solution is", 0
Solution DWORD 20 DUP (?), 0
.CODE
_MainProc PROC
input Prompt, string, 40 ; read ASCII characters (N)
atod string ; ascii to double
mov exponent, eax ; store in memory
push Exponent
push Base
call function
add esp, 8
dtoa Solution, eax ; convert to ASCII characters
output resultLbl, Solution ; output label and sum
mov eax, 0 ; exit with return code 0
ret
_MainProc ENDP
;@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
; Procedure takes base and exponent from main
Function PROC
push ebp ; stores base pointer
mov ebp, esp ; set base pointer to current position
mov ebx, [ebp + 12] ; read base (2)
mov ecx, [ebp + 8] ; read N
MainLoop: ; loop to calculate expoent
mul ebx ; multiply base by its self
add eax, ebx ; save answer in eax
dec ecx ; decrement the exponent
cmp ecx, 0 ; check if exponent is 0 yet
je exit ; Exit if exponent is 0
jg mainloop ; stay in loop if exponent is above 0
Exit:
pop ebp ; restore base pointer
ret
function ENDP
END
我希望程序产生正确的指数结果,并且如果可以计算上述序列,例如,如果给定3,则程序将计算(2 ^ 3 + 2 ^ 2 + 2 ^ 1 + 2 ^ 0)。
答案 0 :(得分:4)
Base 2是一个非常特殊的情况,因为计算机使用二进制整数。
2^n = 1 << n
,即2 n = 1个左移n
次。即该位的位置已设置。
您要输入2^0 + 2^1 + ... 2^n
。 一个数字,所有位都设置为包括一个n
位。
比2^(n+1)
小1,所以您要的号码是 2^(n+1) - 1
。
x86有一条指令来设置寄存器bts reg, reg
= Bit Test and Set中给定位置的位。在Intel CPU上,reg-reg形式解码为单个uop,其中1-周期延迟。 (https://agner.org/optimize/)还将位的旧值放入CF中,但通常我们对此并不关心。通常我们只将目标寄存器清零,然后使用BTS来设置单个位。
在Ryzen上,bts reg,reg
是2 oups,而可变计数移位是单uop,所以mov edx, 1
/ shl edx, cl
也很好。但这对于代码大小来说更糟,而在Intel CPU上则更多。
在Intel Sandybridge系列CPU上,shl reg, cl
解码为3 oups。 BMI2 shlx reg, reg, reg
是1 uop,但是不幸的是,BMI2支持还远远不能想象得到。
(带有寄存器源的内存目标BTS速度很慢,将源视为索引到任意长度的位字符串,而不仅仅是寻址的dword,因此通常应避免使用它来提高性能。)
2^(n+1) - 1
无溢出({C version of this answer on another question,带有编译器输出。)
如果我们在将输入的数字输入1之前将其添加到BTS,它可能会绕回而无法为31
工作(应该置位所有位,但会置位32%32 = 0
)
由于无论如何我们都需要在读取输入后需要一条额外的指令,因此我们可以使用x86的移位加法指令(LEA)再进行一次移位,因为我们减去1。因此,{{1 }}我们从高位开始,然后将其移至零。然后减去1即可根据需要设置所有位
n=31
其逻辑如下
xor edx,edx ; edx = 0
bts edx, eax ; edx |= 1<<eax
lea edx, [edx + edx - 1] ; edx = (edx<<1) - 1
最后一列是第二列的总和,并非巧合。
具有3个“组件”寻址模式的LEA与简单的LEA(例如, Sandybridge系列的3个周期延迟与1个周期相比。但这仍然是一个单一的因素,因此它是吞吐量的绝佳选择。
如果我们真的想优化,并且不必担心n BTS result (2^n) *2 - 1
0 -> 1 -> 1 = 2 - 1
1 -> 2 -> 3 = 4 - 1
2 -> 4 -> 7 = 8 - 1
3 -> 8 -> 15 = 16 - 1
...
30 -> 0x40000000 -> 0x7FFFFFFF = 0x80000000 - 1
31 -> 0x80000000 -> 0xFFFFFFFF = 0 - 1
大小写溢出,我们可以手动编写ASCII-> int循环,但以总计n=31
而不是1
,将0
折叠成n+1
。然后n
会给我们bts
,我们可以简单地2^(n+1)
。
无需将dec
存储到内存中并重新加载;您已经在想要的位置将其保存在寄存器中。
您在exponent
行上的注释是错误的,如果正确,您的代码将毫无意义。将ASCII转换为atod
(例如C函数double
)将以x87 strtod
或SSE2 st0
的形式返回,而不是EAX。 xmm0
实际上代表ASCII(十进制)到整数。也许atod
代表DWORD。
d
要实现 input Prompt, string, 40 ; read ASCII characters (N)
atod string ; ascii (decimal) to integer in EAX
xor edx,edx ; edx = 0
bts edx, eax ; edx |= 1<<eax
lea edx, [edx + edx - 1] ; edx = (edx<<1) - 1
dtoa Solution, edx
需要2条指令,因此将其放入自己的函数中是很愚蠢的。仅仅传递args +调用函数就需要像使用1<<n
那样的工作。
bts
编译器通常使用0 -> 1 -> 1
1 -> 2 -> 3
2 -> 4 -> 7
+ mov edx,1
+ mov ecx, eax
。但这是错过的优化,尤其是在Intel CPU上。
BMI2 shl edx, cl
会有所帮助(避免使用shlx edx, edx, ecx
),但是其代码大小比mov
-零+ xor
差。而且BMI2并不总是可用。
如果您不能使编译器使用BTS(或者您的SHLX使用BMI2),则另一个不错的实现是bts
,因此您可以将多余的移位烘烤成开始移位的数字。因此,(2ULL << n) - 1
的计数可以将位移出并产生0。