-
Notifications
You must be signed in to change notification settings - Fork 18
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add Bubble Sort
example
#158
base: main
Are you sure you want to change the base?
Conversation
Kudos, SonarCloud Quality Gate passed! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM.
I left lots of inline comments. Please go over them, I'm sharing optimization techniques, coding guidelines, code organization tips, that we expect to have in production code.
For the first masm example, I think this is great :)
examples/bubble_sort.masm
Outdated
# Load and increment `g` counter to keep track of added elements | ||
mem_load.3 | ||
add.1 | ||
dup | ||
mem_store.3 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note: It is good practice to always include the cycle count. The best way to find the exact cycle count of an instruction is to look at the assembler:
Each instruction is translated to a series of operations. Each operation takes exactly one cycle. Using the above references we have:
mem_load.3
: 2 cyclesPush(3) MLoad
add.1
: 1 cycleIncr
dup
: 1 cycleDup0
mem_store.3
: 2 cyclesMStore Drop
Once you know the cycle count, it can be documented inline:
# Load and increment `g` counter to keep track of added elements | |
mem_load.3 | |
add.1 | |
dup | |
mem_store.3 | |
# Load and increment `g` counter to keep track of added elements (6 cycles) | |
mem_load.3 | |
add.1 | |
dup | |
mem_store.3 |
And that is useful to compute the cycle count for the procedure.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wouldn't it be impossible to give the cycle count if there is a loop?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it is not impossible, but it is probably harder, use asymptotic approximations, if it is too hard than not having it should be fine.
examples/bubble_sort.masm
Outdated
# Load `adv` array length and check if `g` == `adv`; | ||
# signifying that all elements from array have been added to `operand_stack` | ||
mem_load.4 | ||
eq | ||
if.true | ||
# Stop looping and reset `g` to 0 | ||
push.0 | ||
push.0 | ||
mem_store.3 | ||
else | ||
# Continue looping | ||
push.1 | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is a good idea to optimize loops, since these are executed often.
Here we can do a few things:
- Instead of starting at
0
and counting up ton
. Start at-n
and count up to zero.- The comparison is against the constant
0
, instead of two variables. So this removes the load forlength
. - Going from
-n
to0
instead of0
ton
also performs better because it uses+1
ref instead of-1
ref. This performs better because+1
has a special operationIncr
that takes a single cycle, whereas-1
is two operationsPush(-imm) Add
which takes 2 cycles (and the immediate value takes another 64bits to encode)
- The comparison is against the constant
- Move cleanup out of the loop. In this case the
push.0 mem_store.3
. This eliminates the if-else from the loop. Here this can be done by usingeq not
push.1 | ||
while.true | ||
# Push 1 element from the array into the `operand_stack` | ||
adv_push.1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note: For production code almost always the data should always be validated, here we only know an array of length n
was sorted, but we know nothing about the contents. To validate the contents the input should be a hash of the array instead of its length (this would not leak information because we use cryptographic secure hashes).
To implement this, the stack input would be a commitment instead of a counter. The length would go in the advice_stack
(or the advice_map
which has native support for length). The loop would use adv_pipe
instead of adv_push
. The adv_pipe
instruction does a few things
- reads the data from the advice stack
- hashes the data read
- saves it to memory
Here is an example of that: https://github.com/0xPolygonMiden/examples/blob/main/examples/matrix_multiplication.masm#L108-L146
# incrementCounters() -> void | ||
# | ||
# Increments the `i` and `j` counter variables by 1 | ||
proc.incrementCounters | ||
# load `i` counter, increment it by 1 and store it in mem[1] | ||
mem_load.1 | ||
add.1 | ||
mem_store.1 | ||
|
||
# load `j` counter, increment it by 1 and store it in mem[2] | ||
mem_load.2 | ||
add.1 | ||
mem_store.2 | ||
end | ||
|
||
# decrementCounters() -> void | ||
# | ||
# Decrements the `i` and `j` counter variables by 1 | ||
proc.decrementCounters | ||
# load `i` counter, decrement it by 1 and store it in mem[1] | ||
mem_load.1 | ||
sub.1 | ||
mem_store.1 | ||
|
||
# load `j` counter, decrement by 1 and store it in mem[2] | ||
mem_load.2 | ||
sub.1 | ||
mem_store.2 | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note: For production code, the j
would have to be removed. Since it is the same as i + 1
. All the instructions manipulating it are extra cycle cost that can be removed.
# Initialise the VM | ||
exec.initialise | ||
|
||
# Loop over the array and sort repeatedly | ||
while.true | ||
# If looping has been done over the whole array reset counters to start over from the beginning | ||
exec.resetCounters | ||
|
||
# Sort elements two-by-two repeatedly | ||
exec.sort | ||
|
||
# Check if the array is sorted if so then stop looping | ||
exec.sorted | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
note: for production code we also want to have a nice interface. This would be better wrapper around a single sort
procedure that people can use. Things like initialise
, resetCounters
, sorted
should be treated as implementation details.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe the code would be both more efficient and more readable if it used the stack more often. Here is one suggestion:
#! Sort data
#!
#! Input:
#! operand_stack: [addr, C, ...]
#! advice_stack: [n, d0, d1, .., dn, ..]
#! Output: [...]
#!
#! Where:
#! addr: address were the data will be loaded and sorted in-place
#! C: a commitment to the data to be sorted
#! n: the number of elements to be sorted
#! d0..dn: the data to be sorted
#!
#! Cycles: X
proc.sort
# save a copy of addr
dup movdn.6
# => [C, B, A, addr, C, addr ..]
# initialize the hasher state
# TODO: handle padding / input array with odd number of elements
padw padw padw
# => [C, B, A, addr, C, addr, ..]
# load length
adv_push.1
# => [len, C, B, A, addr, C, addr, ..]
# load data
# optimization: negate the counter, we are going to loop from `[-n,0)`
neg
dup neq.0
while.true
movdn.13 # save len (TODO: movup?)
adv_pipe
movup.13 add.2 # update len
dup neq.0
end
# clear counter and array end
drop movup.13 drop
# TODO: verify commitment
# sort
# 1. copy the addr
# 2. have a boolean to check if any swaps happened
# 3. stop when the boolean is false
end
Should we merge this? @phklive |
Depends on what we want in the examples repo. If we want to have different styles and programs then we could merge now. If we want to show idiomatic and optimised examples that user could take as boilerplate then I could work to find time this week to apply the modifications requested and then merge. What do you think? |
Yes, this solution. |
I didn't get it sorry. The two solutions were:
What is the one you would like to follow? |
Ah sorry for being unclear. Can you work in the suggestions from Augusto? And then we can merge. That way, we have optimized code and maybe we can even use Bubble Sort in the miden-lib eventually. |
Sure, starting now. |
@phklive should we try to close this soon? |
On it now. |
Quality Gate passedKudos, no new issues were introduced! 0 New issues |
Can we merge this? |
Proposing an implementation of the Bubble Sort algorithm in Miden Assembly. Sorting an array that is provided through private inputs using the
advice_stack
.As examples will be mostly read by newcomers, I have tried to create thorough documentation of the code to aid them in understanding the implementation and principles of Miden Assembly, enabling easier onboarding.
Proposed Improvements:
clk
) count.I welcome any feedback or suggestions