【问题标题】:algorithm of addressing a triangle matrices memory using assembly使用汇编寻址三角矩阵内存的算法
【发布时间】:2018-03-08 04:58:25
【问题描述】:

我在 ASM 中做一个关于使用 NASM 的帕斯卡三角形的项目

所以在项目中你需要计算从第0行到第63行的帕斯卡三角形

我的第一个问题是在哪里存储计算结果 -> 内存

第二个问题我在内存中使用什么类型的存储,要理解我的意思我有3种方式首先声明一个完整矩阵所以会像这样 p>

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

这种做法就像手工做一样,

班上的其他人尝试使用宏,如您所见here 但我不明白

到目前为止一切顺利

让我们看看我用过的最后一个 这就像第一个,但我使用 triangle matrices ,那是怎么回事, 通过仅声明我需要的内存量

所以要存储帕斯卡三角形的第 0 行到第 63 行,它给了我一个三角形矩阵,因为每一个新行我都添加一个单元格

我已经为三角形矩阵分配了 2080 个双字,这是怎么回事? 用 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 个 dword

好的,现在我们已经创建了内存来存储结果让我们开始计算

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

我从 i 等于 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 代码来破解它?

【问题讨论】:

  • memoryLabl dd 63*64 dup(?) 是 MASM 语法。在 NASM 中:section .bss / memoryLabl: resd 63*64 为 BSS 中的63*64 dwords 保留空间。标签名称后面的: 是可选的,但在 NASM 中推荐使用。
  • 我如何在第 0 行的所有单元格中放置一个: 这看起来像 column 0,使用 s += i 作为数组索引来跨越通过增加数量来记忆。列是垂直的,行是水平的(您也称它们为“行”)。
  • line2 dd dup(1,2,1)。我想你的意思是line2 dd 1,2,1。我不确定您是否可以复制多个元素的模式。
  • @PeterCordes 是的,你是对的
  • 我有点想知道要回答你什么来帮助你:你对使用三角矩阵的代码更感兴趣,还是你只想要帕斯卡三角?你想要它怎么样?因为我猜存在直接的公式来计算三角形的特定位置(我认为它是组合数学中的“n 高于 k”或类似的东西),而且如果你想要屏幕输出,你可以只使用单个长行缓冲区来输出整个三角形,在每行输出后更新内容等......即打印帕斯卡三角形(在屏幕上或文本文件中)并不需要将其存储在内存中。

标签: algorithm assembly matrix x86 nasm


【解决方案1】:

TL:DR:您只需要按顺序遍历数组,因此您不必计算索引。见第二部分。


要随机访问index into a (lower) triangular matrix,行r 在大小为r-1 的三角形之后开始。一个大小为n 的三角形有n*(n+1)/2 个元素,使用Gauss's formula for the sum of numbers from 1 to n-1。所以一个大小为r-1 的三角形有(r-1)*r/2 元素。一旦我们知道行首的地址,在行中索引列当然是微不足道的。

每个 DWORD 元素都是 4 字节宽,我们可以将缩放作为乘法的一部分,因为 lea lets us shift and add 以及将结果放在不同的寄存器中。我们将n*(n-1)/2 elements * 4 bytes / elem 简化为n*(n-1) * 2 bytes

上述推理适用于基于 1 的索引,其中第 1 行有 1 个元素。如果我们想要从零开始的索引,我们必须在计算之前通过向行索引添加 1 来调整,所以我们想要三角形的大小 r+1 - 1 行,因此 r*(r+1)/2 * 4 bytes。它有助于将线性数组索引放入三角形中以快速复查公式

 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

第 4 行,我们称之为“第 3 行”,从整个数组的开头开始 24 个字节。那是(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 位绝对寻址是可以的 (32-bit absolute addresses no longer allowed in x86-64 Linux?)。如果不是,请使用 RIP 相对 LEA 获取寄存器中的triangle 基地址,并将其添加到rsi*4x86 addressing modes 只能有 3 个组件,其中一个是常量。但是你的静态triangle的情况,所以我们可以充分利用列的缩放索引,基数作为我们计算的行偏移量,实际数组地址作为位移。


计算三角形

这里的诀窍是你只需要按顺序循环它;您不需要随机访问给定的行/列。

你在写下面一行的时候读了一行。 当您到达行尾时,下一个元素就是下一行的开始。当您沿着行向下时,源指针和目标指针之间的距离会越来越远,因为目的地总是前一整行。而且你知道行的长度=行号,所以你实际上可以使用行计数器作为偏移量。

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 位整数(只需更改寄存器名称和偏移量,不要忘记 resdresq),就可以避免这种情况。 64 选择 32 是 1832624140942590534 = 2^60.66。

用于保留空间并将每一行标记为row0row1 等的 %rep 块来自 my answer to the question you linked about macros,比 IMO 的其他答案更理智。

你标记了这个 NASM,所以我使用它,因为我很熟悉它。您在问题中使用的语法是 MASM(直到最后一次编辑)。 MASM 中的主要逻辑是相同的,但请记住,您需要 OFFSET 三角形才能将地址​​作为立即数获取,而不是从中加载。

我使用 x86-64 是因为 32 位已过时,但我避免了过多的寄存器,因此如果需要,您可以轻松地将其移植到 32 位。如果您将其放在函数中而不是独立程序中,请不要忘记保存/恢复调用保留寄存器。

展开内部循环可以节省一些复制寄存器的指令,以及循环开销。这是一个经过一定程度优化的实现,但我主要将其限制为使代码更简单以及更小/更快的优化。 (除了可能使用指针增量而不是索引。)花了一段时间才使它变得如此干净和简单。 :P

在不同的 CPU 上进行数组索引的不同方法会更快。例如可能使用索引寻址模式(相对于dst)来处理内部循环中的负载,因此只需要一个指针增量。但如果你想让它运行得更快,SSE2 或 AVX2 vpaddd 可能会很好。使用palignr 进行改组可能很有用,但也可能是未对齐的负载,而不是一些改组,尤其是使用 AVX2 或 AVX512。

但无论如何,这是我的版本;我不是想像你那样写,你需要为你的作业写你自己的。我正在为未来的读者写信,他们可能会了解 x86 的高效性。 (另请参阅the x86 tag wiki 中的性能部分。)


我是怎么写的

我从头开始编写代码,但很快意识到一个错误会很棘手,我不想只是用循环内部分支的愚蠢方式来编写特殊情况。

最终的帮助是在内部循环的指针上为前置条件和后置条件编写 cmets。这清楚地表明我需要使用 eax=0 进入循环,而不是使用 eax=1 并将 eax 存储为循环内的第一个操作,或类似的东西。

显然每个源值只需要读取一次,所以我不想编写读取[rsi][rsi+4] 或其他内容的内部循环。此外,这会使正确的边界条件变得更加困难(不存在的值必须读取为 0)。

在我最终只为整个三角形使用结束指针之前,我花了一些时间来决定是否要在寄存器中使用实际的计数器来记录行长或行号。在我完成之前并不明显,使用纯指针增量/比较会节省这么多指令(当上限是像 end_triangle 这样的构建时间常量时,寄存器),但效果很好。

【讨论】:

  • 咯咯,他需要你的 32 位代码:stackoverflow.com/q/49185043/3857942
  • 为什么用高斯公式计算从 1 到 n-1 而不是从 1 到 n 的数之和?
  • @TigerTV.ru:我们想要行n开始的索引,其长度为n(基于1的索引)。此行之前的三角形以长度为n-1的行结束。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-06-16
  • 2019-06-11
  • 1970-01-01
  • 2010-12-28
  • 2013-11-05
  • 1970-01-01
  • 2011-12-24
相关资源
最近更新 更多