【问题标题】:Optmization for quicksort in x86 32-bit assemblyx86 32 位汇编中的快速排序优化
【发布时间】:2015-10-02 21:51:37
【问题描述】:

我正在尝试学习一些基本的 x86 32 位汇编编程。因此,在追求这一点时,我决定在汇编中实现快速排序(仅对整数进行排序)。首先我做了一个排序函数的 C 版本,然后我做了一个汇编版本。

但是,当将我的程序集版本与我的 C 版本(在 Debian 上使用 gcc 编译)进行比较时,C 版本在 10000 个整数的数组上的执行速度要快 10 倍以上。

所以我的问题是,是否有人可以就我的快速排序组装例程中可以进行的明显优化提供一些反馈。这纯粹是为了教育目的,我不希望在生成高速代码方面击败编译器制造商,但我很想知道我是否犯了任何阻碍速度的明显错误。

C 版本:

void myqsort(int* elems, int sidx, int eidx)
{

    if (sidx < eidx)
    {
        int pivot = elems[eidx];
        int i = sidx;
        for (int j = sidx; j < eidx; j++)
        {
            if (elems[j] <= pivot)
            {
                swap(&elems[i], &elems[j]);
                i = i + 1;
            }
        }
        swap(&elems[i], &elems[eidx]);
        myqsort(elems, sidx, i - 1);
        myqsort(elems, i + 1, eidx);
    }
}
void swap(int* a, int* b)
{
    int tmp = *a;
    *a = *b;
    *b = tmp;
}

程序集版本 (NASM):

;
; void asm_quick_sort(int* elems, int startindex, int endindex)
; Params:
;       elems - pointer to elements to sort - [ebp + 0x8]
;       sid - start index of items - [ebp + 0xC]
;       eid - end index of items - [ebp + 0x10]
asm_quick_sort:

    push ebp
    mov ebp, esp

    push edi
    push esi
    push ebx

    mov eax, dword [ebp + 0xC]  ; store start index,  = i
    mov ebx, dword [ebp + 0x10] ; store end index
    mov esi, dword [ebp + 0x8]  ; store pointer to first element in esi

    cmp eax, ebx
    jnl qsort_done

    mov ecx, eax                        ; ecx = j, = sid
    mov edx, dword [esi + (0x4 * ebx)]  ; pivot element, elems[eid], edx = pivot
qsort_part_loop:
    ; for j = sid; j < eid; j++
    cmp ecx, ebx                    ; if ecx < end index
    jnb qsort_end_part
    ; if elems[j] <= pivot
    cmp edx, dword [esi + (0x4*ecx)]
    jb qsort_cont_loop
    ; do swap, elems[i], elems[j]
    push edx ; save pivot for now
    mov edx, dword [esi + (0x4*ecx)]        ; edx = elems[j]
    mov edi, dword [esi + (0x4*eax)]        ; edi = elems[i]
    mov dword [esi + (0x4*eax)], edx        ; elems[i] = elems[j]
    mov dword [esi + (0x4*ecx)], edi        ; elems[j] = elems[i]
    pop edx ; restore pivot
    ; i++
    add eax, 0x1
qsort_cont_loop:
    add ecx, 0x1
    jmp qsort_part_loop
qsort_end_part:
    ; do swap, elems[i], elems[eid]
    mov edx, dword [esi + (0x4*eax)]        ; edx = elems[i]
    mov edi, dword [esi + (0x4*ebx)]        ; edi = elems[eid]
    mov dword [esi + (0x4*ebx)], edx        ; elems[eidx] = elems[i]
    mov dword [esi + (0x4*eax)], edi        ; elems[i] = elems[eidx]

    ; qsort(elems, sid, i - 1)
    ; qsort(elems, i + 1, eid)
    sub eax, 0x1
    push eax
    push dword [ebp + 0xC]  ; push start idx
    push dword [ebp + 0x8]  ; push elems vector
    call asm_quick_sort
    add esp, 0x8
    pop eax
    add eax, 0x1
    push dword [ebp + 0x10] ; push end idx
    push eax
    push dword [ebp + 0x8]  ; push elems vector
    call asm_quick_sort
    add esp, 0xC


qsort_done:
    pop ebx
    pop esi
    pop edi

    mov esp, ebp
    pop ebp

    ret

我从 C 中调用汇编例程,并使用 clock() 对例程进行计时。

编辑 在纠正了我的 stackoverflowers 同伴指出的错误之后,性能差异不再是问题。

