다른 스택 오버플로 질문에 답하면 ( 이 질문 ) 흥미로운 하위 문제가 발생했습니다. 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 주기로 설정했습니다. 나는 그것을 놀랍게도 빨리 부릅니다. 다른 개선 사항이 있습니까?
__asm__ volatile (".byte 0x0f, 0x31; shlq $32, %%rdx; orq %%rdx, %0" : "=a" (x) : : "rdx");
가 EDX : EAX에 답을 넣는 반면 GCC는 단일 64 비트 레지스터에이를 기대하기 때문입니다. -O3에서 컴파일하여 버그를 볼 수 있습니다. 또한 빠른 SWAP에 대한 Paul R의 의견을 아래에서 참조하십시오.
CMP EAX, EBX; SBB EAX, EAX
에 EAX
따라 0 또는 0xFFFFFFFF를 넣습니다 . ( "carry에 추가")에 해당하는 "빌리와 함께 빼기 "입니다. 상태는 당신이 참조 비트 이다 캐리 비트. 그럼 다시, 나는 기억 하고 및 펜티엄 4 대에 처리량 끔찍한 대기 시간을 가지고 하고 , 두 번 여전히 코어 CPU에서 느린했다. 80386 이후 조건부 저장 및 조건부 이동 명령도 있지만 속도가 느립니다. EAX
EBX
SBB
ADC
ADC
SBB
ADD
SUB
SETcc
CMOVcc
x-y
그리고x+y
하지 않습니다 언더 나 오버 플로우 원인?