ค้นหาได้อย่างรวดเร็วว่ามีค่าอยู่ในอาร์เรย์ C หรือไม่


124

ฉันมีแอปพลิเคชั่นฝังตัวที่มี ISR ที่สำคัญตามเวลาซึ่งจำเป็นต้องวนซ้ำผ่านอาร์เรย์ขนาด 256 (ควรเป็น 1024 แต่ 256 เป็นค่าต่ำสุด) และตรวจสอบว่าค่าตรงกับเนื้อหาอาร์เรย์หรือไม่ A boolจะถูกตั้งค่าเป็นจริงในกรณีนี้

ไมโครคอนโทรลเลอร์คือ NXP LPC4357 แกน ARM Cortex M4 และคอมไพเลอร์คือ GCC ฉันได้รวมการเพิ่มประสิทธิภาพระดับ 2 ไว้แล้ว (3 ช้ากว่า) และวางฟังก์ชันใน RAM แทนแฟลช ฉันยังใช้เลขคณิตตัวชี้และการforวนซ้ำซึ่งทำการนับลงแทนการขึ้น (ตรวจสอบว่าi!=0เร็วกว่าการตรวจสอบว่าi<256) สรุปแล้วฉันจบลงด้วยระยะเวลา 12.5 ซึ่งจะต้องลดลงอย่างมากเพื่อให้เป็นไปได้ นี่คือรหัส (หลอก) ที่ฉันใช้ตอนนี้:

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: คุณจะประหลาดใจว่าคอมไพเลอร์ได้รับการปรับปรุงมากแค่ไหนในช่วงสามทศวรรษที่ผ่านมา ARM เป็นพิเศษค่อนข้างเป็นมิตรกับคอมไพเลอร์ และฉันรู้ว่า ARM บน GCC สามารถออกคำสั่งโหลดหลายคำสั่งได้ (ตั้งแต่ปี 2009 เป็นอย่างน้อย)
MSalters

8
คำถามที่ยอดเยี่ยมผู้คนลืมไปว่ามีกรณีในโลกแห่งความเป็นจริงที่ประสิทธิภาพมีความสำคัญ หลายครั้งเกินไปคำถามเช่นนี้ได้รับคำตอบด้วย "just use stl"
Kik

14
ชื่อ "... วนซ้ำผ่านอาร์เรย์" ทำให้เข้าใจผิดเนื่องจากคุณเพียงแค่ค้นหาค่าที่กำหนด ในการวนซ้ำอาร์เรย์หมายความว่าต้องทำบางอย่างในแต่ละรายการ การเรียงลำดับหากสามารถตัดจำหน่ายต้นทุนได้ในการค้นหาจำนวนมากเป็นวิธีการที่มีประสิทธิภาพโดยไม่ขึ้นอยู่กับปัญหาการใช้ภาษา
hardmath

8
คุณแน่ใจหรือไม่ว่าคุณไม่สามารถใช้การค้นหาแบบไบนารีหรือตารางแฮชได้? การค้นหาแบบไบนารีสำหรับ 256 รายการ == 8 การเปรียบเทียบ ตารางแฮช == 1 กระโดดโดยเฉลี่ย (หรือกระโดดสูงสุด 1 ครั้งหากคุณมีแฮชที่สมบูรณ์แบบ) คุณควรใช้การเพิ่มประสิทธิภาพแอสเซมบลีหลังจากที่คุณ 1) มีอัลกอริทึมการค้นหาที่เหมาะสม ( O(1)หรือO(logN)เมื่อเทียบกับO(N)) และ 2) คุณได้ทำโปรไฟล์ให้เป็นคอขวด
กรู

คำตอบ:


105

ในสถานการณ์ที่ประสิทธิภาพมีความสำคัญสูงสุดคอมไพเลอร์ C มักจะไม่สร้างโค้ดที่เร็วที่สุดเมื่อเทียบกับสิ่งที่คุณสามารถทำได้ด้วยภาษาแอสเซมบลีที่ปรับแต่งด้วยมือ ฉันมักจะใช้เส้นทางของการต่อต้านน้อยที่สุด - สำหรับกิจวัตรเล็ก ๆ เช่นนี้ฉันแค่เขียนโค้ด asm และมีความคิดที่ดีว่าจะต้องใช้กี่รอบในการดำเนินการ คุณอาจสามารถใช้รหัส C และรับคอมไพเลอร์เพื่อสร้างผลลัพธ์ที่ดีได้ แต่คุณอาจเสียเวลาในการปรับแต่งผลลัพธ์ด้วยวิธีนี้ คอมไพเลอร์ (โดยเฉพาะจาก Microsoft) มาไกลในช่วงไม่กี่ปีที่ผ่านมา แต่คอมไพเลอร์ยังไม่ฉลาดเท่าคอมไพเลอร์ระหว่างหูของคุณเพราะคุณกำลังทำงานกับสถานการณ์เฉพาะของคุณไม่ใช่แค่กรณีทั่วไป คอมไพเลอร์ไม่สามารถใช้ประโยชน์จากคำสั่งบางอย่าง (เช่น LDM) ที่สามารถเร่งความเร็วได้และมัน ไม่น่าจะฉลาดพอที่จะคลายการวนซ้ำ นี่คือวิธีการทำซึ่งรวม 3 แนวคิดที่ฉันพูดถึงในความคิดเห็นของฉัน: Loop unrolling, cache prefetch และใช้ประโยชน์จากคำสั่ง multiple load (ldm) จำนวนรอบคำสั่งจะอยู่ที่ประมาณ 3 นาฬิกาต่อองค์ประกอบอาร์เรย์ แต่สิ่งนี้ไม่ได้คำนึงถึงความล่าช้าของหน่วยความจำ

