값이 C 배열에 있는지 빨리 찾으십니까?


124

256 크기의 배열 (바람직하게는 1024이지만 256이 최소값 임)을 반복해야하는 시간 결정적인 ISR이있는 임베디드 응용 프로그램이 있고 값이 배열 내용과 일치하는지 확인합니다. A bool는 true로 설정됩니다.

마이크로 컨트롤러는 NXP LPC4357, ARM Cortex M4 코어이고 컴파일러는 GCC입니다. 나는 이미 최적화 수준 2 (3이 더 느림)를 결합하고 플래시 대신 RAM에 기능을 배치했습니다. 나는 또한 포인터 산술과 for루프를 사용하는데, 이는 up 대신 down-counting을 수행합니다 (if i!=0검사가 if 검사보다 빠릅니다 i<256). 대체로 12.5 µs의 지속 시간으로 끝납니다. 이는 실현 가능하도록 크게 줄여야합니다. 이것은 내가 지금 사용하는 (의사) 코드입니다.

uint32_t i;
uint32_t *array_ptr = &theArray[0];
uint32_t compareVal = 0x1234ABCD;
bool validFlag = false;

for (i=256; i!=0; i--)
{
    if (compareVal == *array_ptr++)
    {
         validFlag = true;
         break;
     }
}

이를 수행하는 가장 빠른 방법은 무엇입니까? 인라인 어셈블리 사용이 허용됩니다. 다른 '덜 우아한'트릭도 허용됩니다.


28
배열에 값을 다르게 저장하는 방법이 있습니까? 정렬 할 수 있다면 이진 검색이 확실히 더 빠를 것입니다. 저장 및 검색 할 데이터가 일정 범위 내에 있으면 비트 맵 등으로 표현할 수 있습니다.
Remo.D

20
@BitBank : 지난 30 년 동안 컴파일러가 얼마나 향상되었는지 놀라 울 것입니다. ARM은 특히 컴파일러 친화적입니다. 그리고 ARM on GCC
가로드

8
멋진 질문입니다. 사람들은 성능이 중요한 실제 사례가 있다는 사실을 잊어 버립니다. 이 같은 너무 여러 번 질문은 "단지 사용 STL"로 응답합니다
년 Gmbh Kik

14
"... 배열을 통해 반복"이라는 제목은 실제로 주어진 값을 검색하는 것이므로 오해의 소지가 있습니다. 배열을 반복한다는 것은 각 항목에서 무언가를 수행해야 함을 의미합니다. 많은 검색에서 비용을 상각 할 수있는 경우 정렬은 실제로 언어 구현 문제와 관계없이 효율적인 접근 방식입니다.
hardmath

8
이진 검색이나 해시 테이블을 단순히 사용할 수 없다고 확신하십니까? 256 개 항목 == 8 개 비교에 대한 이진 검색. 해시 테이블 == 평균 1 점프 (또는 완벽한 해시가있는 경우 최대 1 점프 ). 1) 적절한 검색 알고리즘 ( O(1)또는 O(logN)과 비교 O(N))이 있고 2) 병목 현상이 발생하도록 프로파일 링 한 후에 만 ​​어셈블리 최적화에 의존해야합니다 .
Groo 2014 년

답변:


105

성능이 가장 중요한 상황에서 C 컴파일러는 수동으로 튜닝 된 어셈블리 언어로 할 수있는 작업에 비해 가장 빠른 코드를 생성하지 못할 가능성이 높습니다. 나는 최소한의 저항의 길을 택하는 경향이 있습니다. 이와 같은 작은 루틴의 경우, 저는 asm 코드를 작성하고 실행하는 데 얼마나 많은 사이클이 소요 될지 좋은 아이디어를 가지고 있습니다. C 코드를 조작하고 컴파일러가 좋은 출력을 생성하도록 할 수는 있지만 출력을 조정하는 데 많은 시간을 낭비하게 될 수 있습니다. 컴파일러 (특히 Microsoft)는 지난 몇 년 동안 먼 길을 걸어 왔지만 일반적인 경우가 아닌 특정 상황에서 작업하기 때문에 여전히 귀 사이의 컴파일러만큼 똑똑하지 않습니다. 컴파일러는이 속도를 높일 수있는 특정 명령어 (예 : LDM)를 사용하지 않을 수 있습니다. 루프를 풀기에 충분히 똑똑하지 않을 것입니다. 내 의견에서 언급 한 3 가지 아이디어를 통합하는 방법은 다음과 같습니다. 루프 풀기, 캐시 프리 페치 및 다중로드 (ldm) 명령어 사용. 명령어 사이클 수는 어레이 요소 당 약 3 클럭으로 나오지만 메모리 지연은 고려하지 않습니다.

작동 이론 : ARM의 CPU 설계는 대부분의 명령어를 한 클럭 주기로 실행하지만 명령어는 파이프 라인에서 실행됩니다. C 컴파일러는 중간에 다른 명령어를 삽입하여 파이프 라인 지연을 제거하려고합니다. 원본 C 코드와 같은 타이트한 루프가 제공되면 컴파일러는 메모리에서 읽은 값을 즉시 비교해야하기 때문에 지연을 숨기는 데 어려움을 겪습니다. 아래의 코드는 메모리 자체의 지연과 데이터를 가져 오는 파이프 라인의 지연을 크게 줄이기 위해 4 개의 레지스터로 구성된 2 세트를 번갈아 사용합니다. 일반적으로 대용량 데이터 세트로 작업 할 때 코드가 사용 가능한 레지스터의 대부분 또는 전부를 사용하지 않으면 최대 성능을 얻지 못합니다.

