【问题标题】:Assembler quicksort slower than java array.sort?汇编程序快速排序比 java array.sort 慢?
【发布时间】:2020-06-02 03:46:29
【问题描述】:

我编写了一个汇编程序 QuickSort,发现它比 java 中的 array.sort 慢。我想知道是否有人会知道为什么我无法达到某些高级语言的速度(我假设 C 可能也会更快)

我已经测试了 1,000,000 * 1024 字节。我大约需要 2 秒来完成排序,而 Java 中的 Array.Sort 将在大约 0.8 秒内完成排序

我认为实际移动数据没有任何问题,因为我已经独立测试过,它移动得很快。

    %macro $copyBytes 3
        mov RSI,%1                                  ; Grab the Src Address
        mov RDI,%2                                  ; Grab the Dst Address
        mov rcx,%3                                  ; Grab the length
    %%l:movsb                                       ; Move the data
        loop %%l    
    %endmacro

    %imacro $getAddress 2
        mov %1,r_Table                              ; Start of Table to RSI/RDI
        mov rax,%2                                  ; Lower/Upper to RAX
        mul r_recordLength                          ; Multiply by Record Length
        add %1,rax                                  ; add offset to RSI/RDI
    %endmacro

    %define r_recordLength  r8
    %define r_startPos      r10             
    %define r_noOfBytes     r11
    %define r_Ubound        r15

    %define @left           r10
    %define @right          r11
    %define @pivotValue     r12
    %define @prevLeft       r13

    [section .bss]
        d_startPos          resq 1
        d_noOfBytes         resq 1      
        pivotValue          resb 128                ; This gives tha max sortkey size
        LeftAddress         resq 1
        RightAddress        resq 1
    [section .text] 
;   Call the recursive SORT
    push 0                                          ; Push the 1st Left
    push r_Ubound                                   ; Push the 1st Right
    Call _QSort
    pop rax
    pop rax


;-----------------------------------------------------------------------
;   Recursive Sort
;-----------------------------------------------------------------------
_QSort:

    mov @left,qword[rsp+16]                         ; get the Left Index
    mov @right,qword[rsp+8]                         ; get the Right Index

;   if  left >= right                   ; Exit when Left is not < Right
    cmp @left,@right
        jnl .Exit

;   pivotValue = RandomNo[ [ left + right / 2 ] ] CALCULATE pivotValue
    mov rax,@left                                   ; Grab the left
    add rax,@right                                  ; add the right
    shr rax,1                                       ; and divide by 2
    $getAddress RSI,rax                          ; get the address of this slot
    $copyBytes RSI,pivotValue,qword[d_noOfBytes]     ; and copy it to pivotValue


;   prevLeftIdx = [moveThem]
    push @left                                      ; preserve the Left index
    push @right                                     ; preserve the Right index
    call _moveThem                                  ; do the moves
    pop @right                                      ; restore the Right index
    pop @left                                       ; restore the Left index                                                        
    mov @prevLeft,rax                               ; and save the returned value

;   -------------------------------------
;   Recursive Call - (left, prevLeft - 1)
;   -------------------------------------
    push @left                                      ; preserve the Left index
    push @right                                     ; preserve the Right index
    push @left                                      ; push the 1st parameter (Left)
    mov rax,@prevLeft                               ; get the previous left
    dec rax                                         ; minus 1
    push rax                                        ; push the 2nd parameter
    Call _QSort                                     ; Call yourself     
    pop rax                                         ; Junk this
    pop rax                                         ; Junk this
    pop @right                                      ; restore the Right index
    pop @left                                       ; restore the Left index