ทฤษฎีการทำงาน:การออกแบบ CPU ของ ARM ดำเนินการคำสั่งส่วนใหญ่ในหนึ่งรอบนาฬิกา แต่คำสั่งจะดำเนินการในท่อ คอมไพเลอร์ C จะพยายามกำจัดความล่าช้าของท่อโดยการแทรกคำสั่งอื่น ๆ ไว้ระหว่างนั้น เมื่อนำเสนอด้วยการวนซ้ำที่แน่นหนาเช่นรหัส C ดั้งเดิมคอมไพเลอร์จะมีช่วงเวลาที่ยากลำบากในการซ่อนความล่าช้าเนื่องจากค่าที่อ่านจากหน่วยความจำจะต้องถูกเปรียบเทียบทันที รหัสของฉันด้านล่างสลับระหว่าง 2 ชุดจาก 4 รีจิสเตอร์เพื่อลดความล่าช้าของหน่วยความจำเองและไปป์ไลน์ที่ดึงข้อมูล โดยทั่วไปเมื่อทำงานกับชุดข้อมูลขนาดใหญ่และรหัสของคุณไม่ได้ใช้ประโยชน์จากการลงทะเบียนส่วนใหญ่หรือทั้งหมดที่มีอยู่คุณจะไม่ได้รับประสิทธิภาพสูงสุด

; 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: ฉันให้โอกาส Visual Studio 2013 SP2 ของ Microsoft ในการทำโค้ดให้ดีขึ้น มันสามารถใช้คำแนะนำ 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 แต่ฉันจะทดสอบประสิทธิภาพของ nVidia Tegra 3 และ Tegra 4 จาก 3 เวอร์ชันที่แตกต่างกันและโพสต์ผลลัพธ์ที่นี่เร็ว ๆ นี้

อัปเดต 3: ฉันรันโค้ดและรหัส ARM ที่คอมไพล์ของ Microsoft บน Tegra 3 และ Tegra 4 (Surface RT, Surface RT 2) ฉันวิ่งวนซ้ำ 1000000 ครั้งซึ่งหาคู่ไม่ได้เพื่อให้ทุกอย่างอยู่ในแคชและวัดได้ง่าย

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

ในทั้งสองกรณีรหัสของฉันทำงานเร็วขึ้นเกือบสองเท่า ซีพียู ARM สมัยใหม่ส่วนใหญ่อาจให้ผลลัพธ์ที่คล้ายกัน


13
@ LưuVĩnhPhúc - โดยทั่วไปแล้วจะเป็นจริง แต่ ISR ที่เข้มงวดเป็นหนึ่งในข้อยกเว้นที่ใหญ่ที่สุดซึ่งคุณมักจะรู้มากกว่าที่คอมไพเลอร์ทำ
sapi

47
ผู้สนับสนุนของปีศาจ: มีหลักฐานเชิงปริมาณว่ารหัสนี้เร็วกว่าหรือไม่?
Oliver Charlesworth

11
@ BitBank: นั่นยังไม่ดีพอ คุณจะต้องกลับขึ้นเรียกร้องของคุณด้วยหลักฐาน
Lightness Races ใน Orbit

13
ฉันได้เรียนรู้บทเรียนเมื่อหลายปีก่อน ฉันสร้างวงในที่ได้รับการปรับให้เหมาะสมอย่างน่าทึ่งสำหรับรูทีนกราฟิกบน Pentium โดยใช้ท่อ U และ V อย่างเหมาะสมที่สุด ลดลงเหลือ 6 รอบนาฬิกาต่อลูป (คำนวณและวัดได้) และฉันก็ภูมิใจในตัวเองมาก เมื่อฉันทดสอบกับสิ่งเดียวกันที่เขียนด้วย C แล้ว C ก็เร็วขึ้น ฉันไม่เคยเขียนบรรทัดอื่นของ Intel แอสเซมเบลอร์อีกเลย
Rocketmagnet

14
"ผู้สงสัยในความคิดเห็นที่คิดว่าประสบการณ์ของฉันเป็นเรื่องเล็กน้อย / ไร้ค่าและต้องการการพิสูจน์" อย่าแสดงความคิดเห็นในเชิงลบมากเกินไป การแสดงหลักฐานทำให้คำตอบที่ดีของคุณดีขึ้นมาก
โคดี้เกรย์

87

มีเคล็ดลับในการปรับให้เหมาะสม (ฉันถูกถามในการสัมภาษณ์งานครั้งหนึ่ง):

  • หากรายการสุดท้ายในอาร์เรย์มีค่าที่คุณต้องการให้ส่งคืนจริง
  • เขียนค่าที่คุณต้องการลงในรายการสุดท้ายในอาร์เรย์
  • ทำซ้ำอาร์เรย์จนกว่าคุณจะพบค่าที่คุณกำลังมองหา
  • หากคุณพบก่อนรายการสุดท้ายในอาร์เรย์ให้คืนค่าจริง
  • ส่งคืนเท็จ

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;
}

สิ่งนี้ให้ผลหนึ่งสาขาต่อการวนซ้ำแทนที่จะเป็นสองสาขาต่อการวนซ้ำ


UPDATE:

หากคุณได้รับอนุญาตให้จัดสรรอาร์เรย์ให้SIZE+1คุณสามารถกำจัดส่วน "last entry swapping":

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

3
Array อยู่ใน RAM แต่ไม่อนุญาตให้เขียน
wlamers

1
ดี แต่อาร์เรย์ไม่มีอีกต่อไปconstซึ่งทำให้ไม่ปลอดภัยต่อเธรด ดูเหมือนราคาสูงที่ต้องจ่าย
EOF

2
@EOF: constเคยพูดถึงคำถามที่ไหน?
barak manos

4
@barakmanos: ถ้าฉันส่งอาร์เรย์และค่าให้คุณและถามคุณว่าค่าอยู่ในอาร์เรย์หรือไม่โดยปกติฉันไม่คิดว่าคุณจะแก้ไขอาร์เรย์ คำถามเดิมไม่ได้กล่าวถึงconstหรือเธรด แต่ฉันคิดว่ามันยุติธรรมที่จะพูดถึงข้อแม้นี้
EOF

62

คุณกำลังขอความช่วยเหลือในการปรับแต่งอัลกอริทึมของคุณซึ่งอาจผลักดันให้คุณเข้าสู่แอสเซมเบลอร์ แต่อัลกอริทึมของคุณ (การค้นหาเชิงเส้น) ไม่ฉลาดนักดังนั้นคุณควรพิจารณาเปลี่ยนอัลกอริทึมของคุณ เช่น:

