汇编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==0
。https://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
有它的东西。
下面的代码应该计算一个几何级数(它的比率等于 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 编译器会做什么:编译器输出通常是优化的良好起点,或者是学习目标机器技巧的一种方式。(另见
您的版本存储第一个元素而不检查其高位,因此如果输入仅设置了高位,您将存储另外 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==0
。https://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
有它的东西。