가장 빠른 고정 길이 6 int 배열


401

다른 스택 오버플로 질문에 답하면 ( 질문 ) 흥미로운 하위 문제가 발생했습니다. 6 개의 정수 배열을 정렬하는 가장 빠른 방법은 무엇입니까?

질문이 매우 낮은 수준이므로

  • 우리는 라이브러리를 사용할 수 있다고 가정 할 수 없으며 (통화 자체에는 비용이 있습니다) 평범한 C 만
  • 명령 파이프 라인 비우기 ( 비용 이 매우 높음) 를 피하려면 분기, 점프 및 다른 모든 종류의 제어 흐름 차단 ( &&또는 시퀀스 지점 뒤에 숨겨져있는 것과 같은)을 최소화해야합니다 ||.
  • 공간이 제한되어 있고 레지스터를 최소화하고 메모리 사용이 문제입니다. 이상적으로는 장소에 따라 정렬하는 것이 가장 좋습니다.

실제로이 질문은 소스 길이를 최소화하는 것이 아니라 실행 시간을 목표로하는 일종의 골프입니다. 책의 제목에서 사용 나는 'Zening'코드를 호출하는 코드 최적화의 선 에 의해 마이클 애 브라시 와 그 속편 .

왜 흥미로운 지에 대해서는 몇 가지 계층이 있습니다.

  • 예는 간단하고 이해하기 쉽고 측정하기 쉬운 C 기술이 아닙니다.
  • 문제에 대한 올바른 알고리즘 선택의 효과뿐만 아니라 컴파일러 및 기본 하드웨어의 효과도 보여줍니다.

여기 내 참조 (순진하고 최적화되지 않은) 구현 및 테스트 세트가 있습니다.

#include <stdio.h>

static __inline__ int sort6(int * d){

    char j, i, imin;
    int tmp;
    for (j = 0 ; j < 5 ; j++){
        imin = j;
        for (i = j + 1; i < 6 ; i++){
            if (d[i] < d[imin]){
                imin = i;
            }
        }
        tmp = d[j];
        d[j] = d[imin];
        d[imin] = tmp;
    }
}

static __inline__ unsigned long long rdtsc(void)
{
  unsigned long long int x;
     __asm__ volatile (".byte 0x0f, 0x31" : "=A" (x));
     return x;
}

int main(int argc, char ** argv){
    int i;
    int d[6][5] = {
        {1, 2, 3, 4, 5, 6},
        {6, 5, 4, 3, 2, 1},
        {100, 2, 300, 4, 500, 6},
        {100, 2, 3, 4, 500, 6},
        {1, 200, 3, 4, 5, 600},
        {1, 1, 2, 1, 2, 1}
    };

    unsigned long long cycles = rdtsc();
    for (i = 0; i < 6 ; i++){
        sort6(d[i]);
        /*
         * printf("d%d : %d %d %d %d %d %d\n", i,
         *  d[i][0], d[i][6], d[i][7],
         *  d[i][8], d[i][9], d[i][10]);
        */
    }
    cycles = rdtsc() - cycles;
    printf("Time is %d\n", (unsigned)cycles);
}

원시 결과

많은 변형이 커지면서 여기 에서 찾을 수있는 테스트 스위트에 모두 모았습니다. . Kevin Stock 덕분에 사용 된 실제 테스트는 위에 표시된 것보다 약간 순진합니다. 자신의 환경에서 컴파일하고 실행할 수 있습니다. 다른 대상 아키텍처 / 컴파일러의 동작에 상당히 관심이 있습니다. (좋아요, 답을 넣으면 새로운 결과 집합의 모든 제공자를 +1 할 것입니다).

나는 1 년 전에 Daniel Stutzbach (골프 용)에게 해답을주었습니다.

Linux 64 비트, gcc 4.6.1 64 비트, Intel Core 2 Duo E8400, -O2

  • qsort 라이브러리 함수 직접 호출 : 689.38
  • 순진한 구현 (삽입 정렬) : 285.70
  • 삽입 정렬 (Daniel Stutzbach) : 142.12
  • 삽입 정렬 언 롤링 : 125.47
  • 순위 순서 : 102.26
  • 레지스터 순위 : 58.03
  • 정렬 네트워크 (Daniel Stutzbach) : 111.68
  • 정렬 네트워크 (Paul R) : 66.36
  • 빠른 스왑으로 네트워크 12 정렬하기 : 58.86
  • Sorting Networks 12 재정렬 Swap : 53.74
  • Sorting Networks 12 재정렬 Simple Swap : 31.54
  • 재순환 정렬 네트워크 (빠른 교체 포함) : 31.54
  • 재 순서 정렬 네트워크 (빠른 교체 V2 포함) : 33.63
  • 인라인 버블 정렬 (파올로 본 지니) : 48.85
  • 펼쳐진 삽입 정렬 (Paolo Bonzini) : 75.30

Linux 64 비트, gcc 4.6.1 64 비트, Intel Core 2 Duo E8400, -O1

  • qsort 라이브러리 함수 직접 호출 : 705.93
  • 순진한 구현 (삽입 정렬) : 135.60
  • 삽입 정렬 (Daniel Stutzbach) : 142.11
  • 삽입 정렬 언 롤링 : 126.75
  • 순위 순서 : 46.42
  • 레지스터 순위 : 43.58
  • 정렬 네트워크 (Daniel Stutzbach) : 115.57
  • 정렬 네트워크 (Paul R) : 64.44
  • 빠른 스왑으로 네트워크 12 정렬하기 : 61.98
  • Sorting Networks 12 재정렬 Swap : 54.67
  • Sorting Networks 12 재정렬 Simple Swap : 31.54
  • 재순환 정렬 네트워크 (빠른 교체 포함) : 31.24
  • 재순환 정렬 네트워크 (빠른 교체 V2 포함) : 33.07
  • 인라인 버블 정렬 (파올로 본 지니) : 45.79
  • 펼쳐진 삽입 정렬 (Paolo Bonzini) : 80.15

놀랍게도 여러 프로그램에서 O2가 O1보다 효율적 이기 때문에 -O1과 -O2 결과를 모두 포함했습니다 . 어떤 최적화가이 효과를 가지는지 궁금합니다.

제안 된 솔루션에 대한 의견

삽입 정렬 (Daniel Stutzbach)

예상대로 분기를 최소화하는 것이 좋습니다.

정렬 네트워크 (Daniel Stutzbach)

삽입 정렬보다 낫습니다. 주요 효과가 외부 루프를 피하지 못했는지 궁금했습니다. 나는 그것을 확인하기 위해 롤링되지 않은 삽입 정렬로 시도했으며 실제로 우리는 대략 같은 수치를 얻었습니다 (코드는 here ).

정렬 네트워크 (Paul R)

지금까지 최고입니다. 테스트에 사용한 실제 코드는 다음과 같습니다 . 왜 다른 정렬 네트워크 구현보다 거의 두 배나 빠른지 아직 모릅니다. 매개 변수 전달? 빠른 최대?

빠른 스왑으로 네트워크 정렬 12 SWAP

Daniel Stutzbach가 제안한 것처럼 12 개의 스왑 정렬 네트워크를 분기없는 빠른 스왑과 결합했습니다 (코드는 다음과 같습니다) ) . 1 더 적은 스왑을 사용하여 예상 할 수 있듯이 적은 마진 (약 5 %)으로 지금까지 가장 빠릅니다.

또한 분기없는 스왑은 PPC 아키텍처에서 if를 사용하는 간단한 것보다 훨씬 효율적이지 않은 것으로 보입니다.

호출 라이브러리 qsort

다른 참조 포인트를 제공하기 위해 라이브러리 qsort (코드는 here )를 호출하는 것이 좋습니다 . 예상보다 훨씬 느리다 : 10 ~ 30 배 느리다 ... 새로운 테스트 스위트에서 명백해 졌기 때문에 주요 문제는 첫 번째 호출 후 라이브러리의 초기로드 인 것처럼 보이고 다른 것과 비교할 때 나쁘지 않습니다. 버전. 내 리눅스에서는 3 배에서 20 배 정도 느립니다. 다른 아키텍처의 테스트에 사용되는 일부 아키텍처에서는 훨씬 빠릅니다 (라이브러리 qsort가 더 복잡한 API를 사용하기 때문에 실제로 놀랍습니다).

순위

Rex Kerr은 완전히 다른 방법을 제안했습니다. 배열의 각 항목마다 최종 위치를 직접 계산합니다. 계산 순위 순서에 분기가 필요하지 않기 때문에 효율적입니다. 이 방법의 단점은 배열의 메모리 양 (배열의 한 사본과 순위 순서를 저장하는 변수의 사본)의 3 배가 걸린다는 것입니다. 성능 결과는 매우 놀랍고 흥미 롭습니다. 32 비트 OS 및 Intel Core2 Quad E8300을 사용하는 참조 아키텍처에서주기 수는 분기 스왑이있는 정렬 네트워크와 같이 1000보다 약간 작습니다. 그러나 내 64 비트 상자 (Intel Core2 Duo)에서 컴파일되고 실행될 때 훨씬 더 잘 수행되었습니다. 지금까지 가장 빠릅니다. 나는 진실한 이유를 마침내 발견했다. 내 32 비트 상자는 gcc 4.4.1 및 64 비트 상자 gcc 4.4를 사용합니다.

업데이트 :

위에 공개 된 그림에서 알 수 있듯이이 효과는 이후 버전의 gcc에 의해 여전히 향상되었으며 순위 순서는 다른 대안보다 일관되게 두 배 빨라졌습니다.

재정렬 된 스왑으로 정렬 네트워크 12

gcc 4.4.3을 사용한 Rex Kerr 제안의 놀라운 효율성으로 인해 3 배나 많은 메모리를 사용하는 프로그램이 분기없는 정렬 네트워크보다 더 빠를 수 있을까? 내 가설은 쓰기 후 읽은 종류의 종속성이 적어 x86의 슈퍼 스칼라 명령 스케줄러를 더 잘 사용할 수 있다는 것입니다. 그것은 나에게 아이디어를 주었다 : 쓰기 의존성 후 읽기를 최소화하기 위해 재정렬 스왑. 더 간단히 말하면 SWAP(1, 2); SWAP(0, 2);두 번째 공통 메모리 셀에 액세스하기 때문에 두 번째 스왑을 수행하기 전에 첫 번째 스왑이 완료 될 때까지 기다려야합니다. 당신이 할 SWAP(1, 2); SWAP(4, 5);때 프로세서는 병렬로 실행할 수 있습니다. 나는 그것을 시도하고 예상대로 작동하며 정렬 네트워크는 약 10 % 더 빠르게 실행됩니다.

간단한 스왑으로 네트워크 12 정렬

원래 게시물 Steinar H. Gunderson이 제안한 1 년 후, 컴파일러를 현명하게 바꾸고 스왑 코드를 단순하게 유지해서는 안된다고 제안했습니다. 결과 코드가 약 40 % 빠르므로 실제로 좋은 생각입니다! 또한 더 많은 사이클을 절약 할 수있는 x86 인라인 어셈블리 코드를 사용하여 수작업으로 최적화 된 스왑을 제안했습니다. 가장 놀랍게도 (프로그래머의 심리학에 관한 책은 1 년 전 어느 누구도 그 버전의 스왑을 시도하지 않았다는 것입니다. 테스트하는 데 사용한 코드는 여기에 있습니다 . 다른 사람들은 C 빠른 스왑을 작성하는 다른 방법을 제안했지만 괜찮은 컴파일러를 사용하는 간단한 것과 동일한 성능을 제공합니다.

"최상의"코드는 다음과 같습니다.

static inline void sort6_sorting_network_simple_swap(int * d){
#define min(x, y) (x<y?x:y)
#define max(x, y) (x<y?y:x) 
#define SWAP(x,y) { const int a = min(d[x], d[y]); \
                    const int b = max(d[x], d[y]); \
                    d[x] = a; d[y] = b; }
    SWAP(1, 2);
    SWAP(4, 5);
    SWAP(0, 2);
    SWAP(3, 5);
    SWAP(0, 1);
    SWAP(3, 4);
    SWAP(1, 4);
    SWAP(0, 3);
    SWAP(2, 5);
    SWAP(1, 3);
    SWAP(2, 4);
    SWAP(2, 3);
#undef SWAP
#undef min
#undef max
}

테스트 세트를 믿는다면 (그리고 예, 상당히 나쁘다. 단순하고 단순하며 측정 대상을 이해하기 쉽다는 장점이있다), 한 종류의 결과 코드의 평균주기 수는 40주기 미만이다 ( 6 개의 테스트가 실행됩니다). 이는 각 스왑을 평균 4 주기로 설정했습니다. 나는 그것을 놀랍게도 빨리 부릅니다. 다른 개선 사항이 있습니까?


2
int에 대한 제약이 있습니까? 예를 들어, 우리는 가정 할 수있는 2 X, Y에 대한 x-y그리고 x+y하지 않습니다 언더 나 오버 플로우 원인?
Matthieu M.

3
내 12 스왑 정렬 네트워크와 Paul의 분기없는 스왑 기능을 결합해야합니다. 그의 솔루션은 모든 매개 변수를 배열에 대한 단일 포인터 대신 스택의 개별 요소로 전달합니다. 그것은 또한 차이를 만들 수 있습니다.
Daniel Stutzbach

2
64 비트에서 rdtsc의 올바른 구현은 rdtsc __asm__ volatile (".byte 0x0f, 0x31; shlq $32, %%rdx; orq %%rdx, %0" : "=a" (x) : : "rdx");가 EDX : EAX에 답을 넣는 반면 GCC는 단일 64 비트 레지스터에이를 기대하기 때문입니다. -O3에서 컴파일하여 버그를 볼 수 있습니다. 또한 빠른 SWAP에 대한 Paul R의 의견을 아래에서 참조하십시오.
Paolo Bonzini

