我正在使用Visual Studio 2013 Ultimate在MASM中编程汇编语言(x86)。我试图使用数组来计算一个斐波那契序列的n个元素使用数组。换句话说,我试图去一个数组元素,获得它前面的两个元素,把它们加起来,然后把结果存储在另一个数组中。
我在设置索引寄存器时遇到了问题。
我的程序设置如下:
TITLE fibonacci.asm
INCLUDE Irvine32.inc
.data
fibInitial BYTE 0, 1, 2, 3, 4, 5, 6
fibComputed BYTE 5 DUP(0)
.code
main PROC
MOVZX si, fibInitial
MOVZX di, fibComputed
MOV cl, LENGTHOF fibInitial
L1:
MOV ax, [si - 1]
MOV dx, [si - 2]
MOV bp, ax + dx
MOV dl, TYPE fibInitial
MOVZX si, dl
MOV [edi], bp
MOV dh, TYPE fibComputed
MOVZX di, dl
loop L1
exit
main ENDP
END main
我无法编译它,因为有一条错误消息说“error A2031:必须是MOV ebp, ax + dx
行的索引或基址寄存器”。然而,我确信还有其他我忽略的逻辑错误。
3条答案
按热度按时间f87krz0w1#
相关:Code-golf打印Fib的前1000位(10**9):my x86 asm answer使用扩展精度
adc
循环,并将二进制转换为字符串。内环针对速度进行了优化,其他部分针对尺寸进行了优化。计算Fibonacci sequence只需要保持两个状态:当前和前一个元素。我不知道你想用
fibInitial
做什么,除了计算它的长度。这不是perl,你在这里做for $n (0..5)
。我知道你们刚刚开始学习asm,但我还是要谈谈性能。学习asm without knowing what's fast and what's not的理由并不多。如果你不需要性能,让编译器从C源代码为你创建asm。另请参阅https://stackoverflow.com/tags/x86/info上的其他链接
使用状态寄存器可以简化在计算
a[1]
时需要查看a[-1]
的问题。您从curr=1
、prev=0
开始,然后从a[0] = curr
开始。要生成Fibonacci numbers的“现代”零开始序列,请从curr=0
,prev=1
开始。幸运的是,我最近正在考虑一个有效的Fibonacci循环,所以我花了时间写了一个完整的函数。请参阅下面的展开和矢量化版本(保存存储指令,但即使在32位CPU编译时也可以使64位int快速):
另一个循环条件可能是
AMD CPU可以融合cmp/分支,但不能融合dec/branch。英特尔CPU也可以macro-fuse
dec/jnz
。(或有符号小于零/大于零)。dec/inc
不会更新进位标志,所以你不能将它们与上面/下面的无符号ja/jb
一起使用。我认为这个想法是,你可以在一个循环中做一个adc
(带进位的加法),使用inc/dec
作为循环计数器,不干扰进位标志,但使用partial-flags slowdowns make this bad on modern CPUs。lea ecx, [eax + edx]
需要一个额外的字节(地址大小前缀),这就是我使用32位dest和64位地址的原因。(这些是64位模式下lea
的默认操作数大小)。对速度没有直接影响,只是通过代码大小间接影响。另一个循环体可以是:
展开循环进行更多的迭代将意味着更少的 Shuffle 。而不是
mov
指令,您只需跟踪哪个寄存器保存哪个变量。也就是说,你用一种寄存器重命名来处理赋值。展开的问题是,您需要清理任何剩余的奇数迭代。2的幂展开因子可以使清理循环稍微容易一些,但是加12并不比加16快。(请参阅本文的上一个版本,其中使用
lea
在第三个寄存器中生成curr + prev
,因为我没有意识到实际上并不需要临时变量。感谢rcgldr捕捉到了这一点。)下面是一个完整的工作展开版本,它可以处理任何计数。
测试前端(本版本新增:金丝雀元素,用于检测写入超过缓冲区末尾的ASM错误。)
这段代码是完全工作和测试(除非我错过了复制我的本地文件中的更改回答案>.<):
展开版本
再次感谢rcgldr让我思考如何处理奇数与甚至在循环设置中计数,而不是在结束时进行清理迭代。
我使用了无分支设置代码,它将4 * count%2添加到起始指针。它可以是零,但添加零比分支更便宜,看看我们是否应该。斐波那契序列很快就会溢出寄存器,因此保持序言代码紧凑和高效非常重要,而不仅仅是循环内的代码。(如果我们要优化的话,我们希望优化许多短长度的调用)。
要生成从零开始的序列,请执行以下操作
而不是现在
我们还可以在两个版本中保存
mov
指令,方法是使用esi
保存prev
,因为prev
依赖于count
。矢量化:
斐波那契数列不是特别可并行化的。没有简单的方法可以从F(i)和F(i-4)得到F(i+4),或者类似的东西。我们对向量所能做的就是减少存储到内存中的次数。开始:
然后
a+=b; b+=a; a+=b; b+=a;
产生:当使用两个64位整型打包成128 b向量时,这就不那么愚蠢了。即使在32位代码中,您也可以使用SSE进行64位整数运算。
此答案的以前版本有一个未完成的压缩32位向量版本,无法正确处理
count%4 != 0
。为了加载序列的前4个值,我使用了pmovzxbd
,所以当我只能使用4 B时,我不需要16 B的数据。得到第一个-1。将序列的1个值装入向量寄存器要容易得多,因为只有一个非零值要加载和混洗。没有必要进一步展开,dep链延迟限制了吞吐量,因此我们总是可以平均每个周期存储一个元素。减少uops中的循环开销可以帮助实现超线程,但这是非常次要的。
正如您所看到的,即使展开两个,处理所有的角落情况也是非常复杂的。它需要额外的启动开销,即使您试图优化它以将其保持在最低限度。很容易以大量的条件分支结束。
更新了main:
性能
对于
count
略低于8192(适合L1 d缓存),在我的Sandybridge i5- 2500 k上,展开2个非向量版本运行接近其理论最大吞吐量,即每个周期1个存储(每个周期3.5条指令)。8192 * 4 B/int = 32768 = L1缓存大小。在实践中,我看到~3.3至~3.4 insn /周期。不过,我用Linuxperf stat
计算整个程序,而不仅仅是紧循环。(注意repeat循环调用Fib函数,所以程序的大部分时间都花在了它上面,尽管有一些启动开销。不管怎样,没有什么必要再继续下去了。显然,在count=47之后,这不再是一个斐波那契序列,因为我们使用了uint32_t。但是,对于较大的
count
,吞吐量受到内存带宽的限制,低至~2.6 insn /周期。在这一点上,我们基本上是在看如何优化memset。使用
movdqu
存储(fib64_sse
)的64位整数版本以每周期3 insns(每两个时钟一个128 b存储)的速度运行,阵列大小约为L2缓存大小的1.5倍。(即./fib64 49152
)。随着阵列大小增加到L3高速缓存大小的更大部分,性能下降到L3高速缓存大小的3/4时每个周期约2 insn(每3个时钟存储一次)。在大小> L3高速缓存时,它达到每6个周期1次存储。因此,使用向量存储比使用标量存储更好,其中数组对于L1 d缓存来说太大,但可以放入L2缓存。
aydmsdu92#
考虑到fib(93)= 12200160415121876738是适合64位无符号整数的最大值,尝试优化它可能没有太多意义,除非计算fib(n)模某个(通常是素数)大n。
有一种方法可以在log2(n)次迭代中直接计算fib(n),使用lucas序列方法或Fibonacci的矩阵方法。lucas序列更快,如下所示。这些可以被修改以执行对某个数字取模的数学运算。
yks3o0rb3#