【问题讨论】:

  • 您可以让编译器输出汇编代码并将生成的代码与您的代码进行比较。
  • 一件显而易见的事情是对mysort() 的第二次递归调用可以消除尾调用。由于您不在汇编代码中执行此操作,这对编译器来说已经是一个很好的优势。
  • @rkhb - 我在原始帖子中添加了交换。
  • @EOF - 感谢您的提示,我将研究如何使用尾递归!
  • @pushbp 您想了解如何消除尾递归(您已经使用了它)。提示:不要再次调用函数(递归),你可以直接跳回(附近)开始吗?

标签: c assembly quicksort


【解决方案1】:

您的汇编排序实现中有一个错误,在您解决它之前,速度比较是没有用的。问题是递归调用:

    myqsort(elems, sidx, i - 1);

鉴于i 不一定是sidx,这可能会将小于sidx 的值传递给函数,如果sidx 为0,则包括-1。这在您的C实现:

if (sidx < eidx)

但是在你的汇编版本中:

cmp eax, ebx
jae qsort_done

这是一个无符号比较分支指令!你应该使用jge。由于这个问题,我看到了一个段错误。修复后,根据我的快速测试(使用 -O3 编译),两种实现的性能似乎大致相同。我使用了以下测试驱动程序:

#include <stdlib.h>
#include <stdio.h>

void myqsort(int * elems, int sidx, int eidx);

#define SIZE 100000

int main(int argc, char **argv)
{
    int * elems = malloc(SIZE * sizeof(int));

    for (int j = 0; j < 1000; j++) {

        for (int i = 0; i < SIZE; i++) {
            elems[i] = rand();
        }

        myqsort(elems, 0, SIZE - 1);
    }
    return 0;
}

使用 C 版本,运行时间约为 5.854 秒。 使用汇编版本,它是 5.829 秒(即稍快)。

【讨论】:

  • @rhkb 我无法对您的评论做出正面或反面。是的,cmp 指令比较索引。不,jae 指令不适用于负索引。上面已经解释过了。
  • @rhkb 当比较的值之一(sidxeidx)恰好为负(而另一个不是)时,这些语句将产生不同的结果。
  • @rhkb “如果两者都是肯定的,那么jaejge 之间没有有效的区别” - 我已经在上面的回答中非常简洁地解释了,@987654336 怎么可能@ 值可能是负数,在这种情况下,jaejge 之间存在重要区别。 “你处理的是否定的sidx 还是否定的eidx” - 这个问题对我来说没有意义。 没有处理也没有处理这些情况。我已向 OP 建议如何处理它们。这个建议在我的回答中,您可以在上面看到。
  • @rhkb 当我通过我在答案中提供的驱动程序运行代码时出现了段错误。将jae 更改为jge 后,我不再看到段错误。由于将 -1 索引值视为 0xFFFFFFFF 索引值而导致内存访问越界而发生段错误。
  • 感谢您的cmets,您是正确的,错误是子程序开头的无符号比较。但是我想自己找到段错误的根源,所以我的回复花了一段时间。在进行更多测试时,我还意识到排序不能正常工作。这是递归调用的参数错误,我将更新我的原始帖子。更正错误后,汇编程序确实执行了我的 C 版本。
【解决方案2】:

您可以仅使用 1 个额外的寄存器 EDI 来优化元素的交换,而无需在 EDX 中推送和弹出枢轴值:

mov  edi, dword [esi + (0x4*eax)]   ; edi = elems[i]
xchg dword [esi + (0x4*ecx)], edi   ; elems[j] = edi, edi = elems[j]
mov  dword [esi + (0x4*eax)], edi   ; elems[i] = edi

第二次交换也可以缩短:

mov  edi, dword [esi + (0x4*ebx)]   ; edi = elems[eid]
xchg dword [esi + (0x4*eax)], edi   ; elems[i] = edi, edi = elems[i]
mov  dword [esi + (0x4*ebx)], edi   ; elems[eid] = edi

您可以安全地从 Epilog 代码中删除 mov esp, ebp,因为它是多余的。如果这 3 个pop 运行良好,您就已经知道堆栈指针具有正确的值。

qsort_done:
  pop ebx
  pop esi
  pop edi
  mov esp, ebp    <-- This is useless!
  pop ebp
  ret

【讨论】:

  • 我尝试使用你描述的 xchg,但是它并没有提高子程序的执行时间。
  • @pushbp 即使使用xchg 并没有提高执行时间,它仍然是更优雅的解决方案。这应该很重要!
猜你喜欢
  • 2011-11-04
  • 1970-01-01
  • 2017-10-20
  • 2013-08-11
  • 2021-07-29
  • 2021-07-21
  • 1970-01-01
  • 2012-09-09
  • 1970-01-01
相关资源
最近更新 更多