; r0 = count, r1 = source ptr, r2 = comparison value

   stmfd sp!,{r4-r11}   ; save non-volatile registers
   mov r3,r0,LSR #3     ; loop count = total count / 8
   pld [r1,#128]
   ldmia r1!,{r4-r7}    ; pre load first set
loop_top:
   pld [r1,#128]
   ldmia r1!,{r8-r11}   ; pre load second set
   cmp r4,r2            ; search for match
   cmpne r5,r2          ; use conditional execution to avoid extra branch instructions
   cmpne r6,r2
   cmpne r7,r2
   beq found_it
   ldmia r1!,{r4-r7}    ; use 2 sets of registers to hide load delays
   cmp r8,r2
   cmpne r9,r2
   cmpne r10,r2
   cmpne r11,r2
   beq found_it
   subs r3,r3,#1        ; decrement loop count
   bne loop_top
   mov r0,#0            ; return value = false (not found)
   ldmia sp!,{r4-r11}   ; restore non-volatile registers
   bx lr                ; return
found_it:
   mov r0,#1            ; return true
   ldmia sp!,{r4-r11}
   bx lr

업데이트 : 내 경험이 일화적이고 가치가 없다고 생각하고 증거가 필요하다고 생각하는 많은 회의론자들이 있습니다. GCC 4.8 (Android NDK 9C)을 사용하여 최적화 -O2 ( 루프 언 롤링포함하여 모든 최적화가 켜짐)으로 다음 출력을 생성했습니다. . 위의 질문에 제시된 원본 C 코드를 컴파일했습니다. GCC가 생산 한 것은 다음과 같습니다.

.L9: cmp r3, r0
     beq .L8
.L3: ldr r2, [r3, #4]!
     cmp r2, r1
     bne .L9
     mov r0, #1
.L2: add sp, sp, #1024
     bx  lr
.L8: mov r0, #0
     b .L2

GCC의 출력은 루프를 풀지 않을뿐만 아니라 LDR 이후 스톨에서 클록을 낭비합니다. 어레이 요소 당 최소 8 개의 클럭이 필요합니다. 루프를 종료 할시기를 알기 위해 주소를 사용하는 것이 좋지만 컴파일러가 수행 할 수있는 모든 마법 같은 작업은이 코드에서 찾을 수 없습니다. 타겟 플랫폼에서 코드를 실행 한 적은 없지만 (저는 소유하지 않음) ARM 코드 성능 경험이있는 사람이라면 누구나 내 코드가 더 빠르다는 것을 알 수 있습니다.

업데이트 2 : Microsoft의 Visual Studio 2013 SP2에 코드를 더 잘 처리 할 수있는 기회를주었습니다. NEON 명령어를 사용하여 배열 초기화를 벡터화 할 수 있었지만 OP에 의해 작성된 선형 값 검색은 GCC가 생성 한 것과 비슷하게 나왔습니다 (더 읽기 쉽게 레이블 이름을 변경했습니다).

loop_top:
   ldr  r3,[r1],#4  
   cmp  r3,r2  
   beq  true_exit
   subs r0,r0,#1 
   bne  loop_top
false_exit: xxx
   bx   lr
true_exit: xxx
   bx   lr

내가 말했듯이, 나는 OP의 정확한 하드웨어를 소유하고 있지 않지만, 3 가지 버전의 nVidia Tegra 3 및 Tegra 4에서 성능을 테스트하고 곧 여기에 결과를 게시 할 것입니다.

업데이트 3 : Tegra 3 및 Tegra 4 (Surface RT, Surface RT 2)에서 내 코드와 Microsoft의 컴파일 된 ARM 코드를 실행했습니다. 모든 것이 캐시에 있고 측정하기 쉽도록 일치 항목을 찾지 못하는 루프를 1000000 번 반복했습니다.

             My Code       MS Code
Surface RT    297ns         562ns
Surface RT 2  172ns         296ns  

두 경우 모두 내 코드가 거의 두 배 빠르게 실행됩니다. 대부분의 최신 ARM CPU는 비슷한 결과를 제공 할 것입니다.


13
@ LưuVĩnhPhúc-일반적으로 사실이지만 엄격한 ISR은 컴파일러보다 더 많이 알고 있다는 점에서 가장 큰 예외 중 하나입니다.
sapi 2014 년

47
악마의 옹호자 :이 코드가 더 빠르다는 정량적 증거가 있습니까?
Oliver Charlesworth 2014 년

11
@BitBank : 충분하지 않습니다. 당신은 증거 와 함께 당신의 주장을 뒷받침해야 합니다.
궤도의 가벼운 경주

13
나는 몇 년 전에 나의 교훈을 배웠다. 저는 U 및 V 파이프를 최적으로 사용하여 펜티엄의 그래픽 루틴을위한 놀랍도록 최적화 된 내부 루프를 만들었습니다. 루프 당 6 클럭 사이클 (계산 및 측정)로 내려 갔고 저는 제 자신이 매우 자랑 스러웠습니다. C로 작성된 것과 동일한 것에 대해 테스트했을 때 C가 더 빨랐습니다. 다시는 인텔 어셈블러 라인을 다시 작성하지 않았습니다.
Rocketmagnet 2014 년

14
"내 경험이 일화 적이거나 가치가 없다고 생각하고 증거가 필요하다고 생각하는 의견의 회의론자." 그들의 의견을 지나치게 부정적으로 받아들이지 마십시오. 증거를 보여 주면 훌륭한 답변이 훨씬 더 좋아집니다.
Cody Gray

87

최적화를위한 트릭이 있습니다 (한 번 면접에서이 질문을 받았습니다).

  • 배열의 마지막 항목에 찾고있는 값이 있으면 true를 반환합니다.
  • 찾고있는 값을 배열의 마지막 항목에 씁니다.
  • 찾고있는 값을 만날 때까지 배열을 반복합니다.
  • 배열의 마지막 항목 이전에 발생한 경우 true를 반환합니다.
  • 거짓 반환

bool check(uint32_t theArray[], uint32_t compareVal)
{
    uint32_t i;
    uint32_t x = theArray[SIZE-1];
    if (x == compareVal)
        return true;
    theArray[SIZE-1] = compareVal;
    for (i = 0; theArray[i] != compareVal; i++);
    theArray[SIZE-1] = x;
    return i != SIZE-1;
}

이렇게하면 반복 당 두 개의 분기 대신 반복 당 하나의 분기가 생성됩니다.


최신 정보:

에 배열을 할당 SIZE+1할 수있는 경우 "마지막 항목 교체"부분을 제거 할 수 있습니다.

bool check(uint32_t theArray[], uint32_t compareVal)
{
    uint32_t i;
    theArray[SIZE] = compareVal;
    for (i = 0; theArray[i] != compareVal; i++);
    return i != SIZE;
}

theArray[i]대신 다음을 사용하여에 포함 된 추가 산술을 제거 할 수도 있습니다 .

bool check(uint32_t theArray[], uint32_t compareVal)
{
    uint32_t *arrayPtr;
    theArray[SIZE] = compareVal;
    for (arrayPtr = theArray; *arrayPtr != compareVal; arrayPtr++);
    return arrayPtr != theArray+SIZE;
}

컴파일러가 아직 적용하지 않은 경우이 함수는 확실히 적용합니다. 반면에 최적화 프로그램에서 루프를 풀기가 더 어려워 질 수 있으므로 생성 된 어셈블리 코드에서 확인해야합니다.


2
@ratchetfreak : OP는이 배열이 언제, 어디서, 어떻게 할당되고 초기화되는지에 대한 세부 정보를 제공하지 않으므로 그것에 의존하지 않는 답변을 제공했습니다.
barak manos 2014 년

3
배열은 RAM에 있지만 쓰기는 허용되지 않습니다.
wlamers 2014 년

1
좋지만 배열이 더 이상 const아니므로 스레드로부터 안전하지 않습니다. 지불해야 할 높은 대가 인 것 같습니다.
EOF 2014 년

2
@EOF : const질문에서 어디에 언급 되었습니까?
barak manos 2014 년

4
@barakmanos : 배열과 값을 전달하고 값이 배열에 있는지 물어 보면 일반적으로 배열을 수정할 것이라고 가정하지 않습니다. 원래 질문에는 const스레드도 언급하지도 않았지만이 경고를 언급하는 것이 공정하다고 생각합니다.
EOF

62

알고리즘 최적화에 대한 도움을 요청하고 있는데, 이로 인해 어셈블러로 이동할 수 있습니다. 그러나 알고리즘 (선형 검색)은 그렇게 영리하지 않으므로 알고리즘 변경을 고려해야합니다. 예 :

완벽한 해시 함수

256 개의 "유효한"값이 정적이고 컴파일 타임에 알려진 경우 완벽한 해시 함수를 사용할 수 있습니다 . 입력 값을 0 .. n 범위의 값에 매핑하는 해시 함수를 찾아야합니다. 여기서 중요한 모든 유효한 값에 대해 충돌 이 없습니다 . 즉, 두 개의 "유효한"값이 동일한 출력 값으로 해시되지 않습니다. 좋은 해시 함수를 검색 할 때 다음을 목표로합니다.

  • 해시 함수를 합리적으로 빠르게 유지하십시오.
  • n을 최소화 하십시오 . 얻을 수있는 최소값은 256 (최소 완전 해시 함수)이지만 데이터에 따라 달성하기 어려울 수 있습니다.

효율적인 해시 함수를 위해 n 은 종종 2의 거듭 제곱이며 이는 하위 비트의 비트 마스크 (AND 연산)와 동일합니다. 해시 함수의 예 :

  • 입력 바이트의 CRC, 모듈로 n .
  • ((x << i) ^ (x >> j) ^ (x << k) ^ ...) % n(많은으로 따기 i, j, k, ... 필요에 따라 왼쪽 또는 오른쪽으로 이동 포함)

그런 다음 해시가 입력 값을 테이블 의 인덱스 i 에 매핑하는 n 항목 의 고정 테이블을 만듭니다 . 유효한 값의 경우 테이블 항목 i 에 유효한 값이 포함됩니다. 다른 모든 테이블 항목의 경우, 인덱스의 각 항목이 있는지 확인 에 해시를하지 않는 다른 잘못된 값이 포함되어 난을 .

그런 다음 인터럽트 루틴에서 입력 x :

  1. x 를 인덱스 i로 해시 (0..n 범위에 있음)
  2. 테이블에서 i 항목을 찾아 x 값이 포함되어 있는지 확인하십시오 .

이것은 256 또는 1024 값의 선형 검색보다 훨씬 빠릅니다.

나는 한 일부 파이썬 코드 작성 합리적인 해시 함수를 찾을 수 있습니다.

이진 검색

256 개의 "유효한"값으로 구성된 배열을 정렬 하면 선형 검색 대신 이진 검색을 수행 할 수 있습니다 . 즉, 256 개 항목 테이블을 8 단계 ( log2(256))로 검색하거나 1024 개 항목 테이블을 10 단계 로 검색 할 수 있어야 합니다. 다시 말하지만 이것은 256 또는 1024 값의 선형 검색보다 훨씬 빠릅니다.


감사합니다. 이진 검색 옵션은 내가 선택한 옵션입니다. 첫 번째 게시물의 이전 댓글도 참조하세요. 이것은 어셈블리를 사용하지 않고도 트릭을 잘 수행합니다.
wlamers 2014 년

11
실제로 코드를 최적화하기 전에 (예 : 어셈블리 또는 기타 트릭 사용) 알고리즘 복잡성을 줄일 수 있는지 확인해야합니다. 일반적으로 알고리즘 복잡성을 줄이는 것이 몇주기를 감추려고하지만 동일한 알고리즘 복잡성을 유지하는 것보다 더 효율적입니다.
ysdx

3
이진 검색의 경우 +1. 알고리즘 재 설계는 최적화하는 가장 좋은 방법입니다.
Rocketmagnet 2014 년

인기있는 개념은 효율적인 해시 루틴을 찾는 데 너무 많은 노력이 필요하므로 "모범 사례"는 이진 검색입니다. 하지만 때로는 "모범 사례"만으로는 충분하지 않습니다. 패킷의 헤더 (페이로드는 아님)가 도착한 순간에 네트워크 트래픽을 즉석에서 라우팅한다고 가정합니다. 이진 검색을 사용하면 제품이 절망적으로 느려질 것입니다. 임베디드 제품에는 일반적으로 x86 실행 환경에서 "모범 사례"가 임베디드에서 "쉬운 탈출구"라는 제약과 요구 사항이 있습니다.
Olof Forshell 2015-06-23

60

테이블을 정렬 된 순서로 유지하고 Bentley의 펼쳐진 이진 검색을 사용합니다.

i = 0;
if (key >= a[i+512]) i += 512;
if (key >= a[i+256]) i += 256;
if (key >= a[i+128]) i += 128;
if (key >= a[i+ 64]) i +=  64;
if (key >= a[i+ 32]) i +=  32;
if (key >= a[i+ 16]) i +=  16;
if (key >= a[i+  8]) i +=   8;
if (key >= a[i+  4]) i +=   4;
if (key >= a[i+  2]) i +=   2;
if (key >= a[i+  1]) i +=   1;
return (key == a[i]);

요점은

  • 테이블이 얼마나 큰지 안다면 얼마나 많은 반복이 있을지 알고 있으므로 완전히 펼칠 수 있습니다.
  • 그런 다음 ==마지막 반복을 제외하고 해당 케이스의 확률이 너무 낮아서 테스트에 시간을 소비하는 것을 정당화 할 수 없기 때문에 각 반복마다 케이스에 대한 포인트 테스트가 없습니다 . **
  • 마지막으로 테이블을 2의 거듭 제곱으로 확장하여 최대 1 개의 비교와 최대 2 개의 스토리지 요소를 추가합니다.

** 확률 측면에서 생각하는 데 익숙하지 않은 경우 모든 결정 지점에는 실행하여 학습하는 평균 정보 인 엔트로피 가 있습니다. 를 들어 >=테스트, 각 지점의 확률은 그래서 당신은 하나 개의 지점을 가지고가는 경우에 방법 당신은 1 개 비트를 배우고, 당신이 다른 분기를 가지고가는 경우에 당신은 한 비트, 평균 배울 것을, 0.5, 및 -log2 (0.5) 1에 관한 것입니다 각 분기에서 배운 내용의 합계에 해당 분기의 확률을 곱한 것입니다. 따라서 테스트 1*0.5 + 1*0.5 = 1의 엔트로피 >=는 1입니다. 학습 할 비트가 10 개이므로 분기가 10 개 필요합니다. 그것이 빠른 이유입니다!

반면에, 첫 번째 테스트는 if (key == a[i+512)? 참일 확률은 1/1024이고 거짓 일 확률은 1023/1024입니다. 그래서 그것이 사실이라면 당신은 모든 10 비트를 배웁니다! 그러나 그것이 거짓이라면 -log2 (1023/1024) = .00141 비트, 사실상 아무것도 배운다! 따라서 그 테스트에서 배우는 평균 양은 10/1024 + .00141*1023/1024 = .0098 + .00141 = .0112비트입니다. 약 100 분의 1 비트. 그 테스트는 그 무게를 지탱하지 않습니다!


4
이 솔루션이 정말 마음에 듭니다. 값의 위치가 민감한 정보 인 경우 타이밍 기반 포렌식을 피하기 위해 고정 된 수의 주기로 실행되도록 수정할 수 있습니다.
OregonTrail 2014 년

1
@OregonTrail : 타이밍 기반 포렌식? 재미있는 문제이지만 슬픈 코멘트.
Mike Dunlavey 2014 년

16
타이밍 공격을 방지하기 위해 암호화 라이브러리에서 이와 같은 풀린 루프를 볼 수 있습니다 . en.wikipedia.org/wiki/Timing_attack . 다음은 좋은 예입니다. github.com/jedisct1/libsodium/blob/… 이 경우 공격자가 문자열의 길이를 추측하는 것을 방지합니다. 일반적으로 공격자는 시간 공격을 수행하기 위해 함수 호출의 수백만 샘플을 취합니다.
OregonTrail 2014 년

3
+1 좋아요! 멋진 작은 펼쳐진 검색. 나는 그것을 전에 본 적이 없었다. 나는 그것을 사용할 수 있습니다.
Rocketmagnet 2014 년

1
@OregonTrail : 타이밍 기반 코멘트 두 번째입니다. 타이밍 기반 공격에 정보가 유출되는 것을 피하기 위해 고정 된 주기로 실행되는 암호화 코드를 한 번 이상 작성해야했습니다.
TonyK 2014

16

테이블의 상수 집합을 미리 알고있는 경우 완벽한 해싱 을 사용 하여 테이블에 한 번만 액세스하도록 할 수 있습니다 . 완벽한 해싱은 모든 흥미로운 키를 고유 한 슬롯에 매핑하는 해시 함수를 결정합니다 (해시 테이블은 항상 조밀하지는 않지만 조밀하지 않은 테이블은 일반적으로 더 간단한 해싱 함수로 이어지는 테이블의 조밀함을 결정할 수 있음).

일반적으로 특정 키 집합에 대한 완벽한 해시 함수는 비교적 계산하기 쉽습니다. 여러 프로브를 수행하는 데 더 많은 시간을 할애하기 때문에 길고 복잡하지 않기를 바랍니다.

완벽한 해싱은 "1-probe max"체계입니다. k 프로브를 만드는 데 걸리는 시간과 해시 코드 계산의 단순성을 교환해야한다는 생각으로 아이디어를 일반화 할 수 있습니다. 결국 목표는 최소한의 프로브 나 가장 단순한 해시 함수가 아니라 "조회하는 데 필요한 최소 총 시간"입니다. 그러나 나는 아무도 k-probes-max 해싱 알고리즘을 구축하는 것을 본 적이 없습니다. 나는 그것을 할 수 있다고 생각하지만 그것은 아마도 연구 일 것입니다.

한 가지 다른 생각 : 프로세서가 매우 빠르다면 완벽한 해시에서 메모리에 대한 하나의 프로브가 실행 시간을 지배 할 것입니다. 프로세서가 그다지 빠르지 않으면 k> 1 프로브보다 실용적 일 수 있습니다.


1
Cortex-M은 극도로 빠르지 않습니다 .
MSalters 2014 년

2
사실이 경우에는 해시 테이블이 전혀 필요하지 않습니다. 그는 특정 키가 세트에 있는지 여부 만 알고 싶고 값에 매핑하고 싶지 않습니다. 따라서 완벽한 해시 함수가 각 32 비트 값을 0 또는 1에 매핑하면 충분합니다. 여기서 "1"은 "세트에 있음"으로 정의 될 수 있습니다.
David Ongaro 2014 년

1
그가 그러한 매핑을 생성하는 완벽한 해시 생성기를 얻을 수 있다면 좋은 지적입니다. 그러나 그것은 "매우 조밀 한 세트"일 것입니다. 나는 그가 그것을 수행하는 완벽한 해시 생성기를 찾을 수 있습니다. 그는 세트에 있으면 상수 K를 생성하고 세트에 없으면 K를 제외한 모든 값을 생성하는 완벽한 해시를 얻는 것이 더 나을 수 있습니다. 나는 후자에 대해서도 완벽한 해시를 얻는 것이 어렵다고 생각합니다.
Ira Baxter

@DavidOngaro table[PerfectHash(value)] == value는 값이 세트에 있으면 1을, 그렇지 않으면 0을 산출하며, PerfectHash 함수를 생성하는 잘 알려진 방법이 있습니다 (예 : burtleburtle.net/bob/hash/perfect.html 참조 ). 세트의 모든 값을 1로 직접 매핑하는 해시 함수를 찾고 0으로 설정되지 않은 모든 값을 찾으려는 것은 어리석은 작업입니다.
Jim Balter 2014 년

@DavidOngaro : 완벽한 해시 함수에는 많은 "거짓 긍정"이 있습니다. 즉, 집합에 없는 값은 집합에있는 값과 동일한 해시를 갖게됩니다. 따라서 "in-the-set"입력 값을 포함하는 해시 값으로 인덱싱 된 테이블이 있어야합니다. 따라서 주어진 입력 값의 유효성을 검사하려면 (a) 해시합니다. (b) 해시 값을 사용하여 테이블 조회를 수행합니다. (c) 테이블의 항목이 입력 값과 일치하는지 확인하십시오.
Craig McQueen

14

해시 세트를 사용하십시오. O (1) 조회 시간을 제공합니다.

다음 코드는 0실제 데이터에서 발생하지 않는 '빈'값으로 값 을 예약 할 수 있다고 가정합니다 . 그렇지 않은 상황에서 솔루션을 확장 할 수 있습니다.

#define HASH(x) (((x >> 16) ^ x) & 1023)
#define HASH_LEN 1024
uint32_t my_hash[HASH_LEN];

int lookup(uint32_t value)
{
    int i = HASH(value);
    while (my_hash[i] != 0 && my_hash[i] != value) i = (i + 1) % HASH_LEN;
    return i;
}

void store(uint32_t value)
{
    int i = lookup(value);
    if (my_hash[i] == 0)
       my_hash[i] = value;
}

bool contains(uint32_t value)
{
    return (my_hash[lookup(value)] == value);
}

이 예제 구현에서 조회 시간은 일반적으로 매우 낮지 만 최악의 경우 저장된 항목 수까지 될 수 있습니다. 실시간 애플리케이션의 경우보다 예측 가능한 조회 시간을 갖는 이진 트리를 사용하는 구현을 고려할 수도 있습니다.


3
이 검색이 효과적이려면이 조회를 몇 번 수행해야하는지에 따라 다릅니다.
maxywb 2014 년

1
어, 조회는 어레이 끝에서 실행될 수 있습니다. 그리고 이런 종류의 선형 해싱은 충돌 률이 높습니다. O (1)을 얻을 수 없습니다. 좋은 해시 세트는 이와 같이 구현되지 않습니다.
Jim Balter 2014 년

@JimBalter 사실, 완벽한 코드가 아닙니다. 일반적인 아이디어와 더 비슷합니다. 기존 해시 세트 코드를 가리킬 수 있습니다. 그러나 이것이 인터럽트 서비스 루틴이라는 점을 고려하면 조회가 매우 복잡한 코드가 아님을 입증하는 것이 유용 할 수 있습니다.
JPA

당신은 그것을 고쳐서 내가 감싸도록해야합니다.
Jim Balter 2014 년

완벽한 해시 함수의 요점은 하나의 프로브를 수행한다는 것입니다. 기간.
Ira Baxter

10

이 경우 Bloom 필터를 조사하는 것이 좋습니다 . 그들은 값이 존재하지 않는다는 것을 신속하게 설정할 수 있습니다. 이는 가능한 2 ^ 32 값의 대부분이 1024 요소 배열에 없기 때문에 좋은 것입니다. 그러나 추가 검사가 필요한 오 탐지가 있습니다.

테이블이 정적으로 보이기 때문에 Bloom 필터에 대해 어떤 오 탐지가 존재하는지 확인하고이를 완벽한 해시에 넣을 수 있습니다.


1
흥미롭게도 이전에 블룸 필터를 본 적이 없었습니다.
Rocketmagnet 2014 년

8

프로세서가 LPC4357의 최대 값 인 204MHz에서 실행되고 타이밍 결과가 평균 케이스 (배열의 절반)를 반영한다고 가정하면 다음과 같은 결과를 얻을 수 있습니다.

  • CPU 주파수 : 204MHz
  • 주기 기간 : 4.9ns
  • 주기 기간 : 12.5µs / 4.9ns = 2551주기
  • 반복 당주기 : 2551/128 = 19.9

따라서 검색 루프는 반복 당 약 20 사이클을 소비합니다. 끔찍하게 들리지는 않지만 더 빨리 만들려면 어셈블리를 살펴 봐야한다고 생각합니다.

인덱스를 삭제하고 대신 포인터 비교를 사용하고 모든 포인터를 만드는 것이 좋습니다 const.

bool arrayContains(const uint32_t *array, size_t length)
{
  const uint32_t * const end = array + length;
  while(array != end)
  {
    if(*array++ == 0x1234ABCD)
      return true;
  }
  return false;
}

적어도 테스트 할 가치가 있습니다.


1
-1, ARM에는 인덱스 주소 모드가 있으므로 이것은 의미가 없습니다. 포인터를 만들 때 constGCC는 이미 변경되지 않는 것을 발견했습니다. constdoesnt't의 추가 아무것도 중 하나.
MSalters 2014 년

11
OK @MSalters, 내가 생성 된 코드를 확인하지 못했다는 점은 C 수준에서 간단하게 뭔가를 표현하기 위해, 그리고 난 그냥 포인터 대신 포인터를 관리 생각하고 인덱스 입니다 간단한. 나는 단순히 " const아무것도 추가하지 않는다 " 는 것에 동의 하지 않는다 : 그것은 독자들에게 가치가 변하지 않을 것이라는 것을 매우 분명하게 알려준다. 그것은 환상적인 정보입니다.
언 와인드

9
이것은 깊이 삽입 된 코드입니다. 지금까지 최적화에는 코드를 플래시에서 RAM으로 이동하는 것이 포함되었습니다. 그러나 여전히 더 빨라야합니다. 이 시점에서 가독성은 목표가 아닙니다 .
MSalters 2014 년

1
@MSalters "ARM에는 인덱스 주소 모드가 있으므로 이것은 무의미합니다."-음, 요점을 완전히 놓친 경우 ... OP는 "나는 또한 포인터 산술과 for 루프를 사용합니다"라고 썼습니다. unwind는 인덱싱을 포인터로 대체하지 않았으며 인덱스 변수를 제거했기 때문에 모든 루프 반복에서 추가 감산이 발생했습니다. 그러나 OP는 현명했고 (많은 사람들이 대답하고 논평하는 것과는 달리) 결국 이진 검색을 수행했습니다.
Jim Balter 2014 년

6

다른 사람들은 이진 검색을 제공하기 위해 테이블을 재구성하거나 끝에 센티널 값을 추가하거나 정렬 할 것을 제안했습니다.

당신은 "나는 또한 포인터 산술과 for 루프를 사용하는데, 이것은 up 대신 down-counting을 i != 0한다.i < 256 )."

내 첫 번째 조언은 포인터 산술과 다운 카운팅을 제거하는 것입니다. 같은 물건

for (i=0; i<256; i++)
{
    if (compareVal == the_array[i])
    {
       [...]
    }
}

컴파일러에게 관용적 인 경향이 있습니다 . 루프는 관용적이며 루프 변수에 대한 배열의 인덱싱은 관용적입니다. 포인터 연산과 포인터와 저글링하는 경향이 당황 컴파일러에 숙어를하고 어떤 관련 코드를 생성 할 수 있도록 당신이 컴파일러 작가가 일반에 대한 최선로 결정 무엇보다는 썼다 작업 .

예를 들어, 위의 코드는 인덱싱 off에서 0 에서 -256또는 -2550으로 실행되는 루프로 컴파일 될 수 있습니다 &the_array[256]. 유효한 C로 표현할 수 없지만 생성하려는 시스템의 아키텍처와 일치하는 항목 일 수 있습니다.

따라서 미세 최적화 하지 마십시오 . 옵티 마이저 작업에 스패너를 던지고 있습니다. 영리 해지고 싶다면 데이터 구조와 알고리즘에 대해 작업하되 표현을 미세 최적화하지 마십시오. 현재 컴파일러 / 아키텍처가 아니라면 다음 번에 다시 물릴 것입니다.

특히 배열과 인덱스 대신 포인터 산술을 사용하는 것은 컴파일러가 정렬, 저장 위치, 앨리어싱 고려 사항 및 기타 사항을 완전히 인식하고 머신 아키텍처에 가장 적합한 방식으로 강도 감소와 같은 최적화를 수행하는 데 독이됩니다.


포인터에 대한 루프는 C에서 관용적이며 좋은 최적화 컴파일러는 인덱스뿐만 아니라 포인터를 처리 할 수 ​​있습니다. 그러나 OP가 이진 검색을 끝내기 때문에이 모든 것은 논쟁의 여지가 있습니다.
Jim Balter 2014 년

3

벡터화는 memchr 구현에서 자주 사용되므로 여기서 사용할 수 있습니다. 다음 알고리즘을 사용합니다.

  1. OS의 비트 수 (64 비트, 32 비트 등)와 동일한 길이로 쿼리 반복 마스크를 만듭니다. 64 비트 시스템에서는 32 비트 쿼리를 두 번 반복합니다.

  2. 목록을 더 큰 데이터 유형의 목록으로 캐스팅하고 값을 가져 오는 방식으로 목록을 한 번에 여러 데이터의 목록으로 처리합니다. 각 청크에 대해 마스크로 XOR 한 다음 0b0111 ... 1로 XOR 한 다음 1을 더한 다음 & 마스크를 0b1000 ... 0으로 반복합니다. 결과가 0이면 확실히 일치하는 것이 없습니다. 그렇지 않으면 (보통 매우 높은 확률로) 일치가있을 수 있으므로 청크를 정상적으로 검색하십시오.

구현 예 : https://sourceware.org/cgi-bin/cvsweb.cgi/src/newlib/libc/string/memchr.c?rev=1.3&content-type=text/x-cvsweb-markup&cvsroot=src


3

애플리케이션 에서 사용할 수있는 메모리 양으로 값의 도메인을 수용 할 수있는 경우 가장 빠른 솔루션은 배열을 비트 배열로 나타내는 것입니다.

bool theArray[MAX_VALUE]; // of which 1024 values are true, the rest false
uint32_t compareVal = 0x1234ABCD;
bool validFlag = theArray[compareVal];

편집하다

나는 비평가의 수에 놀랐다. 이 스레드의 제목은 "값이 C 배열에 있는지 여부를 어떻게 빨리 찾을 수 있습니까?"입니다. 내 대답이 바로 그것에 대한 대답이기 때문에 나는 내 대답을지지 할 것입니다. 나는 이것이 가장 속도 효율적인 해시 함수를 가지고 있다고 주장 할 수 있습니다 (주소 === 값 이후). 나는 코멘트를 읽었고 명백한주의 사항을 알고 있습니다. 의심 할 여지없이 이러한 경고는 해결하는 데 사용할 수있는 문제의 범위를 제한하지만 해결하는 문제의 경우 매우 효율적으로 해결됩니다.

이 대답을 완전히 거부하는 대신 해시 함수를 사용하여 속도와 성능 간의 균형을 더 잘 맞출 수있는 최적의 시작점으로 간주하십시오.


8
어떻게 4 개의 찬성표를 얻나요? 질문에는 Cortex M4라고 나와 있습니다. 262.144KB가 아닌 136KB RAM이 있습니다.
MSalters 2014 년

1
응답자가 나무를 위해 숲을 놓 쳤기 때문에 명백하게 잘못된 답변에 얼마나 많은 찬성표가 주어 졌는지 놀랍습니다. OP의 가장 큰 경우 O (log n) << O (n).
msw

3
훨씬 더 나은 솔루션을 사용할 수있을 때 엄청나게 많은 양의 메모리를 태우는 프로그래머에게 매우 심술 궂습니다. 5 년마다 내 PC의 메모리가 부족한 것 같습니다. 5 년 전에는 그 양이 많았습니다.
Craig McQueen 2014 년

1
@CraigMcQueen Kids 요즘. 메모리 낭비. 어이 없어! 예전에는 1MiB의 메모리와 16 비트의 워드 크기를 가졌습니다. / s
Cole Johnson

2
가혹한 비평가는 무엇입니까? OP는이 코드 부분에서 속도가 절대적으로 중요하다고 명시하고 StephenQuan은 이미 "엄청난 양의 메모리"를 언급했습니다.
Bogdan Alexandru

1

명령 ( "의사 코드")과 데이터 ( "theArray")가 별도의 (RAM) 메모리에 있는지 확인하여 CM4 Harvard 아키텍처를 최대한 활용하십시오. 사용자 설명서에서 :

여기에 이미지 설명 입력

CPU 성능을 최적화하기 위해 ARM Cortex-M4에는 명령어 (코드) (I) 액세스, 데이터 (D) 액세스 및 시스템 (S) 액세스를위한 세 개의 버스가 있습니다. 명령어와 데이터가 별도의 메모리에 보관되면 코드 및 데이터 액세스가 한 사이클에서 병렬로 수행 될 수 있습니다. 코드와 데이터가 동일한 메모리에 보관되는 경우 데이터를로드하거나 저장하는 명령어는 두주기가 걸릴 수 있습니다.


흥미롭게도 Cortex-M7에는 선택적 명령 / 데이터 캐시가 있지만 그 전에는 확실히 그렇지 않습니다. en.wikipedia.org/wiki/ARM_Cortex-M#Silicon_customization .
Peter Cordes

0

내 대답이 이미 대답 되었다면 미안합니다-나는 게으른 독자입니다. 그런 다음 자유롭게 반대 투표하십시오))

1) 카운터 'i'를 제거 할 수 있습니다. 포인터를 비교하면됩니다.

for (ptr = &the_array[0]; ptr < the_array+1024; ptr++)
{
    if (compareVal == *ptr)
    {
       break;
    }
}
... compare ptr and the_array+1024 here - you do not need validFlag at all.

그래도 크게 개선되지는 않지만 이러한 최적화는 컴파일러 자체에서 수행 할 수 있습니다.

2) 다른 답변에서 이미 언급했듯이 거의 모든 최신 CPU는 ARM과 같은 RISC 기반입니다. 최신 Intel X86 CPU조차도 내가 아는 한 내부에서 RISC 코어를 사용합니다 (즉석에서 X86에서 컴파일). RISC의 주요 최적화는 코드 점프를 최소화하는 파이프 라인 최적화 (Intel 및 기타 CPU의 경우)입니다. 이러한 최적화의 한 유형 (아마도 주요 최적화)은 "사이클 롤백"입니다. 믿을 수 없을 정도로 어리 석고 효율적이며 인텔 컴파일러조차도 AFAIK를 수행 할 수 있습니다. 다음과 같이 보입니다.