ฟังก์ชันแฮชที่สมบูรณ์แบบ

หากค่า 256 "ถูกต้อง" ของคุณเป็นแบบคงที่และเป็นที่ทราบกันในขณะคอมไพล์คุณสามารถใช้ฟังก์ชันแฮชที่สมบูรณ์แบบได้ คุณต้องหาฟังก์ชันแฮชที่จับคู่ค่าอินพุตของคุณกับค่าในช่วง 0 .. nโดยที่ไม่มีการชนกันสำหรับค่าที่ถูกต้องทั้งหมดที่คุณสนใจ นั่นคือไม่มีสองค่าที่ "ถูกต้อง" แฮชกับค่าเอาต์พุตเดียวกัน เมื่อค้นหาฟังก์ชันแฮชที่ดีคุณตั้งเป้าที่จะ:

  • ให้ฟังก์ชันแฮชเร็วพอสมควร
  • ย่อn . สิ่งที่เล็กที่สุดที่คุณจะได้คือ 256 (ฟังก์ชันแฮชที่สมบูรณ์แบบน้อยที่สุด) แต่อาจทำได้ยากขึ้นอยู่กับข้อมูล

หมายเหตุสำหรับฟังก์ชันแฮชที่มีประสิทธิภาพnมักจะมีกำลัง 2 ซึ่งเทียบเท่ากับมาสก์ระดับบิตของบิตต่ำ (การทำงานและการดำเนินการ) ตัวอย่างฟังก์ชันแฮช:

  • CRC ของไบต์อินพุต modulo n .
  • ((x << i) ^ (x >> j) ^ (x << k) ^ ...) % n(ยกเป็นจำนวนมากi, j, k... ตามต้องการด้วยการเปลี่ยนแปลงทางซ้ายหรือขวา)

จากนั้นคุณสร้างตารางnรายการคงที่โดยที่แฮชจะจับคู่ค่าอินพุตกับดัชนีiลงในตาราง สำหรับค่าที่ถูกต้องรายการตารางiมีค่าที่ถูกต้อง สำหรับรายการตารางอื่น ๆ ทั้งหมดให้แน่ใจว่าการเข้ามาของดัชนีแต่ละฉันมีบางส่วนที่ไม่ถูกต้องค่าอื่น ๆ ที่ไม่ได้กัญชาเพื่อฉัน

จากนั้นในขั้นตอนการขัดจังหวะของคุณด้วยอินพุตx :

  1. แฮชxเป็นดัชนีi (ซึ่งอยู่ในช่วง 0..n)
  2. ค้นหารายการiในตารางและดูว่ามีค่าxหรือไม่

สิ่งนี้จะเร็วกว่าการค้นหาเชิงเส้นที่มีค่า 256 หรือ 1024 มาก

ฉันได้เขียนโค้ด Pythonเพื่อค้นหาฟังก์ชันแฮชที่สมเหตุสมผล

การค้นหาแบบไบนารี

หากคุณจัดเรียงอาร์เรย์ของค่าที่ "ถูกต้อง" 256 ค่าคุณสามารถทำการค้นหาแบบไบนารีแทนการค้นหาเชิงเส้น นั่นหมายความว่าคุณควรจะค้นหาตาราง 256 รายการได้ใน 8 ขั้นตอนเท่านั้น ( log2(256)) หรือตาราง 1024 รายการใน 10 ขั้นตอน อีกครั้งสิ่งนี้จะเร็วกว่าการค้นหาเชิงเส้นที่มีค่า 256 หรือ 1024 มาก


ขอบคุณสำหรับสิ่งนั้น ตัวเลือกการค้นหาไบนารีคือตัวเลือกที่ฉันเลือก ดูความคิดเห็นก่อนหน้านี้ในโพสต์แรก เคล็ดลับนี้ทำได้ดีมากโดยไม่ต้องใช้แอสเซมบลี
wlamers

11
อันที่จริงก่อนที่จะพยายามเพิ่มประสิทธิภาพโค้ดของคุณ (เช่นการใช้แอสเซมบลีหรือกลเม็ดอื่น ๆ ) คุณควรจะดูว่าคุณสามารถลดความซับซ้อนของอัลกอริทึมได้หรือไม่ โดยปกติแล้วการลดความซับซ้อนของอัลกอริทึมจะได้ผลดีกว่าการพยายามที่จะตัดวงจรสองสามรอบ แต่รักษาความซับซ้อนของอัลกอริทึมเดิมไว้
ysdx

3
+1 สำหรับการค้นหาแบบไบนารี การออกแบบอัลกอริทึมใหม่เป็นวิธีที่ดีที่สุดในการเพิ่มประสิทธิภาพ
Rocketmagnet

แนวความคิดที่ได้รับความนิยมคือการค้นหารูทีนแฮชที่มีประสิทธิภาพมากเกินไปดังนั้น "แนวทางปฏิบัติที่ดีที่สุด" จึงเป็นการค้นหาแบบไบนารี แม้ว่าบางครั้ง "แนวทางปฏิบัติที่ดีที่สุด" จะไม่ดีพอ สมมติว่าคุณกำหนดเส้นทางการรับส่งข้อมูลบนเครือข่ายได้ทันทีในขณะที่ส่วนหัวของแพ็กเก็ตมาถึง (แต่ไม่ใช่ส่วนหัว): การใช้การค้นหาแบบไบนารีจะทำให้ผลิตภัณฑ์ของคุณช้าลงอย่างสิ้นหวัง ผลิตภัณฑ์ฝังตัวมักจะมีข้อ จำกัด และข้อกำหนดดังกล่าวว่า "แนวทางปฏิบัติที่ดีที่สุด" คืออะไรตัวอย่างเช่นสภาพแวดล้อมการดำเนินการ x86 คือ "การหาทางออกที่ง่าย" ในการฝัง
Olof Forshell

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 คุณจะเพิ่มการเปรียบเทียบได้มากที่สุดหนึ่งรายการและมากที่สุดก็คือตัวประกอบของพื้นที่เก็บข้อมูลสองตัว

