我正在ASM中使用NASM做一个有关帕斯卡三角形的项目
所以在项目中,您需要计算从0行到63行的pascal三角形
我的第一个问题是将计算结果存储在哪里->内存
第二个问题,我在内存中使用什么类型的存储,要理解我的意思,我有3种方法 首先 声明一个 完整的矩阵, 所以将像这样
memoryLabl: resd 63*64 ; 63 rows of 64 columns each
但是这样的问题是没有使用一半的矩阵,这使我的程序 效率不高, 所以让我们开始第二种方法
每行声明一个内存标签 ,例如:
line0: dd 1 line1: dd 1,1 line2: dd 1,2,1 ; with pre-filled data for example purposes ... line63: resd 64 ; reserve space for 64 dword entries
这样的操作就像手工操作一样
该类中的其他一些尝试使用宏,如您在此处看到的,但我不明白
到目前为止,一切都很好
让我们转到我使用的最后一个,就像第一个一样,但是我使用 三角矩阵 ,这是怎么回事,通过仅声明我需要的内存量
所以将0行到63行的Pascal三角形存储起来,这给了我一个三角形矩阵,因为我每增加一行新行
我已经为三角形矩阵分配了2080 dword,这是怎么回事?2080 dword解释:
okey we have line0 have 1 dword /* 1 number in first line */ line1 have 2 dword /* 2 numbers in second line */ line2 have 3 dword /* 3 numbers in third line */ ... line63 have 64 dword /* 64 numbers in final line*/ so in the end we have 2080 as the sum of them
我给每个数字1双字
好的,现在我们已经创建了存储结果的内存,让我们开始计算
*Pascal三角形中的 *first# , 第0行的 所有单元格的值都为1
我将用伪代码来完成它,以便您了解如何在第0行的所有单元格中放入一个:
s=0 for(i=0;i<64;i++): s = s+i mov dword[x+s*4],1 /* x is addresses of triangle matrices */
帕斯卡三角形的第二部分是 每行 的 最后一行 等于1
我将使用伪代码使其变得简单
s=0 for(i=2;i<64;i++): s = s+i mov dword[x+s*4],1
我从等于2开始,因为i = 0(i=1)是line0(line1),line0(line1)已满,因为仅持有一个(tow)值,正如我在上面的解释中所说的那样
所以两个伪代码将使我的矩形 在内存中 看起来像 :
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 ... 1 1
现在最困难的部分是使用该值在三角形中填充所有三角形单元格的计算
让我们从这里开始
let's take cell[line][row] we have cell[2][1] = cell[1][0]+cell[1][1] and cell[3][1]= cell[2][0]+cell[2][1] cell[3][2]= cell[2][1]+cell[2][2] in **general** we have cell[line][row]= cell[line-1][row-1]+cell[line-1][row]
我的问题 我无法使用ASM指令中断这种关系,因为我有一个
奇怪的三角形矩阵,谁能用一种关系或非常基本的伪代码或asm代码来帮助打破它?
TL:DR:您只需要按顺序遍历数组,因此不必计算索引。请参阅第二部分。
为了将索引 随机访问 到一个(较低的)三角形矩阵中 ,行r从一个size三角形开始r-1。大小的三角形n具有n*(n+1)/2全部元素,使用高斯公式计算从1到n-1的数字之和。因此,三角形的大小r-1包含(r-1)*r/2元素。一旦我们知道行开始的地址,在索引行中的列当然是很简单的。
r
r-1
n
n*(n+1)/2
(r-1)*r/2
每个DWORD元素的宽度均为4个字节,我们可以在进行乘法时将其缩放,因为[lea我们可以进行移位,加法以及将结果放入不同的寄存器中。我们简化n*(n-1)/2elements * 4 bytes / elem为n*(n-1) * 2 bytes。
lea
n*(n-1)/2elements * 4 bytes / elem
n*(n-1) * 2 bytes
上面的推理适用于基于1的索引,其中第1行有1个元素。如果要在计算之前在行索引上加1,以进行基于零的索引编制,就必须对此进行调整,因此我们需要带r+1 - 1行的三角形的大小,即r*(r+1)/2*4bytes。它有助于将线性数组索引放入三角形中,以便快速重新检查公式
r+1 - 1
r*(r+1)/2*4bytes
0 4 8 12 16 20 24 28 32 36 40 44 48 52 56 60 64 68 72 76 80 84 88 92 96 100 104 108
第四行(我们称为“行3”)从整个数组的开头开始24个字节。那是(3+1)*(3+1-1) * 2= (3+1)*3 * 2; 是的,该r*(r+1)/2公式有效。
(3+1)*(3+1-1) * 2
(3+1)*3 * 2
r*(r+1)/2
;; given a row number in EDI, and column in ESI (zero-extended into RSI) ;; load triangle[row][col] into eax lea ecx, [2*rdi + 2] imul ecx, edi ; ecx = r*(r+1) * 2 bytes mov eax, [triangle + rcx + rsi*4]
假设32位绝对寻址是可以的(x86-64Linux中不再允许使用32位绝对地址?)。如果不是,请使用相对RIP的LEA来获取triangle寄存器中的基地址,并将其添加到中rsi*4。当x86寻址模式之一为常数时,只能具有3个分量。但是,对于static 来说 _就是_这种情况triangle,因此我们可以通过对列使用缩放索引,将base作为我们计算出的行偏移量以及将实际数组地址作为置换来充分利用。
triangle
rsi*4
这里的窍门是,您只需要顺序地遍历它即可 ;您不需要随机访问给定的行/列。
您在写下面的一行时阅读了一行。 当您到达一行的末尾时,下一个元素就是下一行的开始。 当您沿行向下移动时,源指针和目标指针之间的距离会越来越远,因为目标位置始终位于整行的前1行。而且您知道行的长度=行号,因此实际上可以将行计数器用作偏移量。
global _start _start: mov esi, triangle ; src = address of triangle[0,0] lea rdi, [rsi+4] ; dst = address of triangle[1,0] mov dword [rsi], 1 ; triangle[0,0] = 1 special case: there is no source .pascal_row: ; do { mov rcx, rdi ; RCX = one-past-end of src row = start of dst row xor eax, eax ; EAX = triangle[row-1][col-1] = 0 for first iteration ;; RSI points to start of src row: triangle[row-1][0] ;; RDI points to start of dst row: triangle[row ][0] .column: mov edx, [rsi] ; tri[r-1, c] ; will load 1 on the first iteration add eax, edx ; eax = tri[r-1, c-1] + tri[r-1, c] mov [rdi], eax ; store to triangle[row, col] add rdi, 4 ; ++dst add rsi, 4 ; ++src mov eax, edx ; becomes col-1 src value for next iteration cmp rsi, rcx jb .column ; }while(src < end_src) ;; RSI points to one-past-end of src row, i.e. start of next row = src for next iteration ;; RDI points to last element of dst row (because dst row is 1 element longer than src row) mov dword [rdi], 1 ; [r,r] = 1 end of a row add rdi, 4 ; this is where dst-src distance grows each iteration cmp rdi, end_triangle jb .pascal_row ;;; triangle is constructed. Set a breakpoint here to look at it with a debugger xor edi,edi mov eax, 231 syscall ; Linux sys_exit_group(0), 64-bit ABI section .bss ; you could just as well use resd 64*65/2 ; but put a label on each row for debugging convenience. ALIGN 16 triangle: %assign i 0 %rep 64 row %+ i: resd i + 1 %assign i i+1 %endrep end_triangle:
我对此进行了测试,它的工作原理是:正确设置内存中的值,并将其停在正确的位置。但是请注意,整数溢出会在您到达最后一行之前发生。如果使用64位整数(简单地更改注册名称和偏移,同时不要忘记这将避免resd对resq)。64选择32是1832624140942590534 = 2 ^ 60.66。
resd
resq
从我的答案到您所链接的有关宏的问题的答案,%rep到保留空间并将每一行标记为row0,row1等的障碍,比其他IMO答案更为理智。
%rep
row0
row1
您标记了此NASM,所以我使用了它,因为我对此很熟悉。您在问题中使用的语法是MASM(直到最后一次编辑)。MASM中的主要逻辑是相同的,但请记住,您需要使用OFFSET三角形来立即获取地址,而不是从地址中加载。
我使用x86-64是因为32位已过时,但是我避免了太多寄存器,因此您可以根据需要轻松地将其移植到32位。如果将其放入函数而不是独立程序中,请不要忘记保存/恢复调用保留的寄存器。
展开内部循环可以节省一些指令,用于在寄存器周围复制寄存器以及循环开销。这是一个经过某种程度优化的实现,但我主要将其限制在使代码 更简单 ,更小/更快的优化上。(除了可能使用指针增量而不是索引。)花了一些时间使它简洁明了。:P
在不同的CPU上,执行数组索引的不同方法会更快。例如dst,对于内部循环中的负载,可能使用索引寻址模式(相对于),因此仅需要一个指针增量。但是,如果您希望它快速运行,那么SSE2或AVX2 vpaddd可能会很好。与洗牌palignr可能是有用的,但也可能不对齐的负载,而不是一些洗牌,特别是AVX2或AVX512。
dst
vpaddd
palignr
但是无论如何,这是我的版本;我并不是想按照自己的方式来编写它,而是需要为任务分配自己编写的内容。我正在为将来的读者写信,他们可能会了解x86的高效功能。
我是怎么写的 :
我从顶部开始编写代码,但是很快意识到,一次过的错误将非常棘手,我不想只用循环内部分支的愚蠢方式编写代码,以应对特殊情况。
最终的帮助是在内部循环的指针上写了前置条件和后置条件的注释。这清楚地表明,我需要使用eax=0而不是使用进入循环,eax=1并将eax存储为循环内的第一个操作,或类似的东西。
eax=0
eax=1
显然,每个源值仅需要读取一次,因此我不想编写一个内部读取[rsi]和[rsi+4]类似内容的循环。此外,这将使更难确定正确的边界条件(不存在的值必须读取为0)。
[rsi]
[rsi+4]
在我最终只对整个三角形使用结束指针之前,花了一些时间来决定是否要在寄存器中用于行长或行号的计数器。在我完成操作之前,使用纯指针增量/比较并不会节省这么多指令(当上限是诸如的构建时常数时会进行寄存器注册)并不清楚end_triangle,但是效果很好。
end_triangle