if (compareVal == the_array[0]) { validFlag = true; goto end_of_compare; }
if (compareVal == the_array[1]) { validFlag = true; goto end_of_compare; }
...and so on...
end_of_compare:

이런 식으로 최적화는 파이프 라인이 최악의 경우 (배열에 compareVal이없는 ​​경우) 중단되지 않기 때문에 가능한 한 빠릅니다 (물론 해시 테이블, 정렬 된 배열 등의 알고리즘 최적화는 계산하지 않음). 배열 크기에 따라 더 나은 결과를 줄 수있는 다른 답변에서 언급했습니다. 사이클 롤백 접근 방식도 거기에 적용될 수 있습니다. 나는 다른 사람들에서 보지 못했다고 여기에 쓰고 있습니다)

이 최적화의 두 번째 부분은 배열 항목이 직접 주소 (컴파일 단계에서 계산되고 정적 배열을 사용하는지 확인)로 가져오고 배열의 기본 주소에서 포인터를 계산하기 위해 추가 ADD 연산이 필요하지 않다는 것입니다. AFAIK ARM 아키텍처에는 배열 주소 지정 속도를 높이는 특수 기능이 있기 때문에이 최적화는 큰 영향을 미치지 않을 수 있습니다. 그러나 어쨌든 C 코드에서 직접 최선을 다했다는 것을 아는 것이 항상 낫습니다.

