2010-06-16 2 views
1

성능이 저하되고 최악의 경우 프로그램이 충돌하는 포인터가 잘못 정렬되지 않았습니다 (잘못된 c 프로그램을 컴파일하기에 충분히 좋은 컴파일러라고 가정).잘못 정렬 된 포인터 성능

글쎄, 다음 코드는 정렬 된 버전과 정렬되지 않은 버전간에 성능 차이가없는 것 같습니다. 왜 그런가요?

/* brutality.c */ 

#ifdef BRUTALITY 
    xs = (unsigned long *) ((unsigned char *) xs + 1); 
#endif 

...

/* main.c */ 

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

#define size_t_max ((size_t)-1) 
#define max_count(var) (size_t_max/(sizeof var)) 

int main(int argc, char *argv[]) { 

    unsigned long sum, *xs, *itr, *xs_end; 
    size_t element_count = max_count(*xs) >> 4; 

    xs = malloc(element_count * (sizeof *xs)); 
    if(!xs) exit(1); 

    xs_end = xs + element_count - 1; sum = 0; 

    for(itr = xs; itr < xs_end; itr++) 
     *itr = 0; 

#include "brutality.c" 

    itr = xs; 
    while(itr < xs_end) 
     sum += *itr++; 

    printf("%lu\n", sum); 

    /* we could free the malloc-ed memory here */ 
    /* but we are almost done     */ 
    exit(0); 
} 

가 집계하고

gcc -pedantic -Wall -O0 -std=c99 main.c 
for i in {0..9}; do time ./a.out; done 
+0

나는 당신의 트릭을 이해하지 못한다는 것을 인정합니다. size_t는 유형입니다. 그래서 size_t-1은 무엇입니까? 작은 정련은 sum + = itr ++의 마지막 반복은 BRUTALITY가 정의되었을 때 할당 한 것 이상으로 메모리를 읽는다는 것입니다. 나는 printf와 쉘 호출과 루핑을 죽일 것이다. 프로그램 내부의 모든 타이밍을 수행하고 프로그램 내부에 10x 루프를 추가하십시오. –

+0

음, xs_end는 마지막 요소를 지키지 않고 마지막 요소를 가리 킵니다.이 경우 마지막 할당 된 버퍼를 지나서 쓰게됩니다. 그리고 size_t는 부호없는 유형이므로 표준에서는 ... "Google"SIZE_MAX 휴대용을 보장합니다. 그리고 아니오, printf는 심지어 -O2를 사용할 때 컴파일러가 모든 코드를 배수구 아래로 던지지 않도록 보장합니다. –

+0

오, 죄송합니다. size_t에 -1 형 변환합니다. 그리고 맞습니다. 정상적인 xs_end 컨벤션을 가정했습니다. 나는 여전히 당신이 printf() 및 다른 무거운 OS 물건을 루프 밖으로 유지하려고해야한다고 생각합니다. 오 강한 생각이 있습니다. 대신 새로운 대답을하겠습니다. –

답변

3

필자는 이전에 Win32 컴퓨터에서이 기능을 테스트 해본 결과 32 비트 컴퓨터에서 많은 불이익을 느끼지 못했습니다. 64 비트에서는 상당히 느려졌습니다. 예를 들어 다음 코드를 실행했습니다. 32 비트 컴퓨터에서 인쇄 된 시간은 거의 변경되지 않았습니다. 그러나 64 비트 시스템에서 잘못 정렬 된 액세스 시간은 거의 두 배 정도 길었습니다. 시간은 코드를 따른다.

#define UINT unsigned __int64 
#define ENDPART QuadPart 
#else 
#define UINT unsigned int 
#define ENDPART LowPart 
#endif 


int main(int argc, char *argv[]) 
{ 
    LARGE_INTEGER startCount, endCount, freq; 
    int i; 
    int offset; 
    int iters = atoi(argv[1]); 
    char *p = (char*)malloc(16); 
    double *d; 

    for (offset = 0; offset < 9; offset++) 
     { 
     d = (double*)(p + offset); 
     printf("Address alignment = %u\n", (unsigned int)d % 8); 
     *d = 0; 
     QueryPerformanceFrequency(&freq); 
     QueryPerformanceCounter(&startCount); 
     for(i = 0; i < iters; ++i) 
     *d = *d + 1.234; 
     QueryPerformanceCounter(&endCount); 

     printf("Time: %lf\n", 
      (double)(endCount.ENDPART-startCount.ENDPART)/freq.ENDPART); 
     } 
} 

다음은 64 비트 시스템의 결과입니다. 코드를 32 비트 응용 프로그램으로 컴파일했습니다.

[P:\t]pointeralignment.exe 100000000 
Address alignment = 0 
Time: 0.484156 
Address alignment = 1 
Time: 0.861444 
Address alignment = 2 
Time: 0.859656 
Address alignment = 3 
Time: 0.861639 
Address alignment = 4 
Time: 0.860234 
Address alignment = 5 
Time: 0.861539 
Address alignment = 6 
Time: 0.860555 
Address alignment = 7 
Time: 0.859800 
Address alignment = 0 
Time: 0.484898 
+0

Brilliant. 이것은 내가 가상 호스트와 다른 아키텍처의 클라이언트를 포함하여 더 많은 기계를 시험해 보도록 해줍니다. –

0