** หากคุณไม่คุ้นเคยกับการคิดในแง่ของความน่าจะเป็นจุดตัดสินใจทุกจุดจะมีเอนโทรปีซึ่งเป็นข้อมูลเฉลี่ยที่คุณเรียนรู้จากการดำเนินการ สำหรับการ>=ทดสอบความน่าจะเป็นของแต่ละสาขาจะอยู่ที่ 0.5 และ -log2 (0.5) เท่ากับ 1 นั่นหมายความว่าถ้าคุณเรียนสาขาหนึ่งคุณจะเรียน 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บิต ประมาณหนึ่งในร้อยบิต การทดสอบนั้นไม่ได้แบกรับน้ำหนัก!


4
ฉันชอบโซลูชันนี้มาก สามารถแก้ไขให้ทำงานเป็นจำนวนรอบคงที่เพื่อหลีกเลี่ยงการพิสูจน์ตามเวลาหากตำแหน่งของค่านั้นเป็นข้อมูลที่ละเอียดอ่อน
OregonTrail

1
@OregonTrail: นิติตามเวลา? ปัญหาสนุก ๆ แต่ความคิดเห็นที่น่าเศร้า
Mike Dunlavey

16
คุณจะเห็นลูปคลี่เช่นนี้ในห้องสมุดการเข้ารหัสลับเพื่อป้องกันการโจมตีระยะเวลาen.wikipedia.org/wiki/Timing_attack นี่คือตัวอย่างที่ดีgithub.com/jedisct1/libsodium/blob/…ในกรณีนี้เรากำลังป้องกันไม่ให้ผู้โจมตีคาดเดาความยาวของสตริง โดยปกติผู้โจมตีจะใช้ตัวอย่างการเรียกใช้ฟังก์ชันหลายล้านตัวอย่างเพื่อทำการโจมตีตามเวลา
OregonTrail

3
+1 สุดยอด! การค้นหาที่ไม่มีการควบคุมเล็กน้อยดี ฉันไม่เคยเห็นมาก่อน ฉันอาจจะใช้มัน
Rocketmagnet

1
@OregonTrail: ฉันสองความคิดเห็นตามเวลาของคุณ ฉันมีมากกว่าหนึ่งครั้งที่ต้องเขียนรหัสการเข้ารหัสที่ดำเนินการในจำนวนรอบที่กำหนดเพื่อหลีกเลี่ยงการรั่วไหลของข้อมูลไปยังการโจมตีตามเวลา
TonyK

16

หากทราบชุดค่าคงที่ในตารางล่วงหน้าคุณสามารถใช้การแฮชที่สมบูรณ์แบบเพื่อให้แน่ใจว่ามีการเข้าถึงตารางเพียงครั้งเดียว การแฮชที่สมบูรณ์แบบจะกำหนดฟังก์ชันแฮชที่จับคู่คีย์ที่น่าสนใจทั้งหมดกับสล็อตที่ไม่ซ้ำกัน (ตารางนั้นไม่หนาแน่นเสมอไป แต่คุณสามารถตัดสินใจได้ว่าตารางที่ไม่หนาแน่นเพียงใดที่คุณสามารถจ่ายได้โดยที่ตารางที่มีความหนาแน่นน้อยมักจะนำไปสู่ฟังก์ชันการแฮชที่ง่ายกว่า)

โดยปกติแล้วฟังก์ชันแฮชที่สมบูรณ์แบบสำหรับชุดคีย์เฉพาะนั้นค่อนข้างง่ายในการคำนวณ คุณไม่ต้องการให้มันยาวและซับซ้อนเพราะการแข่งขันกันเวลาอาจจะดีกว่าในการทำโพรบหลายตัว

การแฮชที่สมบูรณ์แบบคือโครงร่าง "1-probe max" เราสามารถสรุปแนวคิดได้โดยคิดว่าเราควรแลกเปลี่ยนความเรียบง่ายในการคำนวณรหัสแฮชด้วยเวลาที่ใช้ในการสร้างโพรบ k ท้ายที่สุดเป้าหมายคือ "เวลาทั้งหมดในการค้นหาน้อยที่สุด" ไม่ใช่โพรบน้อยที่สุดหรือฟังก์ชันแฮชที่ง่ายที่สุด อย่างไรก็ตามฉันไม่เคยเห็นใครสร้างอัลกอริธึมการแฮช k-probes-max ฉันสงสัยว่าสามารถทำได้ แต่น่าจะเป็นการวิจัย

ความคิดอีกอย่างหนึ่ง: หากโปรเซสเซอร์ของคุณเร็วมากโพรบหนึ่งไปยังหน่วยความจำจากแฮชที่สมบูรณ์แบบอาจครอบงำเวลาในการดำเนินการ หากโปรเซสเซอร์ไม่เร็วมากโพรบ k> 1 อาจใช้งานได้จริง


1
Cortex-M ไม่มีที่ไหนใกล้เร็วมาก
MSalters

2
ในความเป็นจริงในกรณีนี้เขาไม่ต้องการตารางแฮชเลย เขาแค่อยากรู้ว่าคีย์บางคีย์อยู่ในชุดหรือไม่เขาไม่ต้องการแมปกับค่า ดังนั้นก็เพียงพอแล้วหากฟังก์ชันแฮชที่สมบูรณ์จะจับคู่ค่า 32 บิตแต่ละค่าเป็น 0 หรือ 1 โดยที่ "1" สามารถกำหนดเป็น "อยู่ในชุด" ได้
David Ongaro

1
จุดดีถ้าเขาสามารถสร้างแฮชเครื่องกำเนิดไฟฟ้าที่สมบูรณ์แบบเพื่อสร้างแผนที่ดังกล่าวได้ แต่นั่นจะเป็น "ชุดที่หนาแน่นมาก"; ฉันดูแล้วเขาสามารถหาเครื่องกำเนิดแฮชที่สมบูรณ์แบบที่ทำเช่นนั้นได้ เขาอาจจะดีกว่าถ้าพยายามหาแฮชที่สมบูรณ์แบบที่สร้างค่าคงที่ K ถ้าอยู่ในเซตและค่าใดก็ได้ แต่ K ถ้าไม่อยู่ในเซต ฉันสงสัยว่ามันยากที่จะได้แฮชที่สมบูรณ์แบบแม้ในช่วงหลัง ๆ
Ira Baxter