사이클 롤백은 ROM 낭비로 인해 어색해 보일 수 있지만 (예, 보드가이 기능을 지원하는 경우 RAM의 빠른 부분에 올바르게 배치했습니다) 실제로 RISC 개념을 기반으로하는 속도에 대한 공정한 지불입니다. 이것은 계산 최적화의 일반적인 지점 일뿐입니다. 요구 사항에 따라 속도를 위해 공간을 희생하고 그 반대의 경우도 마찬가지입니다.

1024 개의 요소 배열에 대한 롤백이 귀하의 경우에 너무 큰 희생이라고 생각한다면, 예를 들어 배열을 각각 512 개 항목의 2 개 부분 또는 4x256으로 나누는 등 '부분적 롤백'을 고려할 수 있습니다.

3) 최신 CPU는 종종 SIMD 연산 (예 : ARM NEON 명령어 세트)을 지원합니다. 동일한 연산을 병렬로 실행할 수 있습니다. 솔직히 말해서 비교 작전에 적합한 지 기억이 나지 않지만 그럴 수도 있다고 생각합니다. 인터넷 검색은 최대 속도를 얻으려면 몇 가지 트릭이있을 수 있음을 보여줍니다. https://stackoverflow.com/a/5734019/1028256 참조

나는 그것이 당신에게 새로운 아이디어를 줄 수 있기를 바랍니다.