3
@ 타일러 : 브랜치없이 어셈블리 수준에서 어떻게 구현합니까?
Loren Pechtel

4
@Loren : 각각 이보다 크거나 작은 지 여부 CMP EAX, EBX; SBB EAX, EAXEAX따라 0 또는 0xFFFFFFFF를 넣습니다 . ( "carry에 추가")에 해당하는 "빌리와 함께 빼기 "입니다. 상태는 당신이 참조 비트 이다 캐리 비트. 그럼 다시, 나는 기억 하고 및 펜티엄 4 대에 처리량 끔찍한 대기 시간을 가지고 하고 , 두 번 여전히 코어 CPU에서 느린했다. 80386 이후 조건부 저장 및 조건부 이동 명령도 있지만 속도가 느립니다. EAXEBXSBBADCADCSBBADDSUBSETccCMOVcc
j_random_hacker

답변:


162

최적화를 위해서는 항상 테스트, 테스트, 테스트하는 것이 가장 좋습니다. 적어도 네트워크 정렬과 삽입 정렬을 시도합니다. 베팅하는 경우 과거 경험을 바탕으로 삽입 금액에 돈을 넣었습니다.

입력 데이터에 대해 알고 있습니까? 일부 알고리즘은 특정 종류의 데이터에서 더 잘 수행됩니다. 예를 들어, 정렬 정렬은 정렬되거나 거의 정렬 된 데이터에서 더 잘 수행되므로 평균보다 거의 정렬 된 데이터가있을 경우 더 나은 선택이 될 것입니다.

게시 한 알고리즘은 삽입 정렬과 유사하지만 더 많은 비교 비용으로 스왑 수를 최소화 한 것으로 보입니다. 분기가 명령 파이프 라인을 정지시킬 수 있기 때문에 비교는 스왑보다 훨씬 비쌉니다.

삽입 정렬 구현은 다음과 같습니다.

static __inline__ int sort6(int *d){
        int i, j;
        for (i = 1; i < 6; i++) {
                int tmp = d[i];
                for (j = i; j >= 1 && tmp < d[j-1]; j--)
                        d[j] = d[j-1];
                d[j] = tmp;
        }
}

정렬 네트워크를 구축하는 방법은 다음과 같습니다. 먼저이 사이트 를 사용 하여 적절한 길이의 네트워크에 대한 최소 SWAP 매크로 세트를 생성하십시오. 함수로 마무리하면 다음과 같은 이점이 있습니다.

static __inline__ int sort6(int * d){
#define SWAP(x,y) if (d[y] < d[x]) { int tmp = d[x]; d[x] = d[y]; d[y] = tmp; }
    SWAP(1, 2);
    SWAP(0, 2);
    SWAP(0, 1);
    SWAP(4, 5);
    SWAP(3, 5);
    SWAP(3, 4);
    SWAP(0, 3);
    SWAP(1, 4);
    SWAP(2, 5);
    SWAP(2, 4);
    SWAP(1, 3);
    SWAP(2, 3);
#undef SWAP
}

9
+1 : 좋습니다. 위의 직접 코딩하고 경험적으로 파생 된 네트워크에서 13 개가 아닌 12 개의 교환으로 수행했습니다. 내가 당신을 위해 네트워크를 생성하는 사이트에 대한 링크를 할 수 있다면 또 다른 +1을 줄 것입니다.
Paul R

9
대부분의 요청이 작은 크기의 배열이 될 것으로 예상되는 경우 범용 정렬 기능에 환상적인 아이디어입니다. 이 절차를 사용하여 최적화하려는 경우에 switch 문을 사용하십시오. 기본 경우 라이브러리 정렬 함수를 사용하십시오.
Mark Ransom

5
@Mark 좋은 라이브러리 정렬 함수는 이미 작은 배열을위한 빠른 경로를 가지고 있습니다. 현대의 많은 라이브러리는 재귀 적 QuickSort 또는 MergeSort를 사용하여 재귀 후 InsertionSort로 전환합니다 n < SMALL_CONSTANT.
Daniel Stutzbach

3
@Mark 음, C 라이브러리 정렬 함수는 함수 포터를 통해 비교 연산을 지정해야합니다. 모든 비교에 대해 함수를 호출하는 오버 헤드는 엄청납니다. 일반적으로 이것은 가장 깨끗한 방법입니다. 프로그램에서 중요한 경로는 거의 없기 때문입니다. 그러나 중요한 경로 인 경우 정수와 정확히 6 개를 정렬한다는 것을 알고 있으면 훨씬 빠르게 정렬 할 수 있습니다. :)
Daniel Stutzbach

7
@tgwh : XOR 스왑은 거의 항상 나쁜 생각입니다.
Paul R

63

다음을 사용한 구현이 있습니다. 정렬 네트워크를 .

inline void Sort2(int *p0, int *p1)
{
    const int temp = min(*p0, *p1);
    *p1 = max(*p0, *p1);
    *p0 = temp;
}

inline void Sort3(int *p0, int *p1, int *p2)
{
    Sort2(p0, p1);
    Sort2(p1, p2);
    Sort2(p0, p1);
}

inline void Sort4(int *p0, int *p1, int *p2, int *p3)
{
    Sort2(p0, p1);
    Sort2(p2, p3);
    Sort2(p0, p2);  
    Sort2(p1, p3);  
    Sort2(p1, p2);  
}

inline void Sort6(int *p0, int *p1, int *p2, int *p3, int *p4, int *p5)
{
    Sort3(p0, p1, p2);
    Sort3(p3, p4, p5);
    Sort2(p0, p3);  
    Sort2(p2, p5);  
    Sort4(p1, p2, p3, p4);  
}

당신은 정말 효율적으로 분기가 필요합니다 minmax구현이 필요합니다. 왜냐하면이 코드가 효과적으로 순서 minmax연산 (각각 13 개)으로 요약되기 때문입니다 . 나는 이것을 독자를위한 연습으로 남겨둔다.

이 구현은 벡터화 (예 : SIMD-대부분의 SIMD ISA에는 벡터 최소 / 최대 명령이 있음) 및 GPU 구현 (예 : CUDA-분기가없는 경우 워프 발산에 문제가 없음)에 쉽게 적용됩니다.

다음을 참조하십시오 : 매우 작은 목록을 정렬하는 빠른 알고리즘 구현



1
@Paul : 실제 CUDA 사용 컨텍스트에서 가장 좋은 대답입니다. golf x64 컨텍스트에서도 (그리고 얼마인지) 확인하고 결과를 게시합니다.
kriss

1
Sort3(a+b+c)-(min+max)중앙 번호 라고 언급하면 ​​더 빠릅니다 (대부분의 아키텍처에서) .
Rex Kerr

1
@ 렉스 : 알 겠어요-좋아 보인다. AltiVec 및 SSE와 같은 SIMD 아키텍처의 경우 동일한 명령주기 수 (최대 및 최소는 더하기 / 빼기와 같은 단일주기 명령 임)이지만 일반적인 스칼라 CPU의 경우 방법이 더 좋습니다.
Paul R

2
조건부 이동 명령으로 GCC가 최소값을 최적화하게하면 33 % 속도 향상 : #define SWAP(x,y) { int dx = d[x], dy = d[y], tmp; tmp = d[x] = dx < dy ? dx : dy; d[y] ^= dx ^ tmp; }. 여기에서는? : for d [y]를 사용하지 않습니다. 성능이 약간 떨어지지 만 소음이 거의 없습니다.
Paolo Bonzini

45

이들은 정수이고 비교가 빠르기 때문에 각각의 순위 순서를 직접 계산해보십시오.

inline void sort6(int *d) {
  int e[6];
  memcpy(e,d,6*sizeof(int));
  int o0 = (d[0]>d[1])+(d[0]>d[2])+(d[0]>d[3])+(d[0]>d[4])+(d[0]>d[5]);
  int o1 = (d[1]>=d[0])+(d[1]>d[2])+(d[1]>d[3])+(d[1]>d[4])+(d[1]>d[5]);
  int o2 = (d[2]>=d[0])+(d[2]>=d[1])+(d[2]>d[3])+(d[2]>d[4])+(d[2]>d[5]);
  int o3 = (d[3]>=d[0])+(d[3]>=d[1])+(d[3]>=d[2])+(d[3]>d[4])+(d[3]>d[5]);
  int o4 = (d[4]>=d[0])+(d[4]>=d[1])+(d[4]>=d[2])+(d[4]>=d[3])+(d[4]>d[5]);
  int o5 = 15-(o0+o1+o2+o3+o4);
  d[o0]=e[0]; d[o1]=e[1]; d[o2]=e[2]; d[o3]=e[3]; d[o4]=e[4]; d[o5]=e[5];
}

@Rex : gcc -O1을 사용하면 1000 사이클 미만이며 정렬 네트워크보다 속도는 느리지 만 속도는 느립니다. 코드를 향상시킬 아이디어가 있습니까? 어쩌면 배열 복사를 피할 수 있다면 ...
kriss

@ kriss : -O2를 사용하면 정렬 네트워크보다 빠릅니다. -O2가 좋지 않은 이유가 있습니까? 아니면 -O2에서도 속도가 느린 이유가 있습니까? 아마도 기계 아키텍처의 차이일까요?
렉스 커

1
@Rex : 죄송합니다. 첫눈에> vs> = 패턴이 누락되었습니다. 모든 경우에 작동합니다.
kriss

3
@kriss : 아하. 그것은 놀랄만 한 일이 아닙니다. 많은 변수가 떠 다니고 있으며 레지스터 등에서 신중하게 주문하고 캐시해야합니다.
Rex Kerr

2
@SSpoke 0+1+2+3+4+5=15그들 중 하나가 누락 되었으므로 15에서 나머지를 합한 것에서 하나가 누락되었습니다
Glenn Teitelbaum

35

1 년 늦게 파티에 온 것 같지만 여기에 ...

gcc 4.5.2에서 생성 된 어셈블리를 살펴보면 모든 스왑에 대해로드와 저장이 수행되고 있으며 실제로는 필요하지 않습니다. 6 개의 값을 레지스터에로드하고 정렬 한 다음 다시 메모리에 저장하는 것이 좋습니다. 나는 매장에 가해지는 부하를 가능한 한 가깝게 주문했다. Steinar H. Gunderson의 SWAP 매크로도 사용했습니다. 업데이트 : gcc가 Gunderson과 유사한 것으로 변환되는 Paolo Bonzini의 SWAP 매크로로 전환했지만 gcc는 명시 적 어셈블리로 제공되지 않으므로 지침을 더 잘 주문할 수 있습니다.

더 나은 순서가있을 수 있지만 최고의 성능으로 제공된 재정렬 된 스왑 네트워크와 동일한 스왑 순서를 사용했습니다. 더 많은 시간을 찾으면 많은 순열을 생성하고 테스트합니다.

4000 개 이상의 어레이를 고려하도록 테스트 코드를 변경하고 각주기를 정렬하는 데 필요한 평균주기 수를 보여주었습니다. i5-650에서 ~ 34.1 사이클 / 정렬 (-O3 사용)을 얻는 반면 원래의 재정렬 된 정렬 네트워크가 ~ 65.3 사이클 / 정렬 (-O1, 비트 -O2 및 -O3 사용)을 얻는 것과 비교했습니다.

#include <stdio.h>

static inline void sort6_fast(int * d) {
#define SWAP(x,y) { int dx = x, dy = y, tmp; tmp = x = dx < dy ? dx : dy; y ^= dx ^ tmp; }
    register int x0,x1,x2,x3,x4,x5;
    x1 = d[1];
    x2 = d[2];
    SWAP(x1, x2);
    x4 = d[4];
    x5 = d[5];
    SWAP(x4, x5);
    x0 = d[0];
    SWAP(x0, x2);
    x3 = d[3];
    SWAP(x3, x5);
    SWAP(x0, x1);
    SWAP(x3, x4);
    SWAP(x1, x4);
    SWAP(x0, x3);
    d[0] = x0;
    SWAP(x2, x5);
    d[5] = x5;
    SWAP(x1, x3);
    d[1] = x1;
    SWAP(x2, x4);
    d[4] = x4;
    SWAP(x2, x3);
    d[2] = x2;
    d[3] = x3;

#undef SWAP
#undef min
#undef max
}

static __inline__ unsigned long long rdtsc(void)
{
    unsigned long long int x;
    __asm__ volatile ("rdtsc; shlq $32, %%rdx; orq %%rdx, %0" : "=a" (x) : : "rdx");
    return x;
}

void ran_fill(int n, int *a) {
    static int seed = 76521;
    while (n--) *a++ = (seed = seed *1812433253 + 12345);
}

#define NTESTS 4096
int main() {
    int i;
    int d[6*NTESTS];
    ran_fill(6*NTESTS, d);

    unsigned long long cycles = rdtsc();
    for (i = 0; i < 6*NTESTS ; i+=6) {
        sort6_fast(d+i);
    }
    cycles = rdtsc() - cycles;
    printf("Time is %.2lf\n", (double)cycles/(double)NTESTS);

    for (i = 0; i < 6*NTESTS ; i+=6) {
        if (d[i+0] > d[i+1] || d[i+1] > d[i+2] || d[i+2] > d[i+3] || d[i+3] > d[i+4] || d[i+4] > d[i+5])
            printf("d%d : %d %d %d %d %d %d\n", i,
                    d[i+0], d[i+1], d[i+2],
                    d[i+3], d[i+4], d[i+5]);
    }
    return 0;
}