@DavidOngaro table[PerfectHash(value)] == valueจะให้ 1 ถ้าค่าอยู่ในเซตและ 0 ถ้าไม่ใช่และมีวิธีที่รู้จักกันดีในการสร้างฟังก์ชัน PerfectHash (ดูเช่นburtleburtle.net/bob/hash/perfect.html ) การพยายามค้นหาฟังก์ชันแฮชที่แมปค่าทั้งหมดในเซตให้เป็น 1 โดยตรงและค่าทั้งหมดที่ไม่อยู่ในเซตเป็น 0 นั้นเป็นงานที่โง่เขลา
Jim Balter

@DavidOngaro: ฟังก์ชันแฮชที่สมบูรณ์แบบมี "ผลบวกเท็จ" มากมายกล่าวคือค่าที่ไม่อยู่ในชุดจะมีแฮชเหมือนกับค่าในชุด ดังนั้นคุณต้องมีตารางที่จัดทำดัชนีด้วยค่าแฮชซึ่งมีค่าอินพุต "ในชุด" ดังนั้นเพื่อตรวจสอบความถูกต้องของค่าอินพุตใด ๆ คุณ (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

1
เอ่อการค้นหาสามารถเรียกใช้ส่วนท้ายของอาร์เรย์ และการแฮชเชิงเส้นประเภทนี้มีอัตราการชนกันสูง - ไม่มีทางที่คุณจะได้รับ O (1) ชุดแฮชที่ดีจะไม่ถูกนำมาใช้เช่นนี้
Jim Balter

@JimBalter True รหัสไม่สมบูรณ์ เหมือนความคิดทั่วไป อาจชี้ไปที่รหัสชุดแฮชที่มีอยู่ แต่การพิจารณาว่านี่เป็นขั้นตอนการให้บริการขัดจังหวะอาจเป็นประโยชน์ในการแสดงให้เห็นว่าการค้นหาไม่ใช่รหัสที่ซับซ้อนมากนัก
jpa

คุณควรแก้ไขให้มันล้อมรอบตัวฉัน
Jim Balter

จุดประสงค์ของฟังก์ชันแฮชที่สมบูรณ์แบบคือทำโพรบเดียว ระยะเวลา
Ira Baxter

10

ในกรณีนี้ก็อาจจะคุ้มค่าการตรวจสอบฟิลเตอร์บลูม พวกเขาสามารถระบุได้อย่างรวดเร็วว่าไม่มีค่าอยู่ซึ่งเป็นสิ่งที่ดีเนื่องจากค่าที่เป็นไปได้ 2 ^ 32 ส่วนใหญ่ไม่ได้อยู่ในอาร์เรย์องค์ประกอบ 1024 นั้น อย่างไรก็ตามมีผลบวกเท็จบางอย่างที่ต้องตรวจสอบเพิ่มเติม

เนื่องจากตารางของคุณเป็นแบบคงที่คุณสามารถระบุได้ว่ามีผลบวกปลอมใดบ้างสำหรับตัวกรอง Bloom ของคุณและใส่ไว้ในแฮชที่สมบูรณ์แบบ


1
น่าสนใจฉันไม่เคยเห็นตัวกรอง Bloom มาก่อน
Rocketmagnet

8

สมมติว่าโปรเซสเซอร์ของคุณทำงานที่ 204 MHz ซึ่งน่าจะเป็นค่าสูงสุดสำหรับ LPC4357 และสมมติว่าผลการจับเวลาของคุณสะท้อนถึงกรณีเฉลี่ย (ครึ่งหนึ่งของอาร์เรย์ที่เคลื่อนที่ผ่าน) เราจะได้รับ:

  • ความถี่ CPU: 204 MHz
  • รอบระยะเวลา: 4.9 ns
  • ระยะเวลาในรอบ: 12.5 µs / 4.9 ns = 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 มีโหมดแอดเดรสที่จัดทำดัชนีดังนั้นจึงไม่มีจุดหมาย สำหรับการทำให้ตัวชี้constนั้น GCC ชี้แล้วว่ามันไม่เปลี่ยนแปลง ไม่ได้constเพิ่มอะไรเลย
MSalters

11
@MSalters ตกลงฉันไม่ได้ตรวจสอบด้วยรหัสที่สร้างขึ้นประเด็นคือการแสดงสิ่งที่ทำให้ง่ายขึ้นที่ระดับ C และฉันคิดว่าแค่จัดการพอยน์เตอร์แทนตัวชี้และดัชนีก็ง่ายกว่า ฉันไม่เห็นด้วยที่ " constไม่เพิ่มอะไรเลย" มันบอกผู้อ่านอย่างชัดเจนว่าค่าจะไม่เปลี่ยนแปลง นั่นคือข้อมูลที่ยอดเยี่ยม
ผ่อนคลาย

9
นี่คือรหัสที่ฝังลึก การเพิ่มประสิทธิภาพจนถึงขณะนี้รวมถึงการย้ายรหัสจากแฟลชไปยัง RAM แต่ก็ยังต้องเร็วขึ้น ณ จุดนี้ความสามารถในการอ่านไม่ใช่เป้าหมาย
MSalters

1
@MSalters "ARM มีโหมดแอดเดรสที่จัดทำดัชนีดังนั้นจึงไม่มีจุดหมาย" - ถ้าคุณพลาดประเด็นไปโดยสิ้นเชิง ... OP เขียนว่า "ฉันยังใช้เลขคณิตของตัวชี้และสำหรับการวนซ้ำ" การคลายตัวไม่ได้แทนที่การสร้างดัชนีด้วยพอยน์เตอร์เขาเพียงแค่กำจัดตัวแปรดัชนีและลบพิเศษในการวนซ้ำทุกครั้ง แต่ OP นั้นฉลาด (ไม่เหมือนกับหลาย ๆ คนที่ตอบและแสดงความคิดเห็น) และลงเอยด้วยการค้นหาไบนารี
Jim Balter

6

คนอื่น ๆ แนะนำให้จัดระเบียบตารางของคุณใหม่เพิ่มค่า Sentinel ในตอนท้ายหรือจัดเรียงเพื่อให้มีการค้นหาแบบไบนารี

คุณระบุว่า "ฉันยังใช้เลขคณิตของตัวชี้และสำหรับลูปซึ่งนับลงแทนการขึ้น (ตรวจสอบว่าi != 0เร็วกว่าการตรวจสอบว่าi < 256 ) หรือไม่

คำแนะนำแรกของฉันคือ: กำจัดตัวชี้เลขคณิตและการนับถอยหลัง สิ่งที่ชอบ

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

มีแนวโน้มที่จะเป็นสำนวนสำหรับคอมไพเลอร์ ลูปเป็นสำนวนและการจัดทำดัชนีของอาร์เรย์บนตัวแปรลูปเป็นสำนวน เล่นกลกับเลขคณิตชี้และตัวชี้จะมีแนวโน้มที่จะทำให้งงงวยสำนวนที่จะคอมไพเลอร์และทำให้มันสร้างรหัสที่เกี่ยวข้องกับสิ่งที่คุณเขียนมากกว่าสิ่งที่นักเขียนคอมไพเลอร์ตัดสินใจที่จะเป็นหลักสูตรที่ดีที่สุดสำหรับทั่วไปงาน

ยกตัวอย่างเช่นโค้ดข้างต้นอาจจะเรียบเรียงเป็นห่วงวิ่งออกมาจาก-256หรือไปอยู่ที่ศูนย์การจัดทำดัชนีปิด-255 &the_array[256]อาจเป็นสิ่งที่ไม่สามารถแสดงออกได้ใน C ที่ถูกต้อง แต่ตรงกับสถาปัตยกรรมของเครื่องที่คุณกำลังสร้าง

ดังนั้นอย่าเพิ่มขนาดเล็ก คุณเพียงแค่โยนประแจลงในผลงานของเครื่องมือเพิ่มประสิทธิภาพของคุณ หากคุณต้องการฉลาดให้ทำงานกับโครงสร้างข้อมูลและอัลกอริทึม แต่อย่าปรับขนาดการแสดงออกของมัน มันจะกลับมากัดคุณถ้าไม่ได้อยู่ในคอมไพเลอร์ / สถาปัตยกรรมปัจจุบันในครั้งต่อไป

โดยเฉพาะอย่างยิ่งการใช้ตัวชี้เลขคณิตแทนอาร์เรย์และดัชนีเป็นพิษสำหรับคอมไพเลอร์ที่ตระหนักถึงการจัดตำแหน่งสถานที่จัดเก็บข้อควรพิจารณาในการกำหนดนามแฝงและสิ่งอื่น ๆ และสำหรับการเพิ่มประสิทธิภาพเช่นการลดความแรงด้วยวิธีที่เหมาะสมที่สุดกับสถาปัตยกรรมของเครื่อง


ลูปเหนือพอยน์เตอร์เป็นสำนวนใน C และคอมไพเลอร์การปรับให้เหมาะสมที่ดีสามารถจัดการได้เช่นเดียวกับการจัดทำดัชนี แต่ทั้งหมดนี้เป็นที่สงสัยเพราะ OP จบลงด้วยการค้นหาไบนารี
Jim Balter

3

Vectorization สามารถใช้ที่นี่ได้เนื่องจากมักจะใช้ memchr คุณใช้อัลกอริทึมต่อไปนี้:

  1. สร้างมาสก์ของการค้นหาซ้ำโดยมีความยาวเท่ากับจำนวนบิตของระบบปฏิบัติการของคุณ (64 บิต 32 บิต ฯลฯ ) ในระบบ 64 บิตคุณต้องทำแบบสอบถาม 32 บิตซ้ำสองครั้ง

  2. ประมวลผลรายการเป็นรายการข้อมูลหลาย ๆ ชิ้นพร้อมกันเพียงแค่แคสต์รายการไปยังรายการประเภทข้อมูลที่ใหญ่ขึ้นแล้วดึงค่าออกมา สำหรับแต่ละชิ้นให้ XOR กับมาสก์จากนั้น XOR ด้วย 0b0111 ... 1 จากนั้นเพิ่ม 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" ซึ่งฉันจะยืนตามคำตอบของฉันเพราะมันตอบได้อย่างแม่นยำ ฉันสามารถโต้แย้งได้ว่านี่มีฟังก์ชันแฮชที่มีประสิทธิภาพความเร็วสูงสุด (เนื่องจากค่า address ===) ฉันได้อ่านความคิดเห็นและตระหนักถึงข้อควรระวังที่ชัดเจน ไม่ต้องสงสัยเลยว่าคำเตือนเหล่านี้ จำกัด ขอบเขตของปัญหาที่สามารถใช้ในการแก้ไขได้ แต่สำหรับปัญหาเหล่านั้นที่แก้ได้จะแก้ได้อย่างมีประสิทธิภาพ

แทนที่จะปฏิเสธคำตอบนี้ทันทีให้ถือว่าเป็นจุดเริ่มต้นที่ดีที่สุดที่คุณสามารถพัฒนาได้โดยใช้ฟังก์ชันแฮชเพื่อให้เกิดความสมดุลที่ดีขึ้นระหว่างความเร็วและประสิทธิภาพ


8
วิธีนี้ได้รับ 4 upvotes? คำถามระบุว่าเป็น Cortex M4 สิ่งนี้มี RAM 136 KB ไม่ใช่ 262.144 KB
MSalters

1
เป็นที่น่าประหลาดใจว่ามีการโหวตโหวตให้คำตอบที่ผิดอย่างเห็นได้ชัดกี่ครั้งเนื่องจากผู้ตอบพลาดป่าเพื่อหาต้นไม้ สำหรับกรณีที่ใหญ่ที่สุดของ OP O (log n) << O (n)
msw

3
ฉันรู้สึกไม่พอใจมากกับโปรแกรมเมอร์ที่เผาผลาญหน่วยความจำจำนวนมากอย่างไร้สาระเมื่อมีวิธีแก้ปัญหาที่ดีกว่านี้ ทุกๆ 5 ปีดูเหมือนว่าพีซีของฉันจะมีหน่วยความจำเหลืออยู่ซึ่งเมื่อ 5 ปีที่แล้วจำนวนนั้นมีมากมาย
Craig McQueen

1
@CraigMcQueen Kids วันนี้ สิ้นเปลืองหน่วยความจำ อุกอาจ! ย้อนกลับไปในสมัยของฉันเรามีหน่วยความจำ 1 MiB และขนาดคำ 16 บิต / s
Cole Johnson

2
มีอะไรกับนักวิจารณ์ที่รุนแรง? OP ระบุอย่างชัดเจนว่าความเร็วมีความสำคัญอย่างยิ่งสำหรับโค้ดส่วนนี้และ StephenQuan ได้กล่าวถึง "จำนวนหน่วยความจำที่ไร้สาระ" แล้ว
Bogdan Alexandru

1

ตรวจสอบให้แน่ใจว่าคำแนะนำ ("รหัสหลอก") และข้อมูล ("theArray") อยู่ในหน่วยความจำ (RAM) แยกกันเพื่อให้สถาปัตยกรรม CM4 Harvard ถูกนำไปใช้อย่างเต็มประสิทธิภาพ จากคู่มือผู้ใช้:

ใส่คำอธิบายภาพที่นี่

เพื่อเพิ่มประสิทธิภาพของ CPU ARM Cortex-M4 มีสามบัสสำหรับการเข้าถึง Instruction (รหัส) (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 ที่ทันสมัยเกือบทั้งหมดใช้ RISC เช่น ARM แม้แต่ซีพียู Intel X86 ที่ทันสมัยก็ใช้แกน RISC ภายในเท่าที่ฉันรู้ (รวบรวมจาก X86 ได้ทันที) การเพิ่มประสิทธิภาพที่สำคัญสำหรับ RISC คือการเพิ่มประสิทธิภาพไปป์ไลน์ (และสำหรับ Intel และ CPU อื่น ๆ ด้วย) ลดการกระโดดของโค้ด ประเภทหนึ่งของการเพิ่มประสิทธิภาพดังกล่าว (อาจเป็นประเภทหลัก) คือ "การย้อนกลับรอบ" หนึ่ง มันโง่และมีประสิทธิภาพอย่างไม่น่าเชื่อแม้แต่คอมไพเลอร์ของ Intel ก็สามารถทำ 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 ในอาร์เรย์) ดังนั้นจึงเร็วที่สุดเท่าที่จะเป็นไปได้ (แน่นอนว่าไม่นับการเพิ่มประสิทธิภาพอัลกอริทึมเช่นตารางแฮชอาร์เรย์ที่เรียงลำดับและอื่น ๆ กล่าวไว้ในคำตอบอื่น ๆ ซึ่งอาจให้ผลลัพธ์ที่ดีกว่าขึ้นอยู่กับขนาดอาร์เรย์วิธีการย้อนกลับของ Cycles สามารถใช้ได้เช่นกันฉันกำลังเขียนที่นี่เกี่ยวกับสิ่งที่ฉันคิดว่าฉันไม่เห็นในที่อื่น)

ส่วนที่สองของการเพิ่มประสิทธิภาพนี้คือรายการอาร์เรย์นั้นถูกยึดโดยที่อยู่โดยตรง (คำนวณในขั้นตอนการคอมไพล์ตรวจสอบให้แน่ใจว่าคุณใช้อาร์เรย์แบบคงที่) และไม่จำเป็นต้องใช้ ADD op เพิ่มเติมเพื่อคำนวณตัวชี้จากที่อยู่ฐานของอาร์เรย์ การเพิ่มประสิทธิภาพนี้อาจไม่มีผลอย่างมีนัยสำคัญเนื่องจากสถาปัตยกรรม AFAIK ARM มีคุณสมบัติพิเศษในการเพิ่มความเร็วอาร์เรย์ที่อยู่ แต่อย่างไรก็ตามมันจะดีกว่าเสมอที่จะรู้ว่าคุณทำได้ดีที่สุดในโค้ด C โดยตรงใช่ไหม?

Cycle Rollback อาจดูอึดอัดเนื่องจากเปลือง ROM (ใช่คุณวางมันลงในส่วนที่รวดเร็วของ RAM ถ้าบอร์ดของคุณรองรับคุณสมบัตินี้) แต่จริงๆแล้วมันเป็นการจ่ายที่ยุติธรรมสำหรับความเร็วโดยอิงตามแนวคิด RISC นี่เป็นเพียงจุดทั่วไปของการเพิ่มประสิทธิภาพการคำนวณ - คุณเสียสละพื้นที่เพื่อความเร็วและในทางกลับกันขึ้นอยู่กับความต้องการของคุณ

หากคุณคิดว่าการย้อนกลับสำหรับอาร์เรย์ 1024 องค์ประกอบเป็นการเสียสละที่มากเกินไปสำหรับกรณีของคุณคุณสามารถพิจารณา "การย้อนกลับบางส่วน" เช่นแบ่งอาร์เรย์ออกเป็น 2 ส่วนจาก 512 รายการแต่ละรายการหรือ 4x256 เป็นต้น

3) CPU ที่ทันสมัยมักจะรองรับการทำงานของ SIMD เช่นชุดคำสั่ง ARM NEON ซึ่งอนุญาตให้ดำเนินการปฏิบัติการเดียวกันแบบขนาน พูดตรงไปตรงมาฉันจำไม่ได้ว่ามันเหมาะสำหรับการเปรียบเทียบ แต่ฉันคิดว่ามันอาจจะเป็นคุณควรตรวจสอบสิ่งนั้น Googling แสดงให้เห็นว่าอาจมีเทคนิคบางอย่างเช่นกันเพื่อให้ได้ความเร็วสูงสุดโปรดดูhttps://stackoverflow.com/a/5734019/1028256