OP는 선형 루프 최적화에 초점을 맞춘 모든 어리석은 대답을 우회하고 대신 배열을 사전 정렬하고 이진 검색을 수행했습니다.
Jim Balter 2014 년

@Jim, 그런 종류의 최적화가 먼저 이루어져야한다는 것이 분명합니다. 예를 들어 배열을 정렬 할 시간이없는 경우 '어리석은'답변은 일부 사용 사례에서 어리석지 않게 보일 수 있습니다. 또는 만약 당신이 얻는 속도가 충분하지 않다면
Mixaz 2014 년

"그런 종류의 최적화가 먼저 이루어져야한다는 것은 분명합니다."-분명히 선형 솔루션을 개발하기 위해 많은 노력을 기울인 사람들에게는 해당되지 않습니다. "어레이를 정렬 할 시간이 없습니다."-그게 무슨 뜻인지 모르겠습니다. "또는 당신이 얻는 속도가 어쨌든 충분하지 않다면"-어, 이진 검색의 속도가 "충분하지 않다"면 최적화 된 선형 검색을 수행해도 향상되지 않습니다. 이제이 주제를 끝냈습니다.
Jim Balter 2014 년

@JimBalter, OP와 같은 문제가 있으면 이진 검색과 같은 algs 사용을 확실히 고려할 것입니다. 나는 OP가 이미 그것을 고려하지 않았다고 생각할 수 없었다. "배열을 정렬 할 시간이 없습니다"는 배열 정렬에 시간이 걸린다는 의미입니다. 각 입력 데이터 세트에 대해 수행해야하는 경우 선형 루프보다 더 오래 걸릴 수 있습니다. "또는 만약 당신이 얻는 속도가 충분하지 않다면, 어쨌든 충분하지 않다"는 것은 다음을 의미합니다-위의 최적화 힌트는 바이너리 검색 코드 또는 기타 속도를 높이는 데 사용될 수 있습니다
Mixaz