내가 변경된 테스트 스위트를 수정 또한 더 많은 테스트를 종류에 따라 시계를보고하고 실행에합니다 (CMP 기능이 아니라 핸들 정수 오버 플로우로 업데이트), 여기에 몇 가지 다른 아키텍처에 결과입니다. AMD CPU에서 테스트를 시도했지만 사용 가능한 X6 1100T에서 rdtsc가 신뢰할 수 없습니다.

Clarkdale (i5-650)
==================
Direct call to qsort library function      635.14   575.65   581.61   577.76   521.12
Naive implementation (insertion sort)      538.30   135.36   134.89   240.62   101.23
Insertion Sort (Daniel Stutzbach)          424.48   159.85   160.76   152.01   151.92
Insertion Sort Unrolled                    339.16   125.16   125.81   129.93   123.16
Rank Order                                 184.34   106.58   54.74    93.24    94.09
Rank Order with registers                  127.45   104.65   53.79    98.05    97.95
Sorting Networks (Daniel Stutzbach)        269.77   130.56   128.15   126.70   127.30
Sorting Networks (Paul R)                  551.64   103.20   64.57    73.68    73.51
Sorting Networks 12 with Fast Swap         321.74   61.61    63.90    67.92    67.76
Sorting Networks 12 reordered Swap         318.75   60.69    65.90    70.25    70.06
Reordered Sorting Network w/ fast swap     145.91   34.17    32.66    32.22    32.18

Kentsfield (Core 2 Quad)
========================
Direct call to qsort library function      870.01   736.39   723.39   725.48   721.85
Naive implementation (insertion sort)      503.67   174.09   182.13   284.41   191.10
Insertion Sort (Daniel Stutzbach)          345.32   152.84   157.67   151.23   150.96
Insertion Sort Unrolled                    316.20   133.03   129.86   118.96   105.06
Rank Order                                 164.37   138.32   46.29    99.87    99.81
Rank Order with registers                  115.44   116.02   44.04    116.04   116.03
Sorting Networks (Daniel Stutzbach)        230.35   114.31   119.15   110.51   111.45
Sorting Networks (Paul R)                  498.94   77.24    63.98    62.17    65.67
Sorting Networks 12 with Fast Swap         315.98   59.41    58.36    60.29    55.15
Sorting Networks 12 reordered Swap         307.67   55.78    51.48    51.67    50.74
Reordered Sorting Network w/ fast swap     149.68   31.46    30.91    31.54    31.58

Sandy Bridge (i7-2600k)
=======================
Direct call to qsort library function      559.97   451.88   464.84   491.35   458.11
Naive implementation (insertion sort)      341.15   160.26   160.45   154.40   106.54
Insertion Sort (Daniel Stutzbach)          284.17   136.74   132.69   123.85   121.77
Insertion Sort Unrolled                    239.40   110.49   114.81   110.79   117.30
Rank Order                                 114.24   76.42    45.31    36.96    36.73
Rank Order with registers                  105.09   32.31    48.54    32.51    33.29
Sorting Networks (Daniel Stutzbach)        210.56   115.68   116.69   107.05   124.08
Sorting Networks (Paul R)                  364.03   66.02    61.64    45.70    44.19
Sorting Networks 12 with Fast Swap         246.97   41.36    59.03    41.66    38.98
Sorting Networks 12 reordered Swap         235.39   38.84    47.36    38.61    37.29
Reordered Sorting Network w/ fast swap     115.58   27.23    27.75    27.25    26.54

Nehalem (Xeon E5640)
====================
Direct call to qsort library function      911.62   890.88   681.80   876.03   872.89
Naive implementation (insertion sort)      457.69   236.87   127.68   388.74   175.28
Insertion Sort (Daniel Stutzbach)          317.89   279.74   147.78   247.97   245.09
Insertion Sort Unrolled                    259.63   220.60   116.55   221.66   212.93
Rank Order                                 140.62   197.04   52.10    163.66   153.63
Rank Order with registers                  84.83    96.78    50.93    109.96   54.73
Sorting Networks (Daniel Stutzbach)        214.59   220.94   118.68   120.60   116.09
Sorting Networks (Paul R)                  459.17   163.76   56.40    61.83    58.69
Sorting Networks 12 with Fast Swap         284.58   95.01    50.66    53.19    55.47
Sorting Networks 12 reordered Swap         281.20   96.72    44.15    56.38    54.57
Reordered Sorting Network w/ fast swap     128.34   50.87    26.87    27.91    28.02

레지스터 변수에 대한 아이디어는 Rex Kerr의 "Rank Order"솔루션에 적용되어야합니다. 그것은 가장 -O3빨라야 할 것이고 , 아마도 최적화는 비생산적이지 않을 것입니다.
cdunn2001

1
@ cdunn2001 방금 테스트했는데 개선이 보이지 않습니다 (-O0 및 -Os의 몇 사이클 제외). asm을 보면 gcc는 이미 레지스터를 사용하고 memcpy에 대한 호출을 제거하는 것을 알아 냈습니다.
Kevin 주식

테스트 스왑에 간단한 스왑 버전을 추가하고 싶습니까? 수작업으로 최적화 된 어셈블리 빠른 스왑과 비교하는 것이 흥미로울 것 같습니다.
kriss

1
귀하의 코드는 여전히 Gunderson의 스왑을 사용합니다 #define SWAP(x,y) { int oldx = x; x = x < y ? x : y; y ^= oldx ^ x; }.
Paolo Bonzini

@Paolo Bonzini : 예, 아직 시간이 없었지만 테스트 케이스를 추가하려고합니다. 그러나 인라인 어셈블리를 피할 것입니다.
kriss

15

며칠 전에 Google 에서이 질문에 우연히 발견했습니다. 왜냐하면 6 개의 정수로 된 고정 길이 배열을 신속하게 정렬해야하기 때문입니다. 그러나 필자의 경우 정수는 32 비트 대신 8 비트에 불과하며 C 만 사용해야하는 엄격한 요구 사항은 없습니다. 어쨌든 누군가에게 도움이 될 수 있기 때문에 결과를 공유 할 것이라고 생각했습니다 ...

SSE를 사용하여 비교 및 ​​스왑 작업을 가능한 한 벡터화하는 어셈블리의 네트워크 정렬 변형을 구현했습니다. 배열을 완전히 정렬하려면 6 개의 "패스"가 필요합니다. PADDB (벡터화 된 추가) 만 사용하고 일부 경우에는 PAND (비트 AND) 명령을 사용하여 PCMPGTB (벡터화 된 비교)의 결과를 PSHUFB (벡터화 된 스왑)에 대한 셔플 매개 변수로 직접 변환하는 새로운 메커니즘을 사용했습니다.

이 방법은 또한 진정한 분기없는 기능 을 생성하는 부작용이있었습니다 . 점프 명령은 없습니다.

이 구현 현재 질문에서 가장 빠른 옵션 ( "Sorting Networks 12 with Simple Swap")으로 표시된 구현보다 약 38 % 빠릅니다 . char테스트 중에 배열 요소 를 사용 하여 비교를 공정하게 만들기 위해 해당 구현을 수정했습니다 .

이 방법은 최대 16 개의 요소까지 배열 크기에 적용 할 수 있습니다. 더 큰 어레이에서는 대안에 비해 상대 속도 이점이 더 커질 것으로 예상합니다.

이 코드는 SSSE3가있는 x86_64 프로세서 용 MASM으로 작성되었습니다. 이 기능은 "새로운"Windows x64 호출 규칙을 사용합니다. 여기있어...

PUBLIC simd_sort_6

.DATA

ALIGN 16

pass1_shuffle   OWORD   0F0E0D0C0B0A09080706040503010200h
pass1_add       OWORD   0F0E0D0C0B0A09080706050503020200h
pass2_shuffle   OWORD   0F0E0D0C0B0A09080706030405000102h
pass2_and       OWORD   00000000000000000000FE00FEFE00FEh
pass2_add       OWORD   0F0E0D0C0B0A09080706050405020102h
pass3_shuffle   OWORD   0F0E0D0C0B0A09080706020304050001h
pass3_and       OWORD   00000000000000000000FDFFFFFDFFFFh
pass3_add       OWORD   0F0E0D0C0B0A09080706050404050101h
pass4_shuffle   OWORD   0F0E0D0C0B0A09080706050100020403h
pass4_and       OWORD   0000000000000000000000FDFD00FDFDh
pass4_add       OWORD   0F0E0D0C0B0A09080706050403020403h
pass5_shuffle   OWORD   0F0E0D0C0B0A09080706050201040300h
pass5_and       OWORD 0000000000000000000000FEFEFEFE00h
pass5_add       OWORD   0F0E0D0C0B0A09080706050403040300h
pass6_shuffle   OWORD   0F0E0D0C0B0A09080706050402030100h
pass6_add       OWORD   0F0E0D0C0B0A09080706050403030100h

.CODE

simd_sort_6 PROC FRAME

    .endprolog

    ; pxor xmm4, xmm4
    ; pinsrd xmm4, dword ptr [rcx], 0
    ; pinsrb xmm4, byte ptr [rcx + 4], 4
    ; pinsrb xmm4, byte ptr [rcx + 5], 5
    ; The benchmarked 38% faster mentioned in the text was with the above slower sequence that tied up the shuffle port longer.  Same on extract
    ; avoiding pins/extrb also means we don't need SSE 4.1, but SSSE3 CPUs without SSE4.1 (e.g. Conroe/Merom) have slow pshufb.
    movd    xmm4, dword ptr [rcx]
    pinsrw  xmm4,  word ptr [rcx + 4], 2  ; word 2 = bytes 4 and 5


    movdqa xmm5, xmm4
    pshufb xmm5, oword ptr [pass1_shuffle]
    pcmpgtb xmm5, xmm4
    paddb xmm5, oword ptr [pass1_add]
    pshufb xmm4, xmm5

    movdqa xmm5, xmm4
    pshufb xmm5, oword ptr [pass2_shuffle]
    pcmpgtb xmm5, xmm4
    pand xmm5, oword ptr [pass2_and]
    paddb xmm5, oword ptr [pass2_add]
    pshufb xmm4, xmm5

    movdqa xmm5, xmm4
    pshufb xmm5, oword ptr [pass3_shuffle]
    pcmpgtb xmm5, xmm4
    pand xmm5, oword ptr [pass3_and]
    paddb xmm5, oword ptr [pass3_add]
    pshufb xmm4, xmm5

    movdqa xmm5, xmm4
    pshufb xmm5, oword ptr [pass4_shuffle]
    pcmpgtb xmm5, xmm4
    pand xmm5, oword ptr [pass4_and]
    paddb xmm5, oword ptr [pass4_add]
    pshufb xmm4, xmm5

    movdqa xmm5, xmm4
    pshufb xmm5, oword ptr [pass5_shuffle]
    pcmpgtb xmm5, xmm4
    pand xmm5, oword ptr [pass5_and]
    paddb xmm5, oword ptr [pass5_add]
    pshufb xmm4, xmm5

    movdqa xmm5, xmm4
    pshufb xmm5, oword ptr [pass6_shuffle]
    pcmpgtb xmm5, xmm4
    paddb xmm5, oword ptr [pass6_add]
    pshufb xmm4, xmm5

    ;pextrd dword ptr [rcx], xmm4, 0    ; benchmarked with this
    ;pextrb byte ptr [rcx + 4], xmm4, 4 ; slower version
    ;pextrb byte ptr [rcx + 5], xmm4, 5
    movd   dword ptr [rcx], xmm4
    pextrw  word ptr [rcx + 4], xmm4, 2  ; x86 is little-endian, so this is the right order

    ret

simd_sort_6 ENDP

END

이것을 실행 가능한 객체로 컴파일하여 C 프로젝트에 연결할 수 있습니다. Visual Studio에서이 작업을 수행하는 방법에 대한 지침은 이 문서를 참조하십시오 . 다음 C 프로토 타입을 사용하여 C 코드에서 함수를 호출 할 수 있습니다.

void simd_sort_6(char *values);

귀하의 제안을 다른 어셈블리 수준 제안과 비교하는 것은 방해가 될 것입니다. 구현의 비교 성능에는 포함되지 않습니다. 어쨌든 SSE를 사용하는 것이 좋습니다.
kriss December

향후 연구의 또 다른 영역은이 문제에 새로운 인텔 AVX 지침을 적용하는 것입니다. 더 큰 256 비트 벡터는 8 개의 DWORD에 맞도록 충분히 큽니다.
Joe Crivello

1
대신 !를 pxor / pinsrd xmm4, mem, 0사용하십시오 movd.
Peter Cordes

14

테스트 코드는 꽤 나쁩니다. 초기 배열을 오버플로합니다 (여기 사람들이 컴파일러 경고를 읽지 않습니까?). printf가 잘못된 요소를 인쇄하고 있습니다. 정당한 이유없이 rdtsc에 .byte를 사용합니다. 한 번만 실행됩니다 (!). 최종 결과는 실제로 정확합니다 (따라서 미묘하게 잘못된 것을“최적화”하는 것이 매우 쉽습니다). 포함 된 테스트는 매우 기초적이지 않고 (음수가 아님) 컴파일러가 전체 함수를 죽은 코드로 버리는 것을 막을 수는 없습니다.

즉, 비트 닉 네트워크 솔루션을 개선하는 것도 매우 쉽습니다. 간단히 min / max / SWAP 항목을

#define SWAP(x,y) { int tmp; asm("mov %0, %2 ; cmp %1, %0 ; cmovg %1, %0 ; cmovg %2, %1" : "=r" (d[x]), "=r" (d[y]), "=r" (tmp) : "0" (d[x]), "1" (d[y]) : "cc"); }