ฉันหวังว่ามันจะช่วยให้คุณมีความคิดใหม่ ๆ


OP ข้ามคำตอบที่โง่เขลาทั้งหมดที่มุ่งเน้นไปที่การปรับลูปเชิงเส้นให้เหมาะสมและจัดเรียงอาร์เรย์และทำการค้นหาแบบไบนารีแทน
Jim Balter

@ จิมเห็นได้ชัดว่าควรทำการเพิ่มประสิทธิภาพแบบนั้นก่อน คำตอบที่ 'โง่เขลา' อาจดูไม่โง่เขลานักในบางกรณีการใช้งานเช่นคุณไม่มีเวลาจัดเรียงอาร์เรย์ หรือถ้าความเร็วที่คุณได้รับมันก็ไม่เพียงพออยู่ดี
Mixaz

"เห็นได้ชัดว่าควรทำการเพิ่มประสิทธิภาพแบบนั้นก่อน" ซึ่งเห็นได้ชัดว่าไม่ใช่สำหรับผู้ที่ใช้ความพยายามอย่างมากในการพัฒนาโซลูชันเชิงเส้น "คุณไม่มีเวลาจัดเรียงอาร์เรย์" - ฉันไม่รู้ว่ามันหมายถึงอะไร "หรือถ้าความเร็วที่คุณได้รับก็ยังไม่เพียงพออยู่ดี" - เอ่อถ้าความเร็วจากการค้นหาแบบไบนารี "ไม่เพียงพอ" การค้นหาเชิงเส้นที่ปรับให้เหมาะสมจะไม่ช่วยปรับปรุง ตอนนี้ฉันจบเรื่องนี้แล้ว
Jim Balter

@ JimBalter ถ้าฉันมีปัญหาเช่น OP ฉันจะพิจารณาใช้ algs เช่นการค้นหาไบนารีหรืออะไรบางอย่าง คิดไม่ถึงว่า OP ไม่พิจารณาอยู่แล้ว "คุณไม่มีเวลาจัดเรียงอาร์เรย์" หมายความว่าการจัดเรียงอาร์เรย์ต้องใช้เวลา หากคุณจำเป็นต้องทำสำหรับชุดข้อมูลอินพุตแต่ละชุดอาจใช้เวลานานกว่าการวนซ้ำเชิงเส้น "หรือถ้าความเร็วที่คุณได้รับก็ยังไม่เพียงพออยู่ดี" หมายความว่าต่อไปนี้ - คำแนะนำการเพิ่มประสิทธิภาพด้านบนสามารถใช้เพื่อเร่งความเร็วรหัสค้นหาไบนารีหรืออะไรก็ได้
Mixaz

0

ฉันเป็นแฟนตัวยงของการแฮช แน่นอนว่าปัญหาคือการค้นหาอัลกอริทึมที่มีประสิทธิภาพซึ่งทั้งรวดเร็วและใช้หน่วยความจำขั้นต่ำ (โดยเฉพาะในโปรเซสเซอร์แบบฝังตัว)

หากคุณทราบล่วงหน้าถึงค่าที่อาจเกิดขึ้นคุณสามารถสร้างโปรแกรมที่ทำงานผ่านอัลกอริทึมจำนวนมากเพื่อค้นหาสิ่งที่ดีที่สุดหรือเป็นพารามิเตอร์ที่ดีที่สุดสำหรับข้อมูลของคุณ

ฉันสร้างโปรแกรมที่คุณสามารถอ่านได้ในโพสต์นี้และได้ผลลัพธ์ที่รวดเร็วมาก 16000 รายการแปลโดยประมาณเป็น 2 ^ 14 หรือเฉลี่ย 14 การเปรียบเทียบเพื่อค้นหาค่าโดยใช้การค้นหาแบบไบนารี ฉันตั้งเป้าไว้อย่างชัดเจนสำหรับการค้นหาที่รวดเร็วมากโดยเฉลี่ยแล้วการค้นหาค่าใน <= 1.5 การค้นหาซึ่งส่งผลให้ต้องการ RAM มากขึ้น ฉันเชื่อว่าด้วยค่าเฉลี่ยที่อนุรักษ์นิยมมากขึ้น (พูด <= 3) สามารถบันทึกหน่วยความจำได้มาก โดยการเปรียบเทียบกรณีเฉลี่ยสำหรับการค้นหาแบบไบนารีในรายการ 256 หรือ 1024 รายการของคุณจะส่งผลให้มีการเปรียบเทียบจำนวนเฉลี่ย 8 และ 10 ตามลำดับ

การค้นหาโดยเฉลี่ยของฉันต้องใช้ประมาณ 60 รอบ (บนแล็ปท็อปที่มี Intel i5) ด้วยอัลกอริทึมทั่วไป (ใช้การหารหนึ่งโดยตัวแปร) และ 40-45 รอบด้วยความเชี่ยวชาญ (อาจใช้การคูณ) สิ่งนี้ควรแปลเป็นเวลาการค้นหาย่อยในระดับไมโครวินาทีใน MCU ของคุณขึ้นอยู่กับความถี่สัญญาณนาฬิกาที่ดำเนินการที่

สามารถปรับแต่งได้ในชีวิตจริงเพิ่มเติมหากอาร์เรย์รายการติดตามจำนวนครั้งที่มีการเข้าถึงรายการ หากอาร์เรย์รายการเรียงลำดับจากมากที่สุดไปหาน้อยที่สุดก่อนที่จะคำนวณดัชนีจะพบค่าที่เกิดขึ้นบ่อยที่สุดด้วยการเปรียบเทียบเพียงครั้งเดียว


0

นี่เป็นเหมือนภาคผนวกมากกว่าคำตอบ

ฉันเคยมีกรณีคล้าย ๆ กันในอดีต แต่อาร์เรย์ของฉันคงที่ในการค้นหาจำนวนมาก

ครึ่งหนึ่งของพวกเขาค่าที่ค้นหาไม่มีอยู่ในอาร์เรย์ จากนั้นฉันก็รู้ว่าฉันสามารถใช้ "ตัวกรอง" ก่อนทำการค้นหาใด ๆ

"ตัวกรอง" นี้เป็นเพียงตัวเลขจำนวนเต็มที่คำนวณครั้งเดียวและใช้ในการค้นหาแต่ละครั้ง

อยู่ใน 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.