0

저는 해싱의 훌륭한 팬입니다. 물론 문제는 빠르면서도 최소한의 메모리를 사용하는 효율적인 알고리즘을 찾는 것입니다 (특히 임베디드 프로세서에서).

발생할 수있는 값을 미리 알고있는 경우 여러 알고리즘을 통해 실행되는 프로그램을 만들어 최상의 데이터 또는 데이터에 가장 적합한 매개 변수를 찾을 수 있습니다.

이 포스트 에서 읽을 수있는 프로그램을 만들었고 매우 빠른 결과를 얻었습니다. 16000 개 항목은 이진 검색을 사용하여 값을 찾기 위해 대략 2 ^ 14 또는 평균 14 개의 비교로 변환됩니다. 필자는 평균적으로 <= 1.5 조회에서 값을 찾는 매우 빠른 조회를 목표로하여 RAM 요구 사항이 더 커졌습니다. 보다 보수적 인 평균값 (예 : <= 3)으로 많은 메모리를 절약 할 수 있다고 생각합니다. 비교해 보면 256 개 또는 1024 개 항목에 대한 이진 검색의 평균 사례는 각각 8 개와 10 개 항목의 평균 비교 결과가됩니다.

내 평균 조회는 일반 알고리즘 (변수로 한 분할 사용)을 사용하는 약 60주기 (인텔 i5가있는 랩톱에서)와 특수화 (아마도 곱셈 사용)를 사용하는 40-45주기가 필요했습니다. 이것은 MCU가 실행되는 클럭 주파수에 따라 마이크로 초 미만의 조회 시간으로 변환되어야합니다.