그리고 그것은 나에게 약 65 % 더 빠릅니다 (-O2, amd64, Core i7의 데비안 gcc 4.4.5).


테스트 코드가 잘못되었습니다. 자유롭게 개선하십시오. 예, 어셈블리 코드를 사용할 수 있습니다. 왜 x86 어셈블러를 사용하여 완전히 코딩하고 완전히 코딩하지 않습니까? 휴대 성이 다소 떨어질 수 있지만 왜 귀찮게합니까?
kriss

배열 오버플로에 주목 해 주셔서 수정했습니다. 오버플로가없는 코드를 복사 / 붙여 넣기 링크를 클릭하면 다른 사람들이 알아 채지 못할 수도 있습니다.
kriss

4
실제로는 어셈블러가 필요하지 않습니다. 영리한 트릭을 모두 삭제하면 GCC는 시퀀스를 인식하고 조건부 동작을 삽입합니다. #define min (a, b) ((a <b)? a : b) #define max (a, b) ( (a <b) β b : a) # SWAP (x, y) 정의 {int a = min (d [x], d [y]); int b = 최대 (d [x], d [y]); d [x] = a; d [y] = b; } 인라인 asm 변형보다 몇 퍼센트 느려질 수 있지만 적절한 벤치마킹이 없기 때문에 말하기가 어렵습니다.
Steinar H. Gunderson

3
마지막으로, 숫자가 실수이고 NaN 등에 대해 걱정할 필요가없는 경우 GCC는이를 minss / maxss SSE 명령어로 변환 할 수 있습니다 (아직 ~ 25 % 더 빠름). 사기 : 영리한 비트 트릭 트릭을 버리고 컴파일러가 그 일을하도록하십시오. :-)
Steinar H. Gunderson

13

제공된 스왑 매크로가 정말 마음에 들지만 :

#define min(x, y) (y ^ ((x ^ y) & -(x < y)))
#define max(x, y) (x ^ ((x ^ y) & -(x < y)))
#define SWAP(x,y) { int tmp = min(d[x], d[y]); d[y] = max(d[x], d[y]); d[x] = tmp; }

개선 사항이 있습니다 (좋은 컴파일러가 만들 수 있음).

#define SWAP(x,y) { int tmp = ((x ^ y) & -(y < x)); y ^= tmp; x ^= tmp; }

최소 및 최대 작동 방식을 기록하고 공통 하위 표현식을 명시 적으로 가져옵니다. 이렇게하면 최소 및 최대 매크로가 완전히 제거됩니다.


d [y]가 최대 값 인 x ^ (공통 하위 표현식)을 얻습니다.
케빈 주식

나는 같은 것을 알아 차렸다. 구현이 정확하고 (와 동일 ) d[x]대신 불평등이 있고 (최소 / 최대 코드와는 다른) 불평등 하다고 생각합니다 . xyd[y] < d[x]
Tyler

스왑으로 시도했지만 로컬 최적화는 더 큰 수준에서 부정적인 영향을 미칩니다 (종속성이 도입 된 것 같습니다). 결과는 다른 스왑보다 느립니다. 그러나 제안 된 새로운 솔루션으로 볼 수 있듯이 스왑 최적화를 얻는 데 실제로 많은 성능이있었습니다.
kriss

12

실제 컴파일러 생성 어셈블리를 벤치마킹하지 않고 최소 / 최대를 최적화하지 마십시오. 조건부 이동 명령으로 GCC가 최소값을 최적화하게하면 33 % 속도가 향상됩니다.

#define SWAP(x,y) { int dx = d[x], dy = d[y], tmp; tmp = d[x] = dx < dy ? dx : dy; d[y] ^= dx ^ tmp; }

(테스트 코드에서 280 사이클과 420 사이클). ?로 max를 수행하는 것은 거의 동일하며 노이즈에서 거의 손실되지만 위의 속도는 약간 빠릅니다. 이 SWAP는 GCC와 Clang에서 더 빠릅니다.

또한 컴파일러는 레지스터 할당 및 별칭 분석에서 뛰어난 기능을 수행하여 d [x]를 로컬 변수로 효과적으로 이동시키고 마지막에는 메모리로만 복사합니다. 사실, 지역 변수 (와 같은 d0 = d[0], d1 = d[1], d2 = d[2], d3 = d[3], d4 = d[4], d5 = d[5])를 완전히 사용하는 것보다 훨씬 좋습니다 . 강력한 최적화를 가정하고 컴파일러를 최소 / 최대로 능가하려고하기 때문에 이것을 작성하고 있습니다. :)

그건 그렇고, Clang과 GCC를 시도했습니다. 그들은 동일한 최적화를 수행하지만 스케줄링 차이로 인해 결과에 약간의 차이가 있으므로 실제로 어느 것이 더 빠르거나 느린 지 말할 수는 없습니다. GCC는 정렬 네트워크에서 빠르며 Clang은 2 차 정렬에서 빠릅니다.

완전성을 위해 롤링되지 않은 버블 정렬 및 삽입 정렬도 가능합니다. 버블 정렬은 다음과 같습니다.

SWAP(0,1); SWAP(1,2); SWAP(2,3); SWAP(3,4); SWAP(4,5);
SWAP(0,1); SWAP(1,2); SWAP(2,3); SWAP(3,4);
SWAP(0,1); SWAP(1,2); SWAP(2,3);
SWAP(0,1); SWAP(1,2);
SWAP(0,1);

다음은 삽입 정렬입니다.

