Skip to main content
GameDev.net gamedev.net
🔒 Locked

Help optimizing insertion sort in asm x86

Started by godplusplus Aug 24, 2011 at 9:47 AM 9 replies 7k views
Original Post
godplusplus
godplusplus
Hello everyone!

I was doing some sorting algorithms in asm x86 (gnome sort, insertion sort and quicksort) to sort some ints based on c++ versions I made of them.

They all work, and I wanted to test them against my C++ versions to see if I had managed to make my asm x86 versions faster (or, at least, as fast) as my C++ versions.

My gnome sort and quicksort are pretty much the same speed as their C++ counterparts... But my insertion sort is waaaay slower than my C++ version.

If someone could please look at it and tell me any ideas to make it faster, or at least help me find out which part is slowing it down so much.

Here's the assembly code I wrote:



.386

.model flat, c

.code



;Code by Miguel Casillas.

;This code can be used and reproduced, please give credit


;void InsertionSort(void *pArray, int nItems);

InsertionSort PROC


;These registers must be restored at the end
push EBP
mov EBP, ESP
push EBX
push ESI
push EDI


;EBP + 8 is the array
;EBP + 12 is the number of items in the array


;setting ECX to the number of items
;we multiply by 4 (size of the element) in order to put ECX
;at the last address of the array
mov EAX, [EBP+12]
mov ECX, 4
mul ECX
mov ECX, EAX


;We will move 'i' and 'j' in increments and decrements of 4,
;which is the size of the elements
mov EAX, 4 ;EAX will be our 'i'
xor EBX, EBX ;EBX will be our 'j' (setting it to 0)
mov ESI, [EBP+8] ;ESI is the array


MainLoop:

;If 'i' >= the number of items, exit the loop
cmp EAX, ECX
jge EndLoop


;Save our "number of items" value, we will restore it later
push ECX


;ECX is now our "key", so, ECX = array
mov ECX, [ESI+EAX]


;j = i-1
mov EBX, EAX
sub EBX, 4


EnterWhile:

;If j < 0, exit this loop
cmp EBX, 0
jl EndWhile


;If array[j] <= key, exit this loop
cmp [ESI+EBX], ECX
jle EndWhile


;array[j+1] = array[j]
push [ESI+EBX]

pop [ESI+EBX+4]


;j--
sub EBX, 4


;Go back to the top of this loop
jmp EnterWhile


EndWhile:


;array[j+1] = key
mov [ESI+EBX+4], ECX


;i++
add EAX, 4


;restore our "number of items" value
pop ECX


;Go back to the top of the main loop
jmp MainLoop


EndLoop:


;Restoring the registers
pop EDI
pop ESI
pop EBX
pop EBP


RET

InsertionSort ENDP


END




If you can help me out, I will really appreciate it!!!
rip-off
rip-off
Have you looked at what the C++ compiler is generating?

I suspect the problem is pushing and popping the number of items in the outer loop. I've only limited experience in writing x86 assember, but is there any reason you're not using EDX for the key or N?
godplusplus
godplusplus
Good question. I forgot about EDX at that moment.
Anyway, I made the change and used EDX for the key but there was no difference in the performance unfortunately.
rip-off
rip-off
Have you tried looking at the compiler's version?
godplusplus
godplusplus
Yes, I did. It looks quite similar, except that it has more MOV operations than mine and the for loop is done in a different way (that I can't really understand).

I tried my code in a different computer and I realized that, even though it's still slower than the C++ version, the percentage of difference is quite contrasting. In one computer the difference is of more than double, while in the other one the difference is just about 15%... Which is kinda baffling.
rip-off
rip-off
What does your C++ code look like? How are you testing it? Can you post your test?
godplusplus
godplusplus
Here's the C++ code:



void InsertionSort(int* vArray, int nNumElements)
{
int j(0), key(0);
for(int i = 1; i < nNumElements; i++)
{
key = vArray;
j = i-1;
while((j>=0) && (vArray[j]>key))
{
vArray[j+1] = vArray[j];
j--;
}
vArray[j+1] = key;
}
}





The test is simple, I make an array with 10,000 random ints, then sort it and time the function. This test is run 100 times and the average time is calculated.
rip-off
rip-off
Looking at your original assembler more closely, you're using push and pop to assign values in an inner loop? Avoid this!


int* vSortedArray
[/quote]
This name is misleading - the array isn't sorted until the function returns. I'd call it ... "array".


short sCurrentIndex
[/quote]
Again, misleading - the current indices in this function are i and j. I'd call this "n", "length", "max", something like that.

Also, "short" - you don't want to support sorting more than 32,000 values?


The test is simple, I make an array with 10,000 random ints, then sort it and time the function. This test is run 100 times and the average time is calculated.
[/quote]

I tried my code in a different computer and I realized that, even though it's still slower than the C++ version, the percentage of difference is quite contrasting. In one computer the difference is of more than double, while in the other one the difference is just about 15%... Which is kinda baffling.
[/quote]
You could be spilling outside the cache on one machine, and keeping inside the cache on another. This could "even up" the difference if your version hits memory more often (as it would seem to if it keeps touching the stack).

Try writing the tests to support a variable number of elements and iterations. That way you can test sorting 1,000 elements 1,000,000 times, and 1,000,000 elements 1,000 times, or any other combination. This might be able to prove or disprove the cache theory, by choosing an arrays to fit the various cache sizes on each machine, and seeing what happens when the array doesn't fit on one vs does fit on another.
godplusplus
godplusplus
This function wasn't a separate function, I just had it in the main code, and I had two arrays (an unsorted one that was gonna be kept in order to test it with the different algorithms) and a second array that would be sorted and printed out, so I just called it "vSortedArray".
The current index thing is because I'm using that variable in the code as well (the whole thing is a windows forms program that allows to add numbers to the list, reset the list, choose which algorithms to use, etc).
That's the reason the variables are named like that. If I were to put this as a separate function, of course I would rename it, but for here I just copy pasted and made a fake function header.

I'll edit the post to make the names more readable.

Anyway, back to the assembly… What's wrong with pushing and popping inside the loop? I thought it would be a fast thing, after all, I'm also doing it in my other sorting algorithms and it didn't cause a performance issue.

About the cache thing… I already tried different combinations, I tried combining different array sizes with different loop sizes, I even tried using different numbers for the possibilities of numbers within the array (in order to test arrays with many unique values and arrays with lots of repeated values), and the difference in performance was the same.
Adam_42
Adam_42
Here's a few more ideas to try:

When you're multiplying by a power of two, a left shift is more efficient than a multiply. That's outside the loop though so it won't make much difference, unless the array is very small.

Instead of having an unconditional jump back to the start of the loop, and then immediately doing a test and another jump, consider doing that test at the end of the loop. You can put a jump to that test just before the start of the loop to keep the order of operations the same.

In my experience the compiler will generally turn variables used as array indices into direct pointers when it can. That way there's generally no need to add the start of array offset every time round the loop. That cuts out some extra additions, and more importantly shortens dependency chains. When doing this you'll also need to create a pointer to the end of the array instead of using the count of elements to test against.
Erik Rufelt
Erik Rufelt

Anyway, back to the assembly… What's wrong with pushing and popping inside the loop? I thought it would be a fast thing, after all, I'm also doing it in my other sorting algorithms and it didn't cause a performance issue.
[/quote]

That will read your value and put it on the stack, then read it back from the stack and put it back. It's better to just put it in a register and then move it back to memory.

Topic Locked

This topic has been locked by a moderator. New replies are not allowed.

Sign in to reply to this topic.