86 또는 64를 사용하여 두 개의 별도의 시스템에서 테스트 ?

잘못된 포인터는 64 비트 아키텍처가 충돌이나 성능 저하가 거의 발생하지 않는 x86에서의 킬러였습니다.

+0

"AMD Athlon (tm) XP 2700+"및 "Intel (R) CPU 5150 @ 2.66GHz"에서 테스트되었지만, 결과가 틀린 곳이라면 정말 좋습니다. –

2

x86 아키텍처는 항상 정렬되지 않은 액세스를 처리 할 수 ​​있으므로 충돌이 발생하지 않습니다. 다른 프로세서는 운이 좋지 않을 수 있습니다.

루프가 메모리 바인딩되어 있으므로 시간 차이가 나타나지 않을 수도 있습니다. RAM에서 데이터를 가져올 수있을 때만 실행할 수 있습니다. 정렬 불일치로 인해 RAM에 두 번 액세스 할 수 있다고 생각할 수도 있지만 첫 번째 액세스에서는이를 캐시에 저장하고 두 번째 액세스는 RAM에서 다음 값을 가져 오는 것과 중복 될 수 있습니다.

+0

이것은 가장 합리적인 답처럼 보입니다. 어떤 경우, 어떤 프로세서가 겹치는 메모리 주소를 캐시하지 않습니다. 어떤 경우에 ... 관련된 성능 차이가 언제 생깁니 까? 그 중 하나 또는 내 테스트는 짜증. –

+0

@EliteMx : "관련 성능 차이는 언제 발생합니까?" 응용 프로그램이 실제로 많은 메모리에 액세스하는 경우 중복 된 데이터 (코드)도 단순히 유효 캐시 크기를 줄입니다. 솔직히 말해서 CPU 매뉴얼을 더 잘 읽는 것이 좋습니다. 나는 당신이 Intel-IA-32 또는 EM64T를 사용하고 있다고 추정하고 있습니다. http://www.intel.com/products/processor/manuals/index.htm. – Dummy00001

0

아마도 많은 바이트의 malloc이 NULL을 반환하기 때문일 수 있습니다. 적어도 그것이 나를 위해하는 일입니다.

+0

내 코드가 printf 문에 도착합니다 (예 : malloc succeeds) ... 4가 아닌 5,6,7 비트로 시프트 할 수 있습니다. –

0

게시 된 코드에 BRUTALITY을 정의한 적이 없습니다. '잔인'모드로 테스트 중이십니까?

+0

물론. 내가 정의했다면 ... BRUTALITY 모드 없이는 테스트를 해보지 않았을 것입니다 : P –

0

아마도 거대한 버퍼를 malloc하기 위해서, 시스템은 디스크와 메모리를 페이징하고 있습니다. 그것은 작은 차이를 늪 수 있습니다. 훨씬 더 작은 버퍼와 큰 프로그램 루프 횟수로 시도해보십시오.

필자가 제안한 개조를 덧붙여서 내 시스템 (피곤한, 4 살, 32 비트 노트북)에서 테스트했습니다. 코드는 아래와 같습니다. 측정 가능한 차이는 있지만 약 3 % 정도입니다. 나는 당신의 질문이 아무런 차이가 없다는 것을 나타 내기 때문에 나의 변화가 성공적이라는 것을 계속 유지합니까?

죄송합니다. Windows 용 특정 GetTickCount() API를 사용하고 있습니다. 타이밍 테스트를 자주 수행하고 misnamed API (시스템 시작 이후 실제로 밀리 초를 반환 함)의 단순함을 즐기기 때문에 익숙합니다.

/* main.cpp */ 

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

#define BRUTALITY 

int main(int argc, char *argv[]) { 
    unsigned long i, begin, end; 
    unsigned long sum, *xs, *itr, *xs_begin, *xs_end; 
    size_t element_count = 100000; 

    xs = (unsigned long *)malloc(element_count * (sizeof *xs)); 
    if(!xs) exit(1); 
    xs_end = xs + element_count - 1; 
    #ifdef BRUTALITY 
    xs_begin = (unsigned long *) ((unsigned char *) xs + 1); 
    #else 
    xs_begin = xs; 
    #endif 

    begin = GetTickCount(); 
    for(i=0; i<50000; i++) 
    { 
     for(itr = xs_begin; itr < xs_end; itr++) 
      *itr = 0; 

     sum = 0; 
     itr = xs_begin; 
     while(itr < xs_end) 
      sum += *itr++; 
    } 
    end = GetTickCount(); 

    printf("sum=%lu elapsed time=%lumS\n", sum, end-begin); 

    free(xs); 
    exit(0); 
} 
1

여러분은 x86 또는 x64 아키텍처를 사용하고 있다고 가정합니다.예를 들어, MIPS에서 코드는 SIGBUS (버스 오류) 신호가 발생할 수 있습니다. 다른 아키텍처에서는 정렬되지 않은 액세스가 일반적으로 정렬 된 액세스보다 느리지 만 아키텍처에 많이 의존합니다.

+0

아니요. 나는 아무것도 추측하지 않고있다. 내가 처음 쓴 문장을 읽으십시오. 실제로 그것을 실제로 보는 (버스 결함, 느린 접근) 나를 매우 행복하게 만들 것입니다. 또한 ARM CPU [N900]에서 결과를 테스트하려고합니다. –

관련 문제