//#define ITER(x) { if (t < d[x]) { d[x+1] = d[x]; d[x] = t; } }
//Faster on x86, probably slower on ARM or similar:
#define ITER(x) { d[x+1] ^= t < d[x] ? d[x] ^ d[x+1] : 0; d[x] = t < d[x] ? t : d[x]; }
static inline void sort6_insertion_sort_unrolled_v2(int * d){
    int t;
    t = d[1]; ITER(0);
    t = d[2]; ITER(1); ITER(0);
    t = d[3]; ITER(2); ITER(1); ITER(0);
    t = d[4]; ITER(3); ITER(2); ITER(1); ITER(0);
    t = d[5]; ITER(4); ITER(3); ITER(2); ITER(1); ITER(0);

이 삽입 정렬은 Daniel Stutzbach보다 빠르며 ITER는 3 개의 명령 (SWAP의 경우 4 개)으로 수행 할 수 있기 때문에 GPU 또는 예측이있는 컴퓨터에서 특히 좋습니다. 예를 들어 다음은 t = d[2]; ITER(1); ITER(0);ARM 어셈블리 의 라인입니다.

    MOV    r6, r2
    CMP    r6, r1
    MOVLT  r2, r1
    MOVLT  r1, r6
    CMP    r6, r0
    MOVLT  r1, r0
    MOVLT  r0, r6

6 개의 요소에 대해, 삽입 정렬은 정렬 네트워크와 경쟁적이다 (12 스왑 대 15 반복은 4 명령 / 스왑 대 3 명령 / 반복의 균형을 이룬다). 물론 버블 종류가 느립니다. 그러나 정렬 네트워크는 O (n log n)이고 삽입 정렬은 O (n ^ 2)이므로 크기가 커지면 사실이 아닙니다.


1
다소 관련이 있습니다 : 컴파일러에서 직접 최적화를 구현할 수 있도록 GCC에 보고서 를 제출 했습니다 . 그것이 이루어질 지 확실하지 않지만, 적어도 어떻게 진화하는지 따를 수 있습니다.
Morwenn

11

테스트 스위트를 식별 할 수없는 PPC 아키텍처 시스템으로 포팅했습니다 (코드를 만질 필요가 없으며 테스트 반복을 늘리고 8 가지 테스트 사례를 사용하여 오염 결과를 mod로 피하고 x86 특정 rdtsc를 대체하십시오).

qsort 라이브러리 함수 직접 호출 : 101

순진한 구현 (삽입 정렬) : 299

삽입 정렬 (Daniel Stutzbach) : 108

삽입 정렬 풀림 : 51

정렬 네트워크 (Daniel Stutzbach) : 26

정렬 네트워크 (Paul R) : 85

빠른 스왑으로 네트워크 12 정렬 : 117

정렬 네트워크 12 순서 변경 : 116

순위 순서 : 56


1
정말 흥미로운. 브랜치리스 스왑은 PPC에서 나쁜 생각 인 것 같습니다. 컴파일러 관련 효과 일 수도 있습니다. 어느 것이 사용 되었습니까?
kriss

gcc 컴파일러의 브랜치-최소, 최대 로직은 브랜치가 아닐 것입니다-디스 어셈블리를 검사하고 알려 드리겠습니다.하지만 컴파일러가 영리하지 않으면 x <y와 같은 것을 포함하지 않고 x가 여전히 브랜치가되는 경우-x86에서 / x64 CMOV 명령어는이를 피할 수 있지만 PPC에 고정 소수점 값에 대한 명령어는 없으며 수레 만 있습니다. 나는 내일이 문제에 대해 잘 알고 당신에게 알릴 것입니다-Winamp AVS 소스에 훨씬 더 간단한 분기없는 최소 / 최대가 있었음을 기억하지만 iirc는 수레 전용이지만 실제로는 분기가없는 접근법을 시작하는 것이 좋습니다.
jheriko

4
서명되지 않은 입력이있는 PPC의 분기없는 최소 / 최대는 다음과 같습니다 subfc r5,r4,r3; subfe r6,r6,r6; andc r6,r5,r6; add r4,r6,r4; subf r3,r6,r3. r3 / r4는 입력이고 r5 / r6은 스크래치 레지스터입니다. 출력에서 ​​r3은 최소값을, r4는 최대 값을 얻습니다. 손으로 예약 할 수 있어야합니다. 나는 4 개의 명령 min 및 max 시퀀스에서 시작하여 결합 할 수있는 두 가지를 수동으로 찾고있는 GNU 수퍼 옵티 마이저로 그것을 발견했습니다. 부호있는 입력의 경우, 처음에 모든 요소에 0x80000000을 추가하고 끝에서 다시 빼고 부호없는 것처럼 작동 할 수 있습니다.
Paolo Bonzini

7

XOR 스왑은 스와핑 기능에 유용 할 수 있습니다.

void xorSwap (int *x, int *y) {
     if (*x != *y) {
         *x ^= *y;
         *y ^= *x;
         *x ^= *y;
     }
 }

if는 코드에서 너무 많은 차이를 유발할 수 있지만 모든 정수가 고유하다는 보장이 있으면 편리 할 수 ​​있습니다.


1
xor swap도 같은 값으로 작동합니다. x ^ = y는 x를 0으로 설정하고 y ^ = x는 y를 y (== x)로 남겨두고 x ^ = y는 x를 y로 설정합니다
jheriko

11
이 때 하지 않는 일이다 xy같은 위치에 점.
hobbs

어쨌든 정렬 네트워크와 함께 사용하면 x와 y가 같은 위치를 가리키는 것으로 호출하지 않습니다. 분기없는 스왑과 동일한 효과를 얻기 위해 테스트를 피할 수있는 방법이 여전히 남아 있습니다. 나는 그것을 달성 할 생각이 있습니다.
kriss

5

이것을 시도하고이 예제를 통해 배우기를 기대하지만, 먼저 1GB DDR RAM이있는 1.5GHz PPC Powerbook G4의 타이밍을 살펴보십시오. (저는 http://www.mcs.anl.gov/~kazutomo/rdtsc.html 에서 비슷한 PPC 타이머를 빌 렸습니다 . 타이밍에 대해 .) 프로그램을 몇 번 실행했으며 절대 결과는 다양하지만 일관되게 가장 빠른 테스트는 "Insertion Sort (Daniel Stutzbach)"였으며 "Insertion Sort Unrolled"는 거의 2 초가되었습니다.

마지막 시간은 다음과 같습니다.

**Direct call to qsort library function** : 164
**Naive implementation (insertion sort)** : 138
**Insertion Sort (Daniel Stutzbach)**     : 85
**Insertion Sort Unrolled**               : 97
**Sorting Networks (Daniel Stutzbach)**   : 457
**Sorting Networks (Paul R)**             : 179
**Sorting Networks 12 with Fast Swap**    : 238
**Sorting Networks 12 reordered Swap**    : 236
**Rank Order**                            : 116

4

이 스레드에 대한 나의 기여는 다음과 같습니다. 고유 한 값을 포함하는 6 원 int 벡터 (valp)에 대해 최적화 된 1, 4 간격 쉘 정렬.

void shellsort (int *valp)
{      
  int c,a,*cp,*ip=valp,*ep=valp+5;

  c=*valp;    a=*(valp+4);if (c>a) {*valp=    a;*(valp+4)=c;}
  c=*(valp+1);a=*(valp+5);if (c>a) {*(valp+1)=a;*(valp+5)=c;}

  cp=ip;    
  do
  {
    c=*cp;
    a=*(cp+1);
    do
    {
      if (c<a) break;

      *cp=a;
      *(cp+1)=c;
      cp-=1;
      c=*cp;
    } while (cp>=valp);
    ip+=1;
    cp=ip;
  } while (ip<ep);
}

듀얼 코어 Athlon M300 @ 2Ghz (DDR2 메모리)가 장착 된 HP dv7-3010so 랩탑에서는 165 클럭 주기로 실행됩니다. 이것은 고유 시퀀스마다 타이밍에서 계산 된 평균입니다 (모두 6! / 720). OpenWatcom 1.8을 사용하여 Win32로 컴파일되었습니다. 루프는 본질적으로 삽입 정렬이며 16 명령어 / 37 바이트 길이입니다.

컴파일 할 64 비트 환경이 없습니다.


좋은. 나는 그것을 더 긴 testsuite에 추가 할 것이다
kriss

3

삽입 정렬이 합리적으로 경쟁력이 있다면 쉘 정렬을 시도하는 것이 좋습니다. 나는 6 가지 요소가 아마도 최고의 요소가 되기에는 너무 적을 것이라고 두려워하지만 시도해 볼 가치가 있습니다.

테스트되지 않은 코드, 논쟁의 여지가없는 예제 코드 등 inc = 4 및 inc-= 3 시퀀스를 조정하여 최적을 찾습니다 (예 : inc = 2, inc-= 1).

static __inline__ int sort6(int * d) {
    char j, i;
    int tmp;
    for (inc = 4; inc > 0; inc -= 3) {
        for (i = inc; i < 5; i++) {
            tmp = a[i];
            j = i;
            while (j >= inc && a[j - inc] > tmp) {
                a[j] = a[j - inc];
                j -= inc;
            }
            a[j] = tmp;
        }
    }
}

나는 이것이 이길 것이라고 생각하지 않지만 누군가가 10 가지 요소를 정렬하는 것에 대한 질문을 게시하면

Wikipedia에 따르면 이것은 정렬 네트워크와 결합 될 수도 있습니다 : Pratt, V (1979). 셸 정렬 및 정렬 네트워크 (컴퓨터 과학의 뛰어난 논문). 화환. ISBN 0-824-04406-1


구현을 제안 해
주시기 바랍니다

제안서 추가 버그를 즐기십시오.
gcp

3

나는 내가 최고라고 알고 있지만 다른 솔루션을 실험하는 데 관심이있었습니다. 먼저 해당 페이스트를 정리하고 컴파일하여 리포지토리에 넣었습니다. 다른 사람들이 시도하지 않도록 바람직하지 않은 솔루션을 막 다른 골목으로 유지했습니다. 이 중 첫 번째 솔루션은 x1> x2가 한 번 계산되도록 시도했습니다. 최적화 후에는 다른 간단한 버전보다 빠르지 않습니다.

이 연구의 내 응용 프로그램은 2-8 개의 항목을 정렬하기위한 것이므로 순위 순서 정렬의 반복 버전을 추가 했으므로 가변 개수의 인수가 있으므로 루프가 필요합니다. 이것이 또한 정렬 네트워크 솔루션을 무시한 이유입니다.

테스트 코드는 복제본이 올바르게 처리되었는지 테스트하지 않았으므로 기존 솔루션이 모두 정확했지만 테스트 코드에 특수한 경우를 추가하여 복제본이 올바르게 처리되었는지 확인했습니다.

그런 다음 전적으로 AVX 레지스터에있는 삽입 정렬을 작성했습니다. 내 컴퓨터에서는 다른 삽입 정렬보다 25 % 빠르지 만 순위 순서보다 100 % 느립니다. 나는 순전히 실험을 위해이 작업을 수행했으며 삽입 정렬의 분기로 인해 더 나아질 것으로 기대하지 않았습니다.

static inline void sort6_insertion_sort_avx(int* d) {
    __m256i src = _mm256_setr_epi32(d[0], d[1], d[2], d[3], d[4], d[5], 0, 0);
    __m256i index = _mm256_setr_epi32(0, 1, 2, 3, 4, 5, 6, 7);
    __m256i shlpermute = _mm256_setr_epi32(7, 0, 1, 2, 3, 4, 5, 6);
    __m256i sorted = _mm256_setr_epi32(d[0], INT_MAX, INT_MAX, INT_MAX,
            INT_MAX, INT_MAX, INT_MAX, INT_MAX);
    __m256i val, gt, permute;
    unsigned j;
     // 8 / 32 = 2^-2
#define ITER(I) \
        val = _mm256_permutevar8x32_epi32(src, _mm256_set1_epi32(I));\
        gt =  _mm256_cmpgt_epi32(sorted, val);\
        permute =  _mm256_blendv_epi8(index, shlpermute, gt);\
        j = ffs( _mm256_movemask_epi8(gt)) >> 2;\
        sorted = _mm256_blendv_epi8(_mm256_permutevar8x32_epi32(sorted, permute),\
                val, _mm256_cmpeq_epi32(index, _mm256_set1_epi32(j)))
    ITER(1);
    ITER(2);
    ITER(3);
    ITER(4);
    ITER(5);
    int x[8];
    _mm256_storeu_si256((__m256i*)x, sorted);
    d[0] = x[0]; d[1] = x[1]; d[2] = x[2]; d[3] = x[3]; d[4] = x[4]; d[5] = x[5];
#undef ITER
}

그런 다음 AVX를 사용하여 순위 순서 정렬을 작성했습니다. 이것은 다른 순위 솔루션의 속도와 일치하지만 더 빠르지는 않습니다. 여기서 문제는 AVX를 사용하여 인덱스 만 계산할 수 있고 인덱스 테이블을 만들어야한다는 것입니다. 계산은 소스 기반이 아닌 대상 기반이기 때문입니다. 소스 기반 인덱스에서 대상 기반 인덱스로 변환을 참조하십시오.

static inline void sort6_rank_order_avx(int* d) {
    __m256i ror = _mm256_setr_epi32(5, 0, 1, 2, 3, 4, 6, 7);
    __m256i one = _mm256_set1_epi32(1);
    __m256i src = _mm256_setr_epi32(d[0], d[1], d[2], d[3], d[4], d[5], INT_MAX, INT_MAX);
    __m256i rot = src;
    __m256i index = _mm256_setzero_si256();
    __m256i gt, permute;
    __m256i shl = _mm256_setr_epi32(1, 2, 3, 4, 5, 6, 6, 6);
    __m256i dstIx = _mm256_setr_epi32(0,1,2,3,4,5,6,7);
    __m256i srcIx = dstIx;
    __m256i eq = one;
    __m256i rotIx = _mm256_setzero_si256();
#define INC(I)\
    rot = _mm256_permutevar8x32_epi32(rot, ror);\
    gt = _mm256_cmpgt_epi32(src, rot);\
    index = _mm256_add_epi32(index, _mm256_and_si256(gt, one));\
    index = _mm256_add_epi32(index, _mm256_and_si256(eq,\
                _mm256_cmpeq_epi32(src, rot)));\
    eq = _mm256_insert_epi32(eq, 0, I)
    INC(0);
    INC(1);
    INC(2);
    INC(3);
    INC(4);
    int e[6];
    e[0] = d[0]; e[1] = d[1]; e[2] = d[2]; e[3] = d[3]; e[4] = d[4]; e[5] = d[5];
    int i[8];
    _mm256_storeu_si256((__m256i*)i, index);
    d[i[0]] = e[0]; d[i[1]] = e[1]; d[i[2]] = e[2]; d[i[3]] = e[3]; d[i[4]] = e[4]; d[i[5]] = e[5];
}

저장소는 https://github.com/eyepatchParrot/sort6/ 에서 찾을 수 있습니다.


1
vmovmskps비트 캔 ( ffs) 결과 를 오른쪽으로 이동할 필요없이 정수 벡터 (내장 함수를 행복하게 유지하기 위해 캐스트)를 사용할 수 있습니다 .
Peter Cordes

1
으로 마스킹하는 대신 cmpgt결과를 결과에 따라 조건부로 1을 추가 할 수 있습니다 set1(1). 예를 들면 index = _mm256_sub_epi32(index, gt)않습니다index -= -1 or 0;
피터 코르

1
eq = _mm256_insert_epi32(eq, 0, I)작성된대로 컴파일하면 요소를 0으로 만드는 효율적인 방법이 아닙니다 (특히 vpinsrdXMM 대상에서만 사용할 수 있기 때문에 낮은 4 외부의 요소의 경우 3보다 큰 인덱스를 에뮬레이션해야 함). 대신, 제로 벡터가있는 _mm256_blend_epi32( vpblendd). vpblenddIntel CPU에서 포트 5가 필요한 셔플과 비교하여 모든 포트에서 실행되는 단일 UOP 명령어입니다. ( agner.org/optimize ).
Peter Cordes

1
또한 rot동일한 소스에서 다른 셔플이 있는 벡터를 생성 하거나 차선 교차 셔플 (3 사이클 대기 시간)을 통해 하나의 단일 뎁 체인 대신 교대로 사용하는 2 개의 뎁 체인을 병렬로 실행 하는 것을 고려할 수 있습니다. 단일 정렬 내에서 ILP가 증가합니다. 2 개의 딥 체인은 벡터 상수의 수를 적당한 수로 제한합니다. 한 번의 회전에 대해 2 : 1, 두 개의 회전 단계에 대해 하나만 결합됩니다.
Peter Cordes

2

이 질문은 꽤 오래되었지만 실제로 요즘 같은 문제를 해결해야했습니다. 작은 배열을 정렬하는 빠른 문제. 내 지식을 공유하는 것이 좋은 생각이라고 생각했습니다. 정렬 네트워크를 사용하여 처음 시작한 동안 마침내 6 값의 모든 순열을 정렬하기 위해 수행 한 총 비교 수가 정렬 네트워크보다 작고 삽입 정렬보다 작은 다른 알고리즘을 찾았습니다. 스왑 수는 세지 않았습니다. 나는 그것이 대략 동등 할 것으로 기대합니다 (때로는 약간 더 높을 수도 있음).

알고리즘 sort6은 알고리즘을 사용하는 알고리즘 sort4을 사용합니다 sort3. 다음은 가벼운 C ++ 형식의 구현입니다 (원본은 템플릿이 무겁기 때문에 임의 액세스 반복기와 적절한 비교 함수로 작동 할 수 있습니다).

3 값 정렬

다음 알고리즘은 언 롤링 된 삽입 정렬입니다. 두 개의 스왑 (6 개의 할당)을 수행해야하는 경우 대신 4 개의 할당을 사용합니다.

void sort3(int* array)
{
    if (array[1] < array[0]) {
        if (array[2] < array[0]) {
            if (array[2] < array[1]) {
                std::swap(array[0], array[2]);
            } else {
                int tmp = array[0];
                array[0] = array[1];
                array[1] = array[2];
                array[2] = tmp;
            }
        } else {
            std::swap(array[0], array[1]);
        }
    } else {
        if (array[2] < array[1]) {
            if (array[2] < array[0]) {
                int tmp = array[2];
                array[2] = array[1];
                array[1] = array[0];
                array[0] = tmp;
            } else {
                std::swap(array[1], array[2]);
            }
        }
    }
}

정렬에는 배열의 가능한 모든 순열에 대해 2 ~ 3 개의 비교와 최대 4 개의 대입을 사용하여 세 개의 값을 정렬하는 분기가 하나 이상 있기 때문에 약간 복잡해 보입니다.

4 가지 값 정렬

이 호출 sort3은 배열의 마지막 요소를 사용하여 롤링되지 않은 삽입 정렬을 수행합니다.

void sort4(int* array)
{
    // Sort the first 3 elements
    sort3(array);

    // Insert the 4th element with insertion sort 
    if (array[3] < array[2]) {
        std::swap(array[2], array[3]);
        if (array[2] < array[1]) {
            std::swap(array[1], array[2]);
            if (array[1] < array[0]) {
                std::swap(array[0], array[1]);
            }
        }
    }
}

이 알고리즘은 3-6 개의 비교와 최대 5 개의 스왑을 수행합니다. 삽입 정렬을 해제하는 것은 쉽지만 마지막 정렬에 다른 알고리즘을 사용할 것입니다 ...

6 값 정렬

이것은 내가 이중 삽입 정렬 이라고하는 롤링되지 않은 버전을 사용합니다 . 그 이름은 그렇게 크지는 않지만 꽤 설명 적입니다. 여기서 작동 방식은 다음과 같습니다.

  • 배열의 첫 번째 요소와 마지막 요소를 제외한 모든 것을 정렬합니다.
  • 첫 번째가 마지막보다 큰 경우 첫 번째와 배열의 요소를 교체하십시오.
  • 첫 번째 요소를 앞에서 순서대로 정렬 한 다음 마지막 요소를 뒤에서 삽입하십시오.

스왑 후 첫 번째 요소는 항상 마지막 요소보다 작습니다. 즉, 정렬 된 시퀀스에 요소를 삽입 할 때 최악의 경우 두 요소를 삽입하는 데 N 개 이상의 비교가 발생하지 않습니다. 첫 번째 요소는 세 번째 위치에 삽입 된 후 마지막 요소는 네 번째 위치보다 낮은 위치에 삽입 할 수 없습니다.

void sort6(int* array)
{
    // Sort everything but first and last elements
    sort4(array+1);

    // Switch first and last elements if needed
    if (array[5] < array[0]) {
        std::swap(array[0], array[5]);
    }

    // Insert first element from the front
    if (array[1] < array[0]) {
        std::swap(array[0], array[1]);
        if (array[2] < array[1]) {
            std::swap(array[1], array[2]);
            if (array[3] < array[2]) {
                std::swap(array[2], array[3]);
                if (array[4] < array[3]) {
                    std::swap(array[3], array[4]);
                }
            }
        }
    }

    // Insert last element from the back
    if (array[5] < array[4]) {
        std::swap(array[4], array[5]);
        if (array[4] < array[3]) {
            std::swap(array[3], array[4]);
            if (array[3] < array[2]) {
                std::swap(array[2], array[3]);
                if (array[2] < array[1]) {
                    std::swap(array[1], array[2]);
                }
            }
        }
    }
}

6 값의 모든 순열에 대한 나의 테스트는이 알고리즘이 항상 6과 13 사이의 비교를 수행한다는 것을 보여줍니다. 나는 스왑 수를 계산하지 않았지만 최악의 경우 스왑 수가 11보다 높을 것으로 예상하지 않습니다.

이 질문이 더 이상 실제 문제를 나타내지 않을 수도 있지만 이것이 도움이되기를 바랍니다. :)

편집 : 제공된 벤치 마크에 넣은 후 대부분의 흥미로운 대안보다 훨씬 느립니다. 언 롤링 된 삽입 정렬보다 약간 더 나은 성능을 보이는 경향이 있지만, 그 정도입니다. 기본적으로 정수에 가장 적합한 정렬은 아니지만 값 비싼 비교 연산이있는 유형에는 흥미로울 수 있습니다.


이것들은 좋습니다. 해결 된 문제는 수십 년 전이고 아마도 C 프로그래밍처럼 오래 되었기 때문에 문제는 거의 5 년이되었지만 그다지 관련성이없는 것처럼 보입니다.
kriss

다른 답변의 시간이 정해진 방식을 살펴 봐야합니다. 요점은 이러한 작은 데이터 세트 계산 비교 또는 비교 및 ​​스왑을 사용하면 알고리즘이 얼마나 빠르다는 것을 실제로 말하지는 않는다는 것입니다 (O (6 * 6)이 O (1)이기 때문에 기본적으로 6 ints를 정렬하는 것은 항상 O (1)입니다). 이전에 제안 된 솔루션 중 현재 가장 빠른 솔루션은 큰 비교 (RexKerr)를 사용하여 각 값의 위치를 ​​즉시 찾는 것입니다.
kriss

@kriss 지금 가장 빠릅니까? 결과를 읽은 결과, 분류 네트워크 접근 방식이 가장 빠르며 나빴습니다. 내 솔루션이 내 일반 라이브러리에서 왔으며 항상 정수를 비교하거나 항상 operator<비교에 사용 하는 것은 아닙니다 . 객관적인 비교 및 ​​스왑 수 외에도 알고리즘 시간도 적절하게 설정했습니다. 이 솔루션은 가장 빠른 일반 솔루션이지만 실제로 @RexKerr의 솔루션을 놓쳤습니다. 시도해보십시오 :)
Morwenn