항목 배열이 항목에 액세스 한 횟수를 추적하면 실제 생활에서 더 많이 조정할 수 있습니다. indeces가 계산되기 전에 항목 배열이 가장 많이 액세스 된 것부터 가장 적게 액세스 된 것 순으로 정렬 된 경우 단일 비교로 가장 일반적으로 발생하는 값을 찾습니다.


0

이것은 답변 이라기보다는 부록에 가깝습니다.

내가 했어 유사한 과거 사례를,하지만 내 배열은 검색의 상당수를 통해 일정이었다.

그 중 절반에서는 검색된 값이 배열에 존재하지 않았습니다. 그런 다음 검색을 수행하기 전에 "필터"를 적용 할 수 있다는 것을 깨달았습니다.

이러한 "필터"는 단순한 정수 계산된다 ONCE 각 검색에 사용된다.

Java로되어 있지만 매우 간단합니다.

binaryfilter = 0;
for (int i = 0; i < array.length; i++)
{
    // just apply "Binary OR Operator" over values.
    binaryfilter = binaryfilter | array[i];
}

따라서 이진 검색을 수행하기 전에 binaryfilter를 확인합니다.

// Check binaryfilter vs value with a "Binary AND Operator"
if ((binaryfilter & valuetosearch) != valuetosearch)
{
    // valuetosearch is not in the array!
    return false;
}
else
{
    // valuetosearch MAYBE in the array, so let's check it out
    // ... do binary search stuff ...

}

'더 나은'해시 알고리즘을 사용할 수 있지만 특히 큰 수의 경우 매우 빠를 수 있습니다. 이것은 더 많은 사이클을 절약 할 수 있습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.