英文:
No output from bubble sort Algorithm implemented in MIPS (for strings)
问题
在MIPS汇编代码中没有字符串输出。
我已经尝试查找了一段时间,似乎找不到一个合适的答案。
下面的代码实现了对字符串的冒泡排序,它只适用于小写字母,因为它比较ASCII码数字。(如果我没有使用正确的术语,我很抱歉)
代码上方的注释基本上充分解释了每个部分的作用。
尝试通过在线资源查找问题可能所在,但没有得到任何合适的答案。
示例输入:hello
预期输出:ehllo
代码输出:(无)
英文:
No string output in MIPS assembly code.
I tried looking this up for sometime now and I can't seem to find a proper answer.
The code below implements bubble sort on a string, it only works on lowercase letters as it compares ascii code numbers.(sorry if I am not using proper terminology)
.data
InputPrompt: .asciiz "Please input a string(no more than 255 Characters: "
Buffer: .space 256
.text
li $v0, 4
la $a0, InputPrompt
syscall
li $v0, 8
la $a0, Buffer
li $a1, 255
syscall #taking input
jal BubbleSort
move $a0, $v0
li $v0, 4
syscall
li $v0, 10
syscall
BubbleSort:
addi $sp, $sp, -32
sw $ra, 0($sp)
sw $t0, 4($sp) #temporary storage for the string
sw $t1, 8($sp) #Used to go back to beginning of string
sw $t2, 12($sp) #register to keep track of iterator
sw $t3, 16($sp) #flag to check if a switch happened or not
sw $t4, 20($sp) #for str[i] < str[i+1] condition
sw $t5, 24($sp) #current byte
sw $t6, 28($sp) #next byte
la $t0, ($a0) #loading string
li $t3, 0
OutterLoop: #while(!sorted)
move $t2, $zero #reset iterator
beq $t3, 1, Done # (!sorted) condition
li $t3, 1
li $t1, 0
InnerLoop:
lb $t5, 0($t0) #load current byte
lb $t6, 1($t0) #load next byte
beqz $t6, EndString #'.data
InputPrompt: .asciiz "Please input a string(no more than 255 Characters: "
Buffer: .space 256
.text
li $v0, 4
la $a0, InputPrompt
syscall
li $v0, 8
la $a0, Buffer
li $a1, 255
syscall #taking input
jal BubbleSort
move $a0, $v0
li $v0, 4
syscall
li $v0, 10
syscall
BubbleSort:
addi $sp, $sp, -32
sw $ra, 0($sp)
sw $t0, 4($sp) #temporary storage for the string
sw $t1, 8($sp) #Used to go back to beginning of string
sw $t2, 12($sp) #register to keep track of iterator
sw $t3, 16($sp) #flag to check if a switch happened or not
sw $t4, 20($sp) #for str[i] < str[i+1] condition
sw $t5, 24($sp) #current byte
sw $t6, 28($sp) #next byte
la $t0, ($a0) #loading string
li $t3, 0
OutterLoop: #while(!sorted)
move $t2, $zero #reset iterator
beq $t3, 1, Done # (!sorted) condition
li $t3, 1
li $t1, 0
InnerLoop:
lb $t5, 0($t0) #load current byte
lb $t6, 1($t0) #load next byte
beqz $t6, EndString #'\0' has value of 0
slt $t4, $t5, $t6 #str[i] < str[i+1]
beq $t4, 1, ELSE
sb $t6, 0($t0)
sb $t5, 1($t0)
li $t3, 0
ELSE:
addi $t0, $t0, 1
addi $t1, $t1, 1
j InnerLoop
EndString:
sub $t0, $t0, $t1 #go back to beginning of string
j OutterLoop
Done:
move $v0, $t0 #return string
lw $ra, 0($sp)
lw $t0, 4($sp)
lw $t2, 8($sp)
lw $t3, 12($sp)
lw $t4, 16($sp)
lw $t5, 20($sp)
lw $t6, 24($sp)
addi $sp, $sp, 32
jr $ra
' has value of 0
slt $t4, $t5, $t6 #str[i] < str[i+1]
beq $t4, 1, ELSE
sb $t6, 0($t0)
sb $t5, 1($t0)
li $t3, 0
ELSE:
addi $t0, $t0, 1
addi $t1, $t1, 1
j InnerLoop
EndString:
sub $t0, $t0, $t1 #go back to beginning of string
j OutterLoop
Done:
move $v0, $t0 #return string
lw $ra, 0($sp)
lw $t0, 4($sp)
lw $t2, 8($sp)
lw $t3, 12($sp)
lw $t4, 16($sp)
lw $t5, 20($sp)
lw $t6, 24($sp)
addi $sp, $sp, 32
jr $ra
The comments above pretty much explain what each part does sufficiently.
I tried looking up through online resources on what the problem might be, but didn't get to any proper answers.
Example input: hello
Expected output: ehllo
Output from code: (nothing)
答案1
得分: 0
外部循环的退出条件是您没有交换任何元素,因此它被认为已排序。换句话说,外部循环的继续条件是已经交换了元素,因此它被认为是未排序的。
然而,您不仅在元素小于时进行交换,还在元素小于或等于时进行交换,而 "hello" 中有两个 'l'。因此,当输入字符串中有两个相同的字母时,外部循环永远不会停止。尝试输入 "helo",它将起作用。
等于时进行的这次交换是不必要但无害的,直到字符串完全排序,当它变成唯一的交换时,阻止了循环终止,典型的无限循环。
您应该能够自己调试这个问题。使用单步执行并观察外部循环和内部循环的交换会在字符串完全排序后继续进行。
让我们看看您的交换条件测试:
slt $t4, $t5, $t6 # str[i] < str[i+1]
beq $t4, 1, ELSE
这执行的是 $t5 < $t6
,然后测试用于跳过 then
部分的真条件。因此,then
部分执行条件相反:$t5 >= $t6
,这会导致在 'hello' 中的 'l' == 'l' 时进行交换。实际上,您正在执行以下操作:
if ( p[0] >= p[1] ) {
// 当前字母大于或等于后面的字母时交换,包括相等情况
$t3 = 0;
}
而您真正想要的是:
if ( p[0] > p[1] ) {
// 仅在真正需要交换时执行交换
$t3 = 0;
}
为了解决这个问题,您需要测试 $t5 <= $t6
(作为跳过 then
部分的 if-then 条件),因此执行 then
部分的条件现在是 $t5 > $t6
。
您可以使用 sle
而不是 slt
。
值得注意的是,这是一个伪指令,由汇编器扩展为3个真实的机器码指令,因为MIPS硬件实际上没有这个操作,只有汇编器知道这个操作。
那么,如果我们不想依赖汇编器的帮助,可以做些什么?
我们可以交换 slt
的操作数,即 $t6 < $t5
,然后还要交换退出测试,以便在此条件不成立时转到 ELSE
,这看起来可能是双重否定,但实际上不是:综合考虑,它将相等测试移到另一个条件(if-then 的另一个分支),变为 $t6 >= $t5
,也就是 $t5 <= $t6
,而不仅仅是 $t5 < $t6
。
这意味着只有当不成立时才跳到 ELSE
部分,即当 ! (p[1] < p[0]) 时,这与 p[1] >= p[0] 相同:
slt $t4, $t6, $t5 # str[i+1] < str[i]
beq $t4, $0, ELSE # 当 ! (str[i+1] < str[i]) 时转到 ELSE
# 也就是当 str[i+1] >= str[i] 时转到 ELSE
# 也就是当 str[i] <= str[i+1] 时转到 ELSE
这相当于:
if ( p[1] >= p[0] ) goto ELSE; // 也就是 if ( p[0] <= p[1] ) goto ELSE;
// 仅在真正需要交换时执行交换
$t3 = 0;
ELSE:
这等同于:
if ( p[1] < p[0] ) { // 也等同于:if ( p[0] > p[1] ) {
// 仅在真正需要交换时执行交换
$t3 = 0;
}
- 这是非常令人困惑的内容,很容易犯错误或拼写错误。这里有两个不同但相关的概念,一个是否定,另一个是操作数交换。
- 在否定中,'=' 可以出现或消失,例如
! (a <= b)
变成a > b
,以及! (a < b)
变成a >= b
,
- 而在操作数交换中,'=' 保持不变(无论是否存在),例如
a < b
变成b > a
,以及a <= b
变成b >= a
。
- 我们将否定和操作数交换结合在一起,以使用MIPS硬件具有的
slt
指令。
建议和其他注意事项:
-
您正在将
$t
寄存器保存在堆栈上,然后稍后恢复它们。不要这样做,这是没有意义的,对于这些寄存器(称为临时寄存器或调用破坏寄存器),没有任何好处。只有$s
寄存器需要保存在堆栈上,而且仅在函数中使用它们时需要保存。 -
您正在将
$ra
保存在堆栈上,但由于您的函数从未修改它,因此这也是不必要的。如果您的排序函数调用另一个函数(例如交换函数),那么必须重新为该嵌套调
英文:
The exit condition for the outer loop is that you have not swapped anything, therefore it is considered sorted. To put it another way, the continuation condition for the outer loop is that something has been swapped, therefore it is considered non-sorted.
However, you "swap" not just when less than, but less that or equal, and hello
has two l's. Thus, the outer loop never stops, when the input string has two letters the same. Try helo
for input and it will work.
This swap on equal is unnecessary but harmless — until the string is fully sorted, when it becomes the only swap and thus prevents the loop from terminating — classic infinite loop.
You should be able to debug this yourself. Use single step and observe the outer loop and inner loop swapping continues ad nauseam even after the string is fully sorted.
Let's look at your swap condition test:
slt $t4, $t5, $t6 #str[i] < str[i+1]
beq $t4, 1, ELSE
That's doing $t5 < $t6
, and then testing for the true condition for skipping the then-part. So, the then-part execution condition is the opposite: $t5 >= $t6
, which leads to swapping when 'l' == 'l'
in 'hello'`. In essence, you're doing:
if ( p[0] >= p[1] ) {
// swap when earlier letter is > following letter, but also when equal
$t3 = 0;
}
where you really want:
if ( p[0] > p[1] ) {
// swap only when really necessary
$t3 = 0;
}
To solve the problem, you need to test for $t5 <= $t6
(as the if-then condition for skipping the then-part) instead, such that the condition to execute the then-part is now $t5 > $t6
.
You can use sle
instead of slt
.
Though FYI, this is a pseudo instruction that expands to 3 real machine code instructions, since the MIPS hardware doesn't actually have this operation, only the assembler knows this one.
So, what can be done if we don't want that help from the assembler?
We can swap the operands of slt
, as in $t6 < $t5
, and then also swap the exit test to branch to the ELSE if this condition doesn't hold — this would appear to be double negation but it isn't exactly: taken together it moves the equality test to the other condition (the other branch of the if-then), making it $t6 >= $t5
aka $t5 <= $t6
instead of just $t5 < $t6
.
That means skipping ahead to the ELSE part when ! (p[1] < p[0]), which is the same as p[1] >= p[0]:
slt $t4, $t6, $t5 # str[i+1] < str[i]
beq $t4, $0, ELSE # goto ELSE when ! (str[i+1] < str[i])
# aka goto ELSE when str[i+1] >= str[i]
# aka goto ELSE when str[i] <= str[i+1]
which is equivalent to:
if ( p[1] >= p[0] ) goto ELSE; // aka if ( p[0] <= p[1] ) goto ELSE;
// swap only when really necessary
$t3 = 0;
ELSE:
which is equivalent to:
if ( p[1] < p[0] ) { // and also same as: if ( p[0] > p[1] ) {
// swap only when necessary
$t3 = 0;
}
This is super confusing stuff, easy to make a mistake or typo. There are two different though related concepts here, one is negation, and the other is operand swapping.
- in negation, the
=
comes or goes e.g.! (a <= b)
becomesa > b
, and! (a < b)
becomesa >= b
), whereas
- in operand swapping the
=
stays as is (whether present or absent), e.g.a < b
becomesb > a
, anda <= b
becomesb >= a
.
We combine both negation and operand swapping as here to use the slt
instruction that MIPS hardware has.
Suggestions and other notes:
-
You are storing
$t
registers on the stack, and restoring them later. Don't do this — it is pointless and benefits no one as these registers are scratch / call-clobbered. Only$s
registers need be saved on the stack, and only if they are used by the function. -
You are saving
$ra
on the stack, but since it is never modified by your function, this is also unnecessary. If your sort function were to call another function (a swap, for example), that would necessarily repurpose$ra
for that nested invocation, and you would indeed need to save your function's original$ra
value so the sort function can get back to its caller when its done (heremain
but doesn't always have to be), after the swap function uses$ra
to get back to the sort function. -
syscall #8 accepts input from the user and puts it into the buffer, including the carriage return the user enters, yet you don't strip that from the input prior to sorting. Since the user's carriage return character is also in the buffer, and, the carriage return character has a very low ascii value, the bubble sort will bring that character to the beginning of the string. So you'll have
\nhelo
, which sort of works, but is probably unexpected, and explains the extra newline that comes out first, when printing to the sorted string. -
As a side note let's mention that
beq
with a constant is a psuedo instruction that expands into multiple instructions by the assembler. Sinceslt
yields a boolean1
or0
, it would be simpler to dobne $t4, $0, ELSE
, which is not a pseudo instruction, and also having the same effect of testing for1
in order to exit a loop or skip a then-part. This a better way to do the same as you have (this does not address the>=
vs.>
problem, of course).We can think of
beq ... $0 ...
as a metaphor for branch on false, andbne ... $0 ...
as a metaphor for branch on true, when booleans such as those delivered byslt
are being tested.This also applies to your other boolean
$t3
regarding your exit test for the outer loop.beq $t3, 1, Done # (!sorted) condition
is simpler and more iconic as:
bne $t3, $0, Done # (!sorted) condition
-
If you do choose to strip the newline character before doing the sort operation, be aware that your sort function expects at least 1 character in the user's input (it only tests for null on the 2nd character) and will fail in some way (that may or may not be observable) if the input is empty. A sort function should work properly when there are no elements to sort. You can test for this condition once outside of and before the outer sorting loop, and simply skip the whole sort (jumping to the function exit prologue) if str[0] is null.
When using syscall #8, you'll get a newline followed by null terminator. If you overwrite the newline with a null, that means any string in the buffer will have two nulls, even the empty string, and so your algorithm will be ok.
However, if you used another method to put strings in the buffer, the scenario is that then a long string "hello world" later overwritten by a shorter string "x" might look like this "x\0llo world\0" in the buffer. You're not supposed to look past the first null found, and your algorithm won't with that one but if the later shorter string was simply the empty string, you'll have an initial null followed by some characters from the previous string "\0ello world\0" — in that case your algorithm will stop at the second null, which is in the previous string instead of at the proper first null.
-
You have
la $t0, (a0)
but this again is a pseudo instruction, unnecessary here, this is
better done as:move $t0, $a0
or, if you want to avoid even the simple pseudo instructions then:
add $t0, $a0, $0
-
Your code to reset the main pointer,
$t0
could be simpler. It would be more normal to simply reset the pointer rather than using pointer subtraction. You have:$t0 = $a0; // #1 outer loop $t1 = 0 // #2 inner loop $t1++ // #3 end inner loop $t0 = $t0 - $t1 // #4 end outer loop move $v0, $t0 // #5
This can be simplified as follows:
outer loop $t0 = $a0; // #1 simply initialize, now inside outer loop at top inner loop ... end inner loop end outer loop move $v0, $a0 // #5 to return original string use value from $a0
By re-initializing from the original, unmodified
$a0
, there's no need for the counter variable,$t1
at all, and so #2,#3,#4 can be eliminated.Though of no real consequence in your original code, the return value would probably normally be obtained from the unmodified parameter
$a0
, rather than the mutating pointer,$t0
. In my update here it is important to use$a0
, since$t0
isn't reinitialized at the bottom of the loop, and would reasonably be done after the exit test at the top of the loop, leaving$t0
in a bad place to use as return value.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论