ReccKerr (Order Rank)의 솔루션은 gcc 컴파일러 4.2.3 이후로 X86 아키텍처에서 가장 빠릅니다 (gcc 4.9 현재 두 번째 최고보다 거의 두 배 빠릅니다). 그러나 컴파일러 최적화에 크게 의존하고 있으며 다른 아키텍처에서는 그렇지 않을 수 있습니다.
kriss

@kriss 흥미 롭습니다. 그리고 나는 정말로 더 많은 차이점을 알 수있었습니다 -O3. 정렬 라이브러리에 다른 전략을 채택 할 것입니다. 비교 횟수가 적거나 스왑 횟수가 적거나 잠재적으로 최상의 성능을 발휘할 수있는 3 가지 종류의 알고리즘을 제공하는 것 같습니다. 최소한 독자에게는 투명하게 될 것입니다. 귀하의 통찰에 감사드립니다 :)
Morwenn

1

귀하의 질문에 두 부분이 있다고 생각합니다.

  • 첫 번째는 최적의 알고리즘을 결정하는 것입니다. 이것은 (이 경우 적어도) 가능한 모든 순서를 반복하여 수행되며 (비교는 많지 않음) 비교 및 ​​스왑의 정확한 최소, 최대, 평균 및 표준 편차를 계산할 수 있습니다. 준우승도 가능합니다.
  • 두 번째는 알고리즘을 최적화하는 것입니다. 교과서 코드 예제를 의미 있고 실제 알고리즘으로 변환하기 위해 많은 작업을 수행 할 수 있습니다. 알고리즘이 필요한 정도로 최적화 될 수 없다는 것을 알고 있다면, 1 단계를 시도하십시오.

파이프 라인을 비우는 것에 대해 너무 걱정하지 않아도됩니다 (현재 x86 가정) : 지점 예측이 먼 길을 왔습니다. 내가 걱정하는 것은 코드와 데이터가 각각 하나의 캐시 라인 (코드에 대해 두 개)에 맞는지 확인하는 것입니다. 페치 대기 시간이 새로 고쳐지면 스톨을 보상합니다. 또한 내부 루프는 아마도 10 개의 명령 또는 그 자리에 있어야합니다 (정렬 알고리즘에는 두 개의 다른 내부 루프가 있으며 각각 10 명령 / 22 바이트 및 9/22 길이입니다). 코드에 div가 포함되어 있지 않다고 가정하면 맹목적으로 빠를 것입니다.


귀하의 답변을 이해하는 방법을 잘 모르겠습니다. 먼저 나는 당신이 제안하는 알고리즘을 전혀 이해하지 못합니까? 720 개의 가능한 순서를 반복해야하는 경우 최적의 방법 (기존 답변은 720 회보다 훨씬 적게 소요됨). 임의의 입력이있는 경우 모든 입력 데이터를 신경 쓰지 않는 경우를 제외하고 분기 예측이 50-50보다 더 나은 수행 방법을 상상할 수 없습니다 (이론적 수준에서도). 또한 이미 제안 된 가장 좋은 솔루션은 이미 캐시에서 데이터와 코드 모두에서 작동 할 가능성이 높습니다. 그러나 나는 당신의 대답을 완전히 이해하지 못했습니다. 어떤 코드를 보여주는 마음?
kriss

내가 의미 한 바는 6 개의 정수로 720 (6!)의 서로 다른 조합 만 있고 후보 알고리즘을 통해 이들을 모두 실행함으로써 내가 언급 한 것처럼 많은 것을 결정할 수 있다는 것이 이론적 인 부분입니다. 실용적인 부분은 알고리즘이 가능한 한 적은 클럭 사이클로 실행되도록 미세 조정하는 것입니다. 6 개의 정수를 정렬하기위한 시작점은 1, 4 개의 간격 쉘 정렬입니다. 4 개의 갭은 1 개의 갭에서 좋은 분기 예측을위한 길을 열어줍니다.
Olof Forshell

6의 1, 4 갭 쉘 정렬! 고유 조합 (012345로 시작하고 543210으로 끝나는)은 7 번의 비교와 0 번의 교환과 14 번의 비교와 10 번의 교환 중에서 가장 좋은 경우입니다. 평균 사례는 약 11.14 비교와 6 교환입니다.
Olof Forshell

1
나는 "정규 무작위 분포"를 얻지 못합니다. 내가하고있는 일은 가능한 모든 조합을 테스트하고 최소 / 평균 / 최대 통계를 결정하는 것입니다. Shellsort는 순차 삽입 정렬에서와 같이 단독으로 수행되는 경우보다 최종 증가분-1-이 훨씬 적은 작업을 수행 할 수 있도록 일련의 감소 증분 삽입입니다. 클럭 수와 관련하여 내 알고리즘에는 평균 406 클럭 사이클이 필요하며 여기에는 통계 수집 및 실제 정렬 루틴에 대한 두 번의 호출 (각 간격마다 하나씩)이 포함됩니다. 이것은 Athlon M300 모바일 컴파일러 OpenWatcom에 있습니다.
Olof Forshell

1
"정규 랜덤 분포"는 정렬 된 실제 데이터의 모든 조합이 동일한 확률을 가질 수 없음을 의미합니다. 모든 조합의 확률이 같지 않으면 평균이 주어진 분포가 발생할 확률을 몇 번 고려해야하므로 통계가 손상됩니다. 클럭 수의 경우, 이러한 종류의 다른 구현 (위에 제공된 링크)을 시도하고 테스트 시스템에서 실행하면 비교 기준이되며 선택한 성능이 얼마나 잘 수행되는지 확인할 수 있습니다.
kriss

1

나는 이것이 오래된 질문이라는 것을 안다.

그러나 방금 공유하고 싶은 다른 종류의 솔루션을 작성했습니다.
중첩 된 MIN MAX 만 사용하면

각각 114를 사용하므로 빠르지 않습니다.
간단히 75로 줄일 수 있습니다-> pastebin

그러나 이제는 더 이상 순수 최대 값이 아닙니다.

작동 할 수있는 것은 AVX를 사용하여 여러 정수에서 한 번에 최소 / 최대를 수행하는 것입니다.

PMINSW 참조

#include <stdio.h>

static __inline__ int MIN(int a, int b){
int result =a;
__asm__ ("pminsw %1, %0" : "+x" (result) : "x" (b));
return result;
}
static __inline__ int MAX(int a, int b){
int result = a;
__asm__ ("pmaxsw %1, %0" : "+x" (result) : "x" (b));
return result;
}
static __inline__ unsigned long long rdtsc(void){
  unsigned long long int x;
__asm__ volatile (".byte 0x0f, 0x31" :
  "=A" (x));
  return x;
}

#define MIN3(a, b, c) (MIN(MIN(a,b),c))
#define MIN4(a, b, c, d) (MIN(MIN(a,b),MIN(c,d)))

static __inline__ void sort6(int * in) {
  const int A=in[0], B=in[1], C=in[2], D=in[3], E=in[4], F=in[5];

  in[0] = MIN( MIN4(A,B,C,D),MIN(E,F) );

  const int
  AB = MAX(A, B),
  AC = MAX(A, C),
  AD = MAX(A, D),
  AE = MAX(A, E),
  AF = MAX(A, F),
  BC = MAX(B, C),
  BD = MAX(B, D),
  BE = MAX(B, E),
  BF = MAX(B, F),
  CD = MAX(C, D),
  CE = MAX(C, E),
  CF = MAX(C, F),
  DE = MAX(D, E),
  DF = MAX(D, F),
  EF = MAX(E, F);

  in[1] = MIN4 (
  MIN4( AB, AC, AD, AE ),
  MIN4( AF, BC, BD, BE ),
  MIN4( BF, CD, CE, CF ),
  MIN3( DE, DF, EF)
  );

  const int
  ABC = MAX(AB,C),
  ABD = MAX(AB,D),
  ABE = MAX(AB,E),
  ABF = MAX(AB,F),
  ACD = MAX(AC,D),
  ACE = MAX(AC,E),
  ACF = MAX(AC,F),
  ADE = MAX(AD,E),
  ADF = MAX(AD,F),
  AEF = MAX(AE,F),
  BCD = MAX(BC,D),
  BCE = MAX(BC,E),
  BCF = MAX(BC,F),
  BDE = MAX(BD,E),
  BDF = MAX(BD,F),
  BEF = MAX(BE,F),
  CDE = MAX(CD,E),
  CDF = MAX(CD,F),
  CEF = MAX(CE,F),
  DEF = MAX(DE,F);

  in[2] = MIN( MIN4 (
  MIN4( ABC, ABD, ABE, ABF ),
  MIN4( ACD, ACE, ACF, ADE ),
  MIN4( ADF, AEF, BCD, BCE ),
  MIN4( BCF, BDE, BDF, BEF )),
  MIN4( CDE, CDF, CEF, DEF )
  );


  const int
  ABCD = MAX(ABC,D),
  ABCE = MAX(ABC,E),
  ABCF = MAX(ABC,F),
  ABDE = MAX(ABD,E),
  ABDF = MAX(ABD,F),
  ABEF = MAX(ABE,F),
  ACDE = MAX(ACD,E),
  ACDF = MAX(ACD,F),
  ACEF = MAX(ACE,F),
  ADEF = MAX(ADE,F),
  BCDE = MAX(BCD,E),
  BCDF = MAX(BCD,F),
  BCEF = MAX(BCE,F),
  BDEF = MAX(BDE,F),
  CDEF = MAX(CDE,F);

  in[3] = MIN4 (
  MIN4( ABCD, ABCE, ABCF, ABDE ),
  MIN4( ABDF, ABEF, ACDE, ACDF ),
  MIN4( ACEF, ADEF, BCDE, BCDF ),
  MIN3( BCEF, BDEF, CDEF )
  );

  const int
  ABCDE= MAX(ABCD,E),
  ABCDF= MAX(ABCD,F),
  ABCEF= MAX(ABCE,F),
  ABDEF= MAX(ABDE,F),
  ACDEF= MAX(ACDE,F),
  BCDEF= MAX(BCDE,F);

  in[4]= MIN (
  MIN4( ABCDE, ABCDF, ABCEF, ABDEF ),
  MIN ( ACDEF, BCDEF )
  );

  in[5] = MAX(ABCDE,F);
}

int main(int argc, char ** argv) {
  int d[6][6] = {
    {1, 2, 3, 4, 5, 6},
    {6, 5, 4, 3, 2, 1},
    {100, 2, 300, 4, 500, 6},
    {100, 2, 3, 4, 500, 6},
    {1, 200, 3, 4, 5, 600},
    {1, 1, 2, 1, 2, 1}
  };

  unsigned long long cycles = rdtsc();
  for (int i = 0; i < 6; i++) {
    sort6(d[i]);
  }
  cycles = rdtsc() - cycles;
  printf("Time is %d\n", (unsigned)cycles);

  for (int i = 0; i < 6; i++) {
    printf("d%d : %d %d %d %d %d %d\n", i,
     d[i][0], d[i][1], d[i][2],
     d[i][3], d[i][4], d[i][5]);
  }
}

편집 :
Rex Kerr에서 영감을 얻은 순위 순서 솔루션, 위의 혼란보다 훨씬 빠름

static void sort6(int *o) {
const int 
A=o[0],B=o[1],C=o[2],D=o[3],E=o[4],F=o[5];
const unsigned char
AB = A>B, AC = A>C, AD = A>D, AE = A>E,
          BC = B>C, BD = B>D, BE = B>E,
                    CD = C>D, CE = C>E,
                              DE = D>E,
a =          AB + AC + AD + AE + (A>F),
b = 1 - AB      + BC + BD + BE + (B>F),
c = 2 - AC - BC      + CD + CE + (C>F),
d = 3 - AD - BD - CD      + DE + (D>F),
e = 4 - AE - BE - CE - DE      + (E>F);
o[a]=A; o[b]=B; o[c]=C; o[d]=D; o[e]=E;
o[15-a-b-c-d-e]=F;
}

1
항상 새로운 솔루션을 만나서 반갑습니다. 쉬운 최적화가 가능한 것 같습니다. 결국 그것은 분류 네트워크와 크게 다르지 않을 수 있습니다.
kriss

예, MIN과 MAX의 수는 줄어들 수 있습니다. 예를 들어 MIN (AB, CD)는 몇 번 반복되지만 많이 줄이면 어려울 것입니다. 테스트 사례를 추가했습니다.
PrincePolka

pmin / maxsw는 압축 된 16 비트 부호있는 정수 ( int16_t)에서 작동합니다. 그러나 C 함수는 배열을 정렬한다고 주장합니다 int(해당 asm구문 을 지원하는 모든 C 구현에서 32 비트 ). 높은 반쪽에 0 만있는 작은 양의 정수로만 테스트 했습니까? 작동 int합니다. SSE4.1 pmin/maxsd(d = dword) 이 필요합니다 . felixcloutier.com/x86/pminsd:pminsqpminusd에 대한 uint32_t.
Peter Cordes 2016 년

1

나는 적어도 내 시스템에서 아래 의 기능 sort6_iterator()과 함수 sort6_iterator_local()가 위의 현재 레코드 홀더보다 적어도 빠르고 빠르다는 것을 알았 습니다.

#define MIN(x, y) (x<y?x:y)
#define MAX(x, y) (x<y?y:x)

template<class IterType> 
inline void sort6_iterator(IterType it) 
{
#define SWAP(x,y) { const auto a = MIN(*(it + x), *(it + y)); \
  const auto b = MAX(*(it + x), *(it + y)); \
  *(it + x) = a; *(it + y) = b; }

  SWAP(1, 2) SWAP(4, 5)
  SWAP(0, 2) SWAP(3, 5)
  SWAP(0, 1) SWAP(3, 4)
  SWAP(1, 4) SWAP(0, 3)
  SWAP(2, 5) SWAP(1, 3)
  SWAP(2, 4)
  SWAP(2, 3)
#undef SWAP
}

std::vector타이밍 코드 에서이 함수에 반복자를 전달했습니다 .

나는 (같은 의견에서 의심 반복자를 사용하여 반복자는 다른 방법으로는 얻을 수없는 어떤을 의미하고 이러한 보장 할 수 g ++가 있음 메모리 일어날 수 없습니다 수있는 작업에 대한 g ++ 어떤 보장을 제공하는 것이 다른 곳과) 정렬 코드를 더 잘 최적화하십시오 (예 : 포인터를 사용하면 컴파일러에서 모든 포인터가 다른 메모리 위치를 가리키는 지 확인할 수 없음). 내가 정확하게 기억한다면, 이것은 또한 많은 STL 알고리즘과 같은 이유의 일부 입니다.std::sort() 일반적으로 외설적으로 좋은 성능을 갖는 .

또한, sort6_iterator() 배 (역시, 함수가 호출되는 문맥에 따라) 다음과 일관 정렬 기능으로 상회하는들을 정렬하기 전에 복사 로컬 변수로 데이터. 1 정의 된 지역 변수는 6 개뿐이므로 이러한 지역 변수가 프리미티브 인 경우 실제로는 RAM에 저장되지 않으며 함수 호출이 끝날 때까지 CPU의 레지스터에만 저장되므로이 정렬에 도움이됩니다. 빠른 기능. 또한 컴파일러는 고유 한 로컬 변수가 메모리에서 고유 한 위치를 가지고 있음을 컴파일러가 알도록 도와줍니다.

template<class IterType> 
inline void sort6_iterator_local(IterType it) 
{
#define SWAP(x,y) { const auto a = MIN(data##x, data##y); \
  const auto b = MAX(data##x, data##y); \
  data##x = a; data##y = b; }
//DD = Define Data
#define DD1(a)   auto data##a = *(it + a);
#define DD2(a,b) auto data##a = *(it + a), data##b = *(it + b);
//CB = Copy Back
#define CB(a) *(it + a) = data##a;

  DD2(1,2)    SWAP(1, 2)
  DD2(4,5)    SWAP(4, 5)
  DD1(0)      SWAP(0, 2)
  DD1(3)      SWAP(3, 5)
  SWAP(0, 1)  SWAP(3, 4)
  SWAP(1, 4)  SWAP(0, 3)   CB(0)
  SWAP(2, 5)  CB(5)
  SWAP(1, 3)  CB(1)
  SWAP(2, 4)  CB(4)
  SWAP(2, 3)  CB(2)        CB(3)
#undef CB
#undef DD2
#undef DD1
#undef SWAP
}

참고 정의가 있음 SWAP()으로 다음 몇 가지 약간 더 나은 성능을 번 결과가 약간 더 성능이나 성능 무시할 차이 결과 대부분의 시간이지만.

#define SWAP(x,y) { const auto a = MIN(data##x, data##y); \
  data##y = MAX(data##x, data##y); \
  data##x = a; }

기본 데이터 유형에서 정렬 알고리즘을 원할 경우 gcc -O3은 정렬 함수에 대한 호출이 1에 나타나는 컨텍스트에 관계없이 입력을 전달하는 방법에 따라 일관성있게 최적화 하는 데 다음 두 가지 중 하나를 시도하십시오. 알고리즘 :

template<class T> inline void sort6(T it) {
#define SORT2(x,y) {if(data##x>data##y){auto a=std::move(data##y);data##y=std::move(data##x);data##x=std::move(a);}}
#define DD1(a)   register auto data##a=*(it+a);
#define DD2(a,b) register auto data##a=*(it+a);register auto data##b=*(it+b);
#define CB1(a)   *(it+a)=data##a;
#define CB2(a,b) *(it+a)=data##a;*(it+b)=data##b;
  DD2(1,2) SORT2(1,2)
  DD2(4,5) SORT2(4,5)
  DD1(0)   SORT2(0,2)
  DD1(3)   SORT2(3,5)
  SORT2(0,1) SORT2(3,4) SORT2(2,5) CB1(5)
  SORT2(1,4) SORT2(0,3) CB1(0)
  SORT2(2,4) CB1(4)
  SORT2(1,3) CB1(1)
  SORT2(2,3) CB2(2,3)
#undef CB1
#undef CB2
#undef DD1
#undef DD2
#undef SORT2
}

또는 참조로 변수를 전달하려면 이것을 사용하십시오 (아래 함수는 처음 5 줄에서 위와 다릅니다).

template<class T> inline void sort6(T& e0, T& e1, T& e2, T& e3, T& e4, T& e5) {
#define SORT2(x,y) {if(data##x>data##y)std::swap(data##x,data##y);}
#define DD1(a)   register auto data##a=e##a;
#define DD2(a,b) register auto data##a=e##a;register auto data##b=e##b;
#define CB1(a)   e##a=data##a;
#define CB2(a,b) e##a=data##a;e##b=data##b;
  DD2(1,2) SORT2(1,2)
  DD2(4,5) SORT2(4,5)
  DD1(0)   SORT2(0,2)
  DD1(3)   SORT2(3,5)
  SORT2(0,1) SORT2(3,4) SORT2(2,5) CB1(5)
  SORT2(1,4) SORT2(0,3) CB1(0)
  SORT2(2,4) CB1(4)
  SORT2(1,3) CB1(1)
  SORT2(2,3) CB2(2,3)
#undef CB1
#undef CB2
#undef DD1
#undef DD2
#undef SORT2
}

register키워드 를 사용하는 이유 는 레지스터에서 이러한 값을 원한다는 것을 아는 몇 가지 이유 중 하나이기 때문입니다. 이 없으면 register컴파일러는 대부분 이것을 알아낼 수 있지만 때로는 그렇지 않습니다. register키워드를 사용하면 이 문제를 해결하는 데 도움이됩니다. 그러나 일반적으로 register키워드 속도를 높이는 것보다 코드 속도가 느려질 수 있으므로 키워드를 사용하지 마십시오 .

또한 템플릿 사용에 유의하십시오. inline키워드를 사용 하더라도 템플릿 함수는 일반적으로 바닐라 C 함수보다 gcc에 의해 훨씬 더 적극적으로 최적화 되기 때문에 의도적으로 수행됩니다 (이는 템플릿 함수가 아닌 바닐라 C 함수의 함수 포인터를 처리해야하는 gcc와 관련이 있습니다).

  1. 다양한 정렬 함수를 타이밍하는 동안 정렬 함수를 호출 한 컨텍스트 (예 : 주변 코드)가 성능에 상당한 영향을 미쳤으며, 이는 함수가 인라인 된 다음 최적화 되었기 때문인 것으로 나타났습니다. 예를 들어, 프로그램이 충분히 단순하다면 정렬 함수에 포인터를 전달하는 것과 포인터를 반복자에 전달하는 것 사이에 성능에 큰 차이가 없었습니다. 그렇지 않으면 반복기를 사용하면 일반적으로 눈에 띄게 더 나은 성능을 보였으며 (내 경험으로는 적어도 지금까지는 눈에 띄게 더 나쁜 성능은 없었습니다.) g ++이 충분히 간단한 코드를 전역 적으로 최적화 할 수 있기 때문일 수 있습니다.

0

'정렬 목록 병합'을 시도하십시오. :) 두 개의 배열을 사용하십시오. 작고 큰 배열에 가장 빠릅니다.
연결하는 경우 삽입 위치 만 확인하십시오. 필요하지 않은 다른 더 큰 값은 비교합니다 (cmp = ab> 0).
4 개의 숫자의 경우 시스템 4-5cmp (~ 4.6) 또는 3-6cmp (~ 4.9)를 사용할 수 있습니다. 기포 정렬은 6 cmp (6)를 사용합니다. 큰 숫자의 느린 코드에는 많은 cmp가 있습니다.
이 코드는 5cmp (MSL 정렬 아님)를 사용합니다.
if (cmp(arr[n][i+0],arr[n][i+1])>0) {swap(n,i+0,i+1);} if (cmp(arr[n][i+2],arr[n][i+3])>0) {swap(n,i+2,i+3);} if (cmp(arr[n][i+0],arr[n][i+2])>0) {swap(n,i+0,i+2);} if (cmp(arr[n][i+1],arr[n][i+3])>0) {swap(n,i+1,i+3);} if (cmp(arr[n][i+1],arr[n][i+2])>0) {swap(n,i+1,i+2);}

교장 MSL 9 8 7 6 5 4 3 2 1 0 89 67 45 23 01 ... concat two sorted lists, list length = 1 6789 2345 01 ... concat two sorted lists, list length = 2 23456789 01 ... concat two sorted lists, list length = 4 0123456789 ... concat two sorted lists, list length = 8

JS 코드

function sortListMerge_2a(cmp)	
{
var step, stepmax, tmp, a,b,c, i,j,k, m,n, cycles;
var start = 0;
var end   = arr_count;
//var str = '';
cycles = 0;
if (end>3)
	{
	stepmax = ((end - start + 1) >> 1) << 1;
	m = 1;
	n = 2;
	for (step=1;step<stepmax;step<<=1)	//bounds 1-1, 2-2, 4-4, 8-8...
		{
		a = start;
		while (a<end)
			{
			b = a + step;
			c = a + step + step;
			b = b<end ? b : end;
			c = c<end ? c : end;
			i = a;
			j = b;
			k = i;
			while (i<b && j<c)
				{
				if (cmp(arr[m][i],arr[m][j])>0)
					{arr[n][k] = arr[m][j]; j++; k++;}
				else	{arr[n][k] = arr[m][i]; i++; k++;}
				}
			while (i<b)
				{arr[n][k] = arr[m][i]; i++; k++;
}
			while (j<c)
				{arr[n][k] = arr[m][j]; j++; k++;
}
			a = c;
			}
		tmp = m; m = n; n = tmp;
		}
	return m;
	}
else
	{
	// sort 3 items
	sort10(cmp);
	return m;
	}
}


0

사용법 cmp == 0으로 4 개의 항목을 정렬하십시오. cmp의 수는 ~ 4.34 (FF 기본 값은 ~ 4.52 임)이지만 병합 목록보다 3 배의 시간이 걸립니다. 그러나 큰 숫자 나 큰 텍스트가 있으면 cmp 작업이 줄어 듭니다. 편집 : 수리 버그

온라인 테스트 http://mlich.zam.slu.cz/js-sort/x-sort-x2.htm

function sort4DG(cmp,start,end,n) // sort 4
{
var n     = typeof(n)    !=='undefined' ? n   : 1;
var cmp   = typeof(cmp)  !=='undefined' ? cmp   : sortCompare2;
var start = typeof(start)!=='undefined' ? start : 0;
var end   = typeof(end)  !=='undefined' ? end   : arr[n].length;
var count = end - start;
var pos = -1;
var i = start;
var cc = [];
// stabilni?
cc[01] = cmp(arr[n][i+0],arr[n][i+1]);
cc[23] = cmp(arr[n][i+2],arr[n][i+3]);
if (cc[01]>0) {swap(n,i+0,i+1);}
if (cc[23]>0) {swap(n,i+2,i+3);}
cc[12] = cmp(arr[n][i+1],arr[n][i+2]);
if (!(cc[12]>0)) {return n;}
cc[02] = cc[01]==0 ? cc[12] : cmp(arr[n][i+0],arr[n][i+2]);
if (cc[02]>0)
    {
    swap(n,i+1,i+2); swap(n,i+0,i+1); // bubble last to top
    cc[13] = cc[23]==0 ? cc[12] : cmp(arr[n][i+1],arr[n][i+3]);
    if (cc[13]>0)
        {
        swap(n,i+2,i+3); swap(n,i+1,i+2); // bubble
        return n;
        }
    else    {
    cc[23] = cc[23]==0 ? cc[12] : (cc[01]==0 ? cc[30] : cmp(arr[n][i+2],arr[n][i+3]));  // new cc23 | c03 //repaired
        if (cc[23]>0)
            {
            swap(n,i+2,i+3);
            return n;
            }
        return n;
        }
    }
else    {
    if (cc[12]>0)
        {
        swap(n,i+1,i+2);
        cc[23] = cc[23]==0 ? cc[12] : cmp(arr[n][i+2],arr[n][i+3]); // new cc23
        if (cc[23]>0)
            {
            swap(n,i+2,i+3);
            return n;
            }
        return n;
        }
    else    {
        return n;
        }
    }
return n;
}

1
유스 케이스는 질문의 초기 컨텍스트와 약간 다릅니다. 고정 길이 정렬을 사용하면 세부 사항이 중요하며 스왑의 cmp를 계산하는 것으로 충분하지 않습니다. 시간을 소비하는 실제 정렬이 아니더라도 init에서 typeof ()를 호출하는 완전히 다른 빛이라면 놀라지 않을 것입니다. Javascript를 사용하여 실제 시계 시간 측정을 수행하는 방법을 모르겠습니다. 아마도 노드?
kriss

0

어쩌면 나는 파티에 늦게,하지만 적어도 내 기여는이다 새로운 접근 방식.

  • 코드는 실제로 인라인되어야합니다
  • 인라인하더라도 분기가 너무 많습니다
  • 분석 부분은 기본적으로 O (N (N-1))이며 N = 6에 대해 괜찮은 것처럼 보입니다.
  • 경우 코드가 더 효과적 일 수 의 비용이swap 높은 것 (비용 IRT compare)
  • 정적 함수가 인라인되는 것을 신뢰합니다.
  • 이 방법은 순위 정렬과 관련이 있습니다
    • 순위 대신 상대 순위 (오프셋)가 사용됩니다.
    • 순열 그룹의 모든 주기 에 대해 순위의 합이 0입니다 .
    • SWAP()두 가지 요소 를 사용 하는 대신 하나의 temp와 하나의 (register-> register) 스왑 (new <-old) 만 있으면주기가 추적됩니다.

업데이트 : 코드를 조금 변경했습니다. 일부 사람들은 C ++ 컴파일러를 사용하여 C 코드를 컴파일합니다 ...

#include <stdio.h>

#if WANT_CHAR
typedef signed char Dif;
#else
typedef signed int Dif;
#endif

static int walksort (int *arr, int cnt);
static void countdifs (int *arr, Dif *dif, int cnt);
static void calcranks(int *arr, Dif *dif);

int wsort6(int *arr);

void do_print_a(char *msg, int *arr, unsigned cnt)
{
fprintf(stderr,"%s:", msg);
for (; cnt--; arr++) {
        fprintf(stderr, " %3d", *arr);
        }
fprintf(stderr,"\n");
}

void do_print_d(char *msg, Dif *arr, unsigned cnt)
{
fprintf(stderr,"%s:", msg);
for (; cnt--; arr++) {
        fprintf(stderr, " %3d", (int) *arr);
        }
fprintf(stderr,"\n");
}

static void inline countdifs (int *arr, Dif *dif, int cnt)
{
int top, bot;

for (top = 0; top < cnt; top++ ) {
        for (bot = 0; bot < top; bot++ ) {
                if (arr[top] < arr[bot]) { dif[top]--; dif[bot]++; }
                }
        }
return ;
}
        /* Copied from RexKerr ... */
static void inline calcranks(int *arr, Dif *dif){

dif[0] =     (arr[0]>arr[1])+(arr[0]>arr[2])+(arr[0]>arr[3])+(arr[0]>arr[4])+(arr[0]>arr[5]);
dif[1] = -1+ (arr[1]>=arr[0])+(arr[1]>arr[2])+(arr[1]>arr[3])+(arr[1]>arr[4])+(arr[1]>arr[5]);
dif[2] = -2+ (arr[2]>=arr[0])+(arr[2]>=arr[1])+(arr[2]>arr[3])+(arr[2]>arr[4])+(arr[2]>arr[5]);
dif[3] = -3+ (arr[3]>=arr[0])+(arr[3]>=arr[1])+(arr[3]>=arr[2])+(arr[3]>arr[4])+(arr[3]>arr[5]);
dif[4] = -4+ (arr[4]>=arr[0])+(arr[4]>=arr[1])+(arr[4]>=arr[2])+(arr[4]>=arr[3])+(arr[4]>arr[5]);
dif[5] = -(dif[0]+dif[1]+dif[2]+dif[3]+dif[4]);
}

static int walksort (int *arr, int cnt)
{
int idx, src,dst, nswap;

Dif difs[cnt];

#if WANT_REXK
calcranks(arr, difs);
#else
for (idx=0; idx < cnt; idx++) difs[idx] =0;
countdifs(arr, difs, cnt);
#endif
calcranks(arr, difs);

#define DUMP_IT 0
#if DUMP_IT
do_print_d("ISteps ", difs, cnt);
#endif

nswap = 0;
for (idx=0; idx < cnt; idx++) {
        int newval;
        int step,cyc;
        if ( !difs[idx] ) continue;
        newval = arr[idx];
        cyc = 0;
        src = idx;
        do      {
                int oldval;
                step = difs[src];
                difs[src] =0;
                dst = src + step;
                cyc += step ;
                if(dst == idx+1)idx=dst;
                oldval = arr[dst];
#if (DUMP_IT&1)
                fprintf(stderr, "[Nswap=%d] Cyc=%d Step=%2d Idx=%d  Old=%2d New=%2d #### Src=%d Dst=%d[%2d]->%2d <-- %d\n##\n"
                        , nswap, cyc, step, idx, oldval, newval
                        , src, dst, difs[dst], arr[dst]
                        , newval  );
                do_print_a("Array ", arr, cnt);
                do_print_d("Steps ", difs, cnt);
#endif

                arr[dst] = newval;
                newval = oldval;
                nswap++;
                src = dst;
                } while( cyc);
        }

return nswap;
}
/*************/
int wsort6(int *arr)
{
return walksort(arr, 6);
}

거품 정렬처럼 보입니다. 잠재적으로 가장 느린 구현에는 좋은 경쟁자가 될 수 있지만, 코드 작업이 그렇게 큰 차이를 가져 오는지 아는 것은 여전히 ​​흥미로울 수 있습니다. 다른 코드와 동일한 형식으로 코드를 입력하면 벤치 마크를 실행할 수 있습니다.
kriss

@kriss en.wikipedia.org/wiki/Permutation_group 확실히 버블 정렬 은 아닙니다 . 코드는 주어진 순열의주기를 감지하고 이러한주기를 따라 가면서 각 요소를 최종 위치에 둡니다. 최종 wsort6()기능에는 올바른 인터페이스가 있습니다.
joop February

@ joop : 내 나쁜, 실제로 거품 정렬이 없습니다. 문맥에서 말하면 여전히 다른 현재 구현보다 코드가 훨씬 더 나쁠 것으로 기대합니다. 그런데 순위 주문 솔루션은 모든 항목의 최종 위치를 직접 찾기 때문에 스왑 수와 관련하여 최적입니다. 정렬 된 모든 숫자가 여기와 다르다는 가설을 제거 할 때 walksort가 작동하는지 확실하지 않습니다. 코드를 벤치마킹하려면 추적 코드를 사용해야합니다. 또한 일반적으로 C ++ 컴파일러에서 컴파일 할 때 OP가 변수 "new"라고 부르기 때문에 코드가 작동하지 않습니다 (구문 강조 표시가 깨짐).
kriss

이 방법은 순위 순서에 매우 가깝고 최종 할당 만 수행 됩니다 . ranks 외에도 o1..o5두 번째 임시 e[6]배열이 필요하지 않습니다 . 그리고 : C ++ 컴파일러에서 C 코드를 컴파일하고 코드를 비난합니까?
joop

@ greybeard : 감사합니다, 전에 공백을 추가했습니다 #include. 고정
wildplasser

0
//Bruteforce compute unrolled count dumbsort(min to 0-index)
void bcudc_sort6(int* a)
{
    int t[6] = {0};
    int r1,r2;

    r1=0;
    r1 += (a[0] > a[1]);
    r1 += (a[0] > a[2]);
    r1 += (a[0] > a[3]);
    r1 += (a[0] > a[4]);
    r1 += (a[0] > a[5]);
    while(t[r1]){r1++;}
    t[r1] = a[0];

    r2=0;
    r2 += (a[1] > a[0]);
    r2 += (a[1] > a[2]);
    r2 += (a[1] > a[3]);
    r2 += (a[1] > a[4]);
    r2 += (a[1] > a[5]);
    while(t[r2]){r2++;} 
    t[r2] = a[1];

    r1=0;
    r1 += (a[2] > a[0]);
    r1 += (a[2] > a[1]);
    r1 += (a[2] > a[3]);
    r1 += (a[2] > a[4]);
    r1 += (a[2] > a[5]);
    while(t[r1]){r1++;}
    t[r1] = a[2];

    r2=0;
    r2 += (a[3] > a[0]);
    r2 += (a[3] > a[1]);
    r2 += (a[3] > a[2]);
    r2 += (a[3] > a[4]);
    r2 += (a[3] > a[5]);
    while(t[r2]){r2++;} 
    t[r2] = a[3];

    r1=0;
    r1 += (a[4] > a[0]);
    r1 += (a[4] > a[1]);
    r1 += (a[4] > a[2]);
    r1 += (a[4] > a[3]);
    r1 += (a[4] > a[5]);
    while(t[r1]){r1++;}
    t[r1] = a[4];

    r2=0;
    r2 += (a[5] > a[0]);
    r2 += (a[5] > a[1]);
    r2 += (a[5] > a[2]);
    r2 += (a[5] > a[3]);
    r2 += (a[5] > a[4]);
    while(t[r2]){r2++;} 
    t[r2] = a[5];

    a[0]=t[0];
    a[1]=t[1];
    a[2]=t[2];
    a[3]=t[3];
    a[4]=t[4];
    a[5]=t[5];
}

static __inline__ void sort6(int* a)
{
    #define wire(x,y); t = a[x] ^ a[y] ^ ( (a[x] ^ a[y]) & -(a[x] < a[y]) ); a[x] = a[x] ^ t; a[y] = a[y] ^ t;
    register int t;

    wire( 0, 1); wire( 2, 3); wire( 4, 5);
    wire( 3, 5); wire( 0, 2); wire( 1, 4);
    wire( 4, 5); wire( 2, 3); wire( 0, 1); 
    wire( 3, 4); wire( 1, 2); 
    wire( 2, 3);

    #undef wire
}

속도에 관계없이 작동합니까? bruteforce 정렬에서는 루프가 모호합니다. 정렬 된 값이 0이면 작동하지 않는 것 같습니다.
kriss 2019

1
t [6] 배열은 0x0으로 초기화됩니다. 따라서 0x0 값 키가 작성되는 위치와 위치는 중요하지 않습니다.
FranG

-1

글쎄요, 6 개의 요소에 불과하고 병렬 처리를 활용할 수 있다면 조건 분기를 최소화하고 싶을 것입니다. 왜 모든 조합을 생성하지 않고 순서를 테스트하지 않습니까? 일부 아키텍처에서는 메모리가 사전 할당되어있는 한 꽤 빠를 수 있습니다.


9
720 개의 주문이 있으며 빠른 버전은 100주기 미만입니다. 대규모 병렬 처리를 활용할 수 있더라도, 아주 작은 시간 규모에서 스레드를 생성하고 동기화하는 비용은 하나의 코어에서 어레이를 정렬하는 비용을 초과 할 가능성이 높습니다.
케빈 주식

-3

다음은 세 가지 분류 알고리즘 분류를 나타내는 세 가지 일반적인 분류 방법입니다.

Insertion Sort: Θ(n^2)

Heap Sort: Θ(n log n)

Count Sort: Θ(3n)

그러나 가장 빠른 정렬 알고리즘에 대한 Stefan Nelsson의 토론을 확인하십시오 . 그는 아래로가는 해결책을 논의합니다 O(n log log n). C에서 구현을 확인하십시오.

이 Semi-Linear Sorting 알고리즘은 1995 년에 논문으로 발표되었습니다.

A. Andersson, T. Hagerup, S. Nilsson 및 R. Raman. 선형 시간으로 정렬 하시겠습니까? 컴퓨팅 이론에 관한 제 27 차 연례 ACM 심포지엄, 427-436, 1995 쪽.


8
이것은 흥미롭지 만 요점 옆에 있습니다. Big-Θ는 일정한 요인을 숨기고 문제 크기 (n)가 커질 때 추세를 보여줍니다. 여기서 문제는 고정 된 문제 크기 (n = 6)에 관한 것이며 일정한 요소를 고려합니다.
kriss

@kriss 당신은 그 사건에 대한 빠르거나 아니라면 실제 비교가 표시됩니다, 그래서 바로, 내 비교, 점근이다있어
Khaled.K

4
각각의 다른 알고리즘이 다른 K 곱셈 상수 (및 C 부가 상수)를 숨기므로 결론을 내릴 수 없습니다. 즉, 삽입 정렬의 경우 k0, c0, 힙 정렬의 경우 k1, c1 등입니다. 모든 상수는 실제로 다릅니다 (물리적 용어로 각 알고리즘에는 자체 "마찰 계수"가 있다고 말할 수 있음).이 경우 알고리즘이 실제로 더 빠르다는 결론을 내릴 수는 없습니다 (또는 고정 된 n 경우).
kriss
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.