;   ----------------------------------
;   Recursive Call - (prevLeft, right)  
;   ----------------------------------
    push @left                                      ; preserve the Left index
    push @right                                     ; preserve the Right index
    push @prevLeft                                  ; push the 1st parameter (previous left
    push @right                                     ; push the 2nd parameter (Right0
    Call _QSort                                     ; Call yourself
    pop rax                                         ; Junk this
    pop rax                                         ; Junk this
    pop @right                                      ; restore the Right index
    pop @left                                       ; restore the Left index

    .Exit:
_QSort_EXIT:
    ret

_moveThem:

;   repeat.while (left <= right)
    cmp @left,@right                                ; Compare Left to Right Index
        jg .Exit                                    ; exit if Left is > Right

;   repeat.while (Key[left] < pivotValue)
;           left    = left + 1
;   end.repeat
    .start1:$getAddress RSI,@left                    ; Get the Start of the record 
            add RSI,qword[d_startPos]               ; add the requested starting position
            mov qword[LeftAddress],RSI              ; and save it
            mov RDI,pivotValue                      ; Get the start of the pivotValue
            mov rcx,qword[d_noOfBytes]              ; Grab the length (LOOP Ctr)
            cld                                     ; Clear the direction flag - left to right
    repe    cmpsb                                   ; Compare the String
            jnl .cont1                              ; If result not < than exit While Loop                                          
            inc @left                               ; get the next table slot
            jmp .start1                             ; and repeat
    .cont1:

    cmp @left,@right                                ; Compare Left to Right Index
        jg .Exit                                    ; exit if Left is > Right

;   repeat.while (Key[right]> pivotValue)
;       right   = right - 1
;   end.repeat
    .start2:$getAddress RSI,@right                   ; Get the Start of the record 
            add RSI,qword[d_startPos]               ; add the requested starting position
            mov qword[RightAddress],RSI             ; and save it
            mov RDI,pivotValue                      ; Get the start of the pivotValue 
            mov rcx,qword[d_noOfBytes]              ; Grab the length (LOOP Ctr)
            cld                                     ; Clear the direction flag - left to right
    repe    cmpsb                                   ; Compare the string    
            jng .cont2                              ; If result not > than exit While Loop  
            dec @right                              ; get the previous table slot
            jmp .start2                             ; and repeat
    .cont2:

    cmp @left,@right                                ; Compare Left to Right Index
        jg .Exit                                    ; exit if Left is > Right
        jl .DoTheMove                               ; Do the move if less than

    ;   No need to do the move if left = right
        inc @left                                   ; increment the left 
        jmp .Exit                                   ; and head for the exit


;   if left <= right - SWAP left and right data
.DoTheMove:
    mov RSI,qword[LeftAddress]                      ; grab the Left address
    mov RDI,qword[RightAddress]                     ; grab the Right address        
    mov rcx,r_recordLength                          ; and grab the record length
    .start3:cmp rcx,8                               ; Have we still got 8 bytes to move
                jb .start4                          ; If not go and pick up the last few bytes
            mov rax,qword[RSI]                      ; grab the left 8 bytes                                     
            mov rbx,qword[RDI]                      ; grab the right 8 bytes
            xchg rax,rbx                            ; swap them over
            mov qword[RSI],rax                      ; put the right 8 bytes to the left
            mov qword[RDI],rbx                      ; put the left 8 bytes to the right 
            add RSI,8                               ; setup RSI for the next 8 bytes
            add RDI,8                               ; setup RDI for the next 8 bytes                                                    
            sub rcx,8                               ; take 8 away from the counter
            jmp .start3                             ; and loop around

    .start4:cmp rcx,0                               ; Have we still got anything to move
                jna .cont4                          ; If not we are done            
            mov ah,byte[RSI]                        ; grab the left byte                                            
            mov al,byte[RDI]                        ; grab the right byte
            xchg ah,al                              ; swap them over
            mov byte[RSI],ah                        ; put the right byte to the left
            mov byte[RDI],al                        ; put the left byte to the right
            add RSI,1                               ; setup RSI for the next byte
            add RDI,1                               ; setup RDI for the next bytes
            sub rcx,1                               ; take 1 away from the counter
            jmp .start4                             ; and loop around
    .cont4:

    inc @left                                       ; Next Left
    dec @right                                      ; Previous Right
    jmp _moveThem                                   ; Back to the Start

.Exit:  
    mov rax,@left                                   ; Return the Left value

_moveThem_EXIT:
    ret
import java.util.Arrays;

class QuickSort {

    public static void Sort(String[] strArray, int lowerIndex, int higherIndex) 
    {        
        int left = lowerIndex;
        int right= higherIndex;
        int x;
        String pivot, temp;

        pivot = strArray[lowerIndex+(higherIndex-lowerIndex)/2];

        while (left <= right) {

          x = strArray[left].compareTo(pivot);
          while (x < 0) {
            left++;  
            x = strArray[left].compareTo(pivot);
          }

          x = strArray[right].compareTo(pivot);
          while (x > 0) {
            right--;
            x = strArray[right].compareTo(pivot);
          }

          if (left <= right) {
            temp = strArray[left];
            strArray[left] = strArray[right];
            strArray[left] = temp;
            left++;
            right--;
          }
        }

        if (lowerIndex < right)
            Sort(strArray,lowerIndex,right);

        if (left < higherIndex)
            Sort(strArray,left,higherIndex);
    }
}

public class V2_11 {

  public static void main(String[] args) {

    String strArray[] = new String[1000001];       
    String letters[] = {"a","b","c","d","e","f","g","h","i","j",
                        "k","l","m","n","o","p","q","r","s","t",
                        "u","v","w","x","y","z"};
    String word;
    String spaces[] = new String[998];
    int random;

  //---------------
  //Build the Array
  //---------------
    System.out.println("Filling Array");  
    System.out.println(java.time.LocalTime.now());  
    for (int i=0;i < 1000000;i++){ 
      word="";
      for (int j=0;j< 26;j++){ 
        random = (int)(Math.random() * 9 + 0);
        word=word+letters[random];
      }
      strArray[i]=word+spaces;
    }   
    System.out.println(java.time.LocalTime.now());

  //--------------
  //Sort the Array
  //--------------
    System.out.println("Sorting");  
    System.out.println(java.time.LocalTime.now());    
  //---Arrays.sort(strArray);
    QuickSort.Sort(strArray,0, 999999);   
    System.out.println(java.time.LocalTime.now());

  //-------------------
  //Display some Reults
  //-------------------
    for (int i=100;i< 110;i++)
    { 
      System.out.println(strArray[i]);
    }
  }
}
#include <stdio.h>
#include <string.h>
#include <time.h>
#include <sys/time.h>
#include <stdlib.h>

    struct timeval start, stop;
    double secs = 0;

    char strArray[1000000][27];
    char pivotValue[27]; 
    char temp[27]; 
    int prevLeftIdx;

int moveThem(int left, int right)
{   int x;

    while (left <= right) {

       x = strcmp(strArray[left],pivotValue);
       while (x < 0) {
         left++;  
            x = strcmp(strArray[left],pivotValue);
       }

       x = strcmp(strArray[right],pivotValue);
       while (x > 0) {
         right--;
         x = strcmp(strArray[right],pivotValue);
       }

       if (left <= right) {
         strcpy(temp,strArray[left]);
         strcpy(strArray[left],strArray[right]);
         strcpy(strArray[right],temp);
         left++;
         right--;
       }
    }

    return left;

}

int QuickSort(int left, int right) 
{

    if (left>=right)
        {return 0;}

    strcpy(pivotValue, strArray[(left+right)/2]);
    prevLeftIdx=moveThem(left,right);

    QuickSort(left,prevLeftIdx-1);
    QuickSort(prevLeftIdx,right);      

}


int main()
{

    char letters[26] = "abcdefghijklmnopqrstuvwxyz";
    char c_Null[1] = "\0";
    char word[27];
    int i,j,random;

  //---------------
  //Build the Array
  //---------------
    puts("Filling Array"); 

    gettimeofday(&start, NULL);
    srand(time(NULL));
    for (i=0;i < 1000000;i++){ 
      for (j=0;j< 26;j++){ 
        random = rand() % 24;
        word[j] = letters[random];
      }
      word[j] = *c_Null;
      strcpy(strArray[i],word);
    }   
    gettimeofday(&stop, NULL);
    secs = (double)(stop.tv_usec - start.tv_usec) / 1000000 + (double)(stop.tv_sec - start.tv_sec);
    printf("time taken %f\n",secs);

  //--------------
  //Sort the Array
  //--------------
    puts("Sorting"); 

    gettimeofday(&start, NULL);   
    QuickSort(0, 999999);   
    gettimeofday(&stop, NULL);
    secs = (double)(stop.tv_usec - start.tv_usec) / 1000000 + (double)(stop.tv_sec - start.tv_sec);
    printf("time taken %f\n",secs);


  //-------------------
  //Display some Reults
  //-------------------
   for (i=0;i<10 ;i++)
   { 
     puts(strArray[i]); 
   }

}
RESULTS FOR C program
Filling Array
time taken 0.245878
Sorting
time taken 0.473481
aaaagajqpbutmaxxhgjkauwmwg
aaaakestelpvsrgiemafwtujev
aaaamnvrdueegvfilkvsabdomd
aaaapspaurwurvecikmjibkwfk
aaaauclobckwflwushhnhiafws
aaabaiueabmqlumijuhlrvmgtf
aaabcqehhoeegkhpeondnbsqjb
aaabmwxdrcpfxlhbmggxvjcaoc
aaabplwowigaxmpjipqxkntilc
aaacjsuqkmbwnglltqsspvvtlr

【问题讨论】:

  • 我希望这个问题的答案类似于C++ code for testing the Collatz conjecture faster than hand-written assembly - why? 的答案——因为手写的汇编器真的很糟糕。
  • edit 包含代码的问题。除非您确实对自己的问题有答案,否则请使用 cmets 来回答,而不是回答。在这种情况下是的,请发布您自己问题的答案。
  • 是的,约瑟夫是正确的。您发明了自己的低效自定义调用约定,在堆栈而不是寄存器上传递参数。而且您使用的是双重递归而不是循环。而且您正在将字符串与慢速 repe cmpsb 进行比较,而不是快速 SIMD (SSE2 pcmpeqb)。交换不需要xchg,只需存储相反的寄存器。您至少要在一次一个字节之前交换 8 个字节的块。循环结构效率低下,有一堆不必要的跳转; Why are loops always compiled into "do...while" style (tail jump)?.
  • IDK 如果有任何算法问题,如枢轴选择;一个好的算法的低效实现足以解释 2 倍的减速。
  • 另一个问题是 Java 正在对引用进行排序,因此它只移动 8(或 4)字节来重定位元素,而不是 1024。

标签: java performance sorting assembly x86-64


【解决方案1】:

找到了一个至少让我满意的解决方案。

作为起点,快速排序算法对 1,000,000 个元素的 qwords 数组执行如下操作。 (以秒为单位显示 5 次运行的时间) .091067、.087505、.087407、.088104、.086983 - 非常快

在一个 1,000,000 * 1024byte 的字符串数组上排序第 1 次 8 字节的时间是:- 2.265867、2.241919、2.281978、2.264712、2.291069 - 所以..不太热

在一个 1,000,000 * 1024byte 的字符串数组上排序第 1 个 8 字节(代码更改后)时间为:- 0.719363, 0.688310, 0.717557, 0.710861, 0.746405 - 如果不比 Java 好,也可以比较

代码修复包括替换

     mov rsi,Src Address
     mov rdi,Dst Address
     mov rcx,SortKey Length
repe cmpsb

    mov rax,[Src Address]
    bswap rax
    mov rbx,[Dst Address]
    bswap
    cmp rax,rbx

我猜你是一次比较 8 个字节,它会更快是有道理的。这显然不是最好的方法,但不会被卷入我不想进入的事情,它会做得很好。

作为旁注,代码需要完成以处理可变长度的键,但这只是坐下来完成工作的情况。我希望这对任何偶然发现此线程的人有所帮助。 (特别感谢@PeterCordes 的努力)

【讨论】:

  • 是的,我相信microcode startup overhead for rep cmpsb 会严重破坏性能。与我提到的其他小事不同(比必要的推送更多),这确实会成为管道的瓶颈。不过,您真的要复制整个 1024 字节的数组元素吗?或者这就像你的 C 测试用例 char strArray[1000000][27]; 一样,其中元素足够小,复制它们可能会在消除一定程度的间接性和缓存局部性方面为自己付出代价?
  • @PeterCordes - 大约 0.7 秒出现的版本只是移动地址,所以我确实使用了完整的 1024 字节记录(当你只是移动 64 位地址时显然不相关)。我确实查看了 strcpy-sse2-unaligned 中的代码,但它违反了该项目的设计原则。即移植到其他处理器。仅对 qwords 进行排序的版本非常快啊!!!
  • SSE2 是 x86-64 的基线。如果您使用 x86-64 asm 编写,则可以始终使用 SSE2,而无需检查 CPUID 以查看它是否受支持。只有像 SSE3 和更高版本、popcnt、BMI 等之后的扩展不是基线。如果您需要移植到 x86-64 以外的 ISA,那么汇编语言可能是错误的选择。 :P 也就是说,用 C 语言编写这部分可能是个好主意;听起来您从 C 编译器中获得了非常好的结果,与您的手写 asm 相当。尽管您一直在乱扔数字,但没有确切说明它们的用途(排序或不排序),所以 IDK。
  • @PeterCordes - 我所做的工作是为了我所写的语言。这并不是为了在性能方面获得最好的结果,它实际上只是为了让自己开心并边走边学。现在是困难时期,编写代码是一种非常廉价的爱好。
猜你喜欢
  • 2021-03-11
  • 2021-06-06
  • 2010-10-04
  • 2017-03-18
  • 2014-05-17
  • 1970-01-01
  • 2016-12-23
  • 1970-01-01
  • 2021-06-19
相关资源
最近更新 更多