汇编ARM编程/单周期处理器指令

Assembly ARM programming/ Monocycle processor instruction

下面的代码应该计算一个几何级数(它的比率等于 2)并且它应该显示 10 个系列。参考输入数必须选择 2 的幂(在选择数为 7 的情况下)。有一些实施要求是:

I) 所有的字符串值必须从一个基本位置 X * (X 100 乘以 100),接收所有 32 位值所需的偏移量。

II) 除了RX寄存器中数据存储器的最后位置外,还应存储最终值。

III) 程序应检查值是否小于 2,147,483,648 progression(10000000000000000000000000000000)2

IV) TST指令、LSL、CMP指令必须一次性实现,另外ARM单周期已经实现的指令可以根据需要使用。

START
        AND     R5, R5, #0      ;Reset Registers
        AND     R7, R7, #0
        AND     R0, R0, #0
        AND     R4, R4, #0

        ADD     R5, R5, #1      ;R5 receives de base number of GP
        ADD     R7, R7, R5      ;R7 = register of reference number
        ADD     R0, R0, #200
        ADD     R0, R0, #200
        ADD     R0, R0, #200
        ADD     R0, R0, #100    ;sets the memory base position in 700

        ADD     R4, R4, #1      ;starts the count
        STR     R7, [R0]        ;saves the first GP value in the memory

LOOP
        TST     R7, #2147483648 ;check if GP values is higher than 2^31
        BNE     FIM             ;if so, ends the code
        LSL     R7, R7, #1      ;else, multiply by 2
        ADD     R4, R4, #1      ;increments the count
        ADD     R0, R0, #4      ;increments memory adress
        STR     R7, [R0]        ;saves value in the memory
        CMP     R4, #10         ;check if 10 interations have been executed
        BEQ     FIM             ;if so, ends the code
        B       LOOP            ;else, restarts loop loop
END

因为我是 ASM 编程的 'newbie',我想知道,根据前面所述的四个要求,这段代码可以改进什么?任何改变都会很好,学习东西的最好方法是从不同的角度看它,对吗?提前致谢

这看起来像是作业,所以我只是给你一些提示。

好吧,你需要在某处为 FIM 贴上标签。我假设它在您的代码末尾。

强制寄存器为零然后向其添加小的立即值看起来有点像 MIPS 代码。了解哪些值可以用作立即数并研究 MOV 和 MOVW 指令。

在无条件分支周围有一个条件分支是糟糕的编程,用一个条件分支代替。

了解 STR 指令的更多高级选项,这样您就不需要额外的指令来调整您的地址指针。

你的循环分支应该是 cmp / bne loop,所以你会失败以结束循环。这意味着循环内少了一条分支指令。参见

此外,利用您已经需要的指令设置标志,而不是使用单独的 TST 或 CMP 指令。

如果您要使用与输出指针分开的计数器,请将其向下计数至零,这样您就可以 subs r4, r4, #1 / bne.


您的代码中有 很多 遗漏的优化,尤其是您在寄存器中创建常量的疯狂方式。 ARM有一个真正的mov指令;使用它而不是与零相加或相加。

看看优秀的 C 编译器会做什么:编译器输出通常是优化的良好起点,或者是学习目标机器技巧的一种方式。(另见 and Matt Godbolt's CppCon2017 talk “What Has My Compiler Done for Me Lately? Unbolting the Compiler's Lid”。)


您的版本存储第一个元素而不检查其高位,因此如果输入仅设置了高位,您将存储另外 9 个零。 IDK 如果那是你想要的,或者如果那是你不必处理的情况。 (即,也许您的输入保证是非负符号数)。

// uint32_t would be more portable, but ARM has 32-bit unsigned int
void store_sequence(unsigned val)
{
    unsigned *dst = (unsigned *)700;
    unsigned *endp = dst + 10;

    // val = 1;   // use the function arg so it's not a compile-time constant
    for (; dst < endp; dst++) {
        *dst = val;    // first store without checking the value
        val <<= 1;
        if (val >= (1UL << 31))
            break;
    }
}

在移位后立即检查 val 会给出很好的 asm:否则编译器并不总是利用移位来设置标志。请注意,即使它是一个 for() 循环,编译器也可以在第一时间证明条件为真,并且不会在顶部添加额外的检查/分支来查看循环是否应该 运行零次。

我将此代码放在 the Godbolt compiler explorer 上以获得 ARM 的 gcc 和 clang 输出。

gcc7.2.1 -O3 完全展开循环。随着计数的增加,它最终决定进行循环,但展开的循环很有趣:如果完全展开,则不需要指针增量。使用不同的移位计数来重新移位原始指令也会创建指令级并行性(CPU 可以并行 运行 多个移位指令,因为不依赖于前一个的结果。)

请注意,lsls 从移位设置标志,并且 ARM 的标志包括一个 N 标志,如果结果的高位被设置则该标志被设置。如果 N==1,则 MInus 条件为真。这个名字来自 2 的负数补码,但一切都是位,你可以用它在高位上分支。 (PLus 条件的名字很奇怪:它对包括零在内的非负结果为真,即它只检查 N==0https://community.arm.com/processors/b/blog/posts/condition-codes-1-condition-flags-and-codes

编译器决定使用谓词 bx lr,而不是实际的 bmi(如果为负则分支)。即 return 如果 MInus,否则 运行 作为 NOP。 (使用 -mcpu=cortex-a57 导致循环底部的 bmi,那里有 bx lr。显然,该微体系结构的调整选项使 gcc 避免了预测的 bx 指令。)

@ On function entry, val is in r0.  Use  mov r0, #1  if you want
@ from gcc7.2.1 -O3
store_sequence:
    mov     r3, #0             @ this is the most efficient way to zero a reg

    lsls    r2, r0, #1         @ instruction scheduling: create r2 early
    str     r0, [r3, #700]     @ gcc just uses offsets from a zeroed reg

    bxmi    lr                 @ if(val<<1 has its high bit set) return;

    lsls    r1, r0, #2
    str     r2, [r3, #704]     @ 2nd store, using val<<1 after checking it
    bxmi    lr

    lsls    r2, r0, #3         @ alternating r1 and r2 for software pipelining
    str     r1, [r3, #708]     @ 3rd store, using val<<2 after checking it
    bxmi    lr
...

要获得汇总循环,您可以增加循环计数,或使用 -Os 进行编译(针对代码大小进行优化)。

使用 endp = dst+100 和 gcc -O3 mcpu=cortex-a57(避免 bxmi lr),我们得到一个有趣的循环,它通过跳到中间进入,所以它可以在底部掉下来. (在这种情况下,让 cmp / beq 运行 成为第一次迭代可能会更有效,或者将 cmp/bne 放在底部。-Os 做后者。)

@ gcc -O3 -mcpu=cortex-a57     with loop count = 100 so it doesn't unroll.
store_sequence:
    mov     r3, #700
    movw    r2, #1100         @ Cortex-A57 has movw.  add would work, too.
    b       .L3

.L6:                          @ do {
    cmp     r3, r2
    beq     .L1                  @ if(p==endp) break;
.L3:                                 @ first iteration loop entry point
    str     r0, [r3]
    lsls    r0, r0, #1           @ val <<= 1
    add     r3, r3, #4           @ add without clobbering flags
    bpl     .L6               @ } while(val's high bit is clear)
.L1:
    bx      lr

有了 -Os,我们得到了一个更好看的循环。唯一的缺点是 bmi(或 bxmi lr)在 lsls 设置标志后立即在下一条指令中读取标志。不过,您可以在它们之间安排 add。 (或者在 Thumb 模式下你想这样做,因为 adds 的编码比 add 短。)

@ gcc7.2.1 -Os  -mcpu=cortex-a57
store_sequence:
    mov     r3, #700               @ dst = 700
.L3:                            @ do{
    str     r0, [r3]
    lsls    r0, r0, #1            @ set flags from val <<= 1
    bxmi    lr                    @  bmi  to the end of the loop would work

    add     r3, r3, #4            @ dst++
    cmp     r3, #740
    bne     .L3                 @ } while(p != endp)

 @ FIM:
    bx      lr

较大的 endp 不适合 cmp 的直接操作数,gcc 在循环外的 reg 中计算它。

它总是使用mov,或者从内存中的文字池加载它,而不是使用add r2, r3, #8192或其他东西。我不确定我是否构建了一个案例,其中 add 的立即数会起作用,但 movw 的立即数不会。

无论如何,常规 mov 适用于小立即数,但 movw 是一种较新的编码,不是基线,因此 gcc 仅在使用 -mcpu= 编译时使用 movw有它的东西。