อะไรคือการหารจำนวนเต็มที่เร็วที่สุดที่รองรับการหารด้วยศูนย์ไม่ว่าผลลัพธ์จะเป็นอย่างไร


109

สรุป:

ฉันกำลังมองหาวิธีที่เร็วที่สุดในการคำนวณ

(int) x / (int) y

y==0โดยไม่ได้รับข้อยกเว้นสำหรับ แต่ฉันแค่ต้องการผลลัพธ์ตามอำเภอใจ


พื้นหลัง:

เมื่อทำการเข้ารหัสอัลกอริทึมการประมวลผลภาพฉันมักจะต้องหารด้วยค่าอัลฟา (สะสม) ตัวแปรที่ง่ายที่สุดคือรหัส C ธรรมดาที่มีเลขคณิตจำนวนเต็ม ปัญหาของฉันคือฉันมักจะได้รับข้อผิดพลาดการหารด้วยศูนย์สำหรับพิกเซลผลลัพธ์ด้วยalpha==0. อย่างไรก็ตามเรื่องนี้จะตรงพิกเซลที่ผลที่ไม่ได้เรื่องที่ทั้งหมด: alpha==0ฉันไม่สนใจเกี่ยวกับค่าสีของพิกเซลพร้อม


รายละเอียด:

ฉันกำลังมองหาสิ่งที่ต้องการ:

result = (y==0)? 0 : x/y;

หรือ

result = x / MAX( y, 1 );

x และ y เป็นจำนวนเต็มบวก รหัสถูกเรียกใช้งานเป็นจำนวนมากในลูปที่ซ้อนกันดังนั้นฉันจึงกำลังมองหาวิธีกำจัดการแตกกิ่งก้านตามเงื่อนไข

เมื่อ y ไม่เกินช่วงไบต์ฉันพอใจกับวิธีแก้ปัญหา

unsigned char kill_zero_table[256] = { 1, 1, 2, 3, 4, 5, 6, 7, [...] 255 };
[...]
result = x / kill_zero_table[y];

แต่เห็นได้ชัดว่าสิ่งนี้ไม่ได้ผลสำหรับช่วงที่ใหญ่กว่า

ฉันเดาว่าคำถามสุดท้ายคืออะไรคือการแฮ็คบิตที่เร็วที่สุดที่เปลี่ยน 0 เป็นค่าจำนวนเต็มอื่น ๆ ในขณะที่ค่าอื่น ๆ ทั้งหมดไม่เปลี่ยนแปลง?


คำชี้แจง

ฉันไม่แน่ใจ 100% ว่าการแยกกิ่งนั้นแพงเกินไป อย่างไรก็ตามมีการใช้คอมไพเลอร์ที่แตกต่างกันดังนั้นฉันจึงชอบการเปรียบเทียบที่มีการปรับให้เหมาะสมเพียงเล็กน้อย (ซึ่งเป็นเรื่องที่น่าสงสัยอย่างยิ่ง)

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

โค้ดควรเข้ากันได้กับ C อย่างสมบูรณ์แพลตฟอร์มหลักคือ Linux 64 Bit พร้อม gcc & clang และ MacOS


22
คุณพิจารณาได้อย่างไรว่า if-branch นั้นแพงเกินไป?
djechlin

7
คุณมีวิธีการที่กำหนดว่ามีเป็นสาขา?
leemes

13
+1 สำหรับการจัดทำโปรไฟล์ด้วยการทำนายสาขาสมัยใหม่คุณอาจไม่จำเป็นต้องใช้สิ่งนี้ นอกจากนี้ทำไมคุณถึงเข้ารหัสอัลกอริทึมการประมวลผลภาพของคุณเอง
TC1

8
"แฮ็คบิตที่เร็วที่สุดคืออะไร ... " อาจจะy += !y? ไม่จำเป็นต้องมีสาขาในการคำนวณ คุณสามารถเปรียบเทียบx / (y + !y)กับและอาจจะยังx / max(y, 1) y ? (x/y) : 0ฉันเดาว่าจะไม่มีสาขาในทั้งสองอย่างอย่างน้อยก็เปิดการเพิ่มประสิทธิภาพไว้
leemes

6
ใครก็ตามที่คิดว่าการทำนายสาขาในยุคปัจจุบันหมายความว่าคุณไม่จำเป็นต้องทำสิ่งนี้ยังไม่มีข้อมูลรหัสการกำจัดสาขาที่ทำงานในระดับพิกเซลต่อพิกเซลมากพอ การทำนายสาขาสมัยใหม่เป็นที่ยอมรับได้หาก0ส่วนอัลฟ่ามีขนาดใหญ่และติดกัน มีสถานที่สำหรับเล่นซอรอบกับการเพิ่มประสิทธิภาพไมโครเป็นและต่อพิกเซลการดำเนินงานเป็นว่าสถานที่ที่
Yakk - Adam Nevraumont

คำตอบ:


107

แรงบันดาลใจจากความคิดเห็นบางส่วนฉันได้กำจัดสาขาใน Pentium และgccคอมไพเลอร์โดยใช้ไฟล์

int f (int x, int y)
{
        y += y == 0;
        return x/y;
}

โดยทั่วไปคอมไพเลอร์จะรับรู้ว่าสามารถใช้แฟล็กเงื่อนไขของการทดสอบในการเพิ่มได้

ตามคำขอการชุมนุม:

.globl f
    .type   f, @function
f:
    pushl   %ebp
    xorl    %eax, %eax
    movl    %esp, %ebp
    movl    12(%ebp), %edx
    testl   %edx, %edx
    sete    %al
    addl    %edx, %eax
    movl    8(%ebp), %edx
    movl    %eax, %ecx
    popl    %ebp
    movl    %edx, %eax
    sarl    $31, %edx
    idivl   %ecx
    ret

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

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

เวอร์ชันของ Philipp ที่มีคอมไพเลอร์ ARM:

f PROC
        CMP      r1,#0
        BNE      __aeabi_idivmod
        MOVEQ    r0,#0
        BX       lr

เวอร์ชันของ Philipp กับ GCC:

f:
        subs    r3, r1, #0
        str     lr, [sp, #-4]!
        moveq   r0, r3
        ldreq   pc, [sp], #4
        bl      __divsi3
        ldr     pc, [sp], #4

รหัสของฉันกับคอมไพเลอร์ ARM:

f PROC
        RSBS     r2,r1,#1
        MOVCC    r2,#0
        ADD      r1,r1,r2
        B        __aeabi_idivmod

รหัสของฉันกับ GCC:

f:
        str     lr, [sp, #-4]!
        cmp     r1, #0
        addeq   r1, r1, #1
        bl      __divsi3
        ldr     pc, [sp], #4

ทุกเวอร์ชันยังคงต้องมีสาขาไปยังรูทีนการแบ่งเนื่องจาก ARM เวอร์ชันนี้ไม่มีฮาร์ดแวร์สำหรับแผนก แต่การทดสอบy == 0จะดำเนินการอย่างสมบูรณ์ผ่านการดำเนินการที่กำหนดไว้ล่วงหน้า


คุณช่วยแสดงรหัสแอสเซมเบลอร์ที่เป็นผลลัพธ์ให้เราได้ไหม หรือไม่ทราบได้อย่างไรว่าไม่มีสาขา?
Haatschii

1
น่ากลัว สามารถทำconstexprและหลีกเลี่ยงการปลดเปลื้องประเภทไม่มีความจำเป็นเช่นนี้template<typename T, typename U> constexpr auto fdiv( T t, U u ) -> decltype(t/(u+!u)) { return t/(u+!u); } และถ้าคุณต้องการ255,(lhs)/(rhs+!rhs) & -!rhs
Yakk - อดัม Nevraumont

1
@leemes แต่ฉันไม่หมายถึงไม่ได้| &อ๊ะ - ( (lhs)/(rhs+!rhs) ) | -!rhsควรตั้งค่าของคุณไป0xFFFFFFFถ้าrhsเป็น0และถ้าlhs/rhs rhs!=0
Yakk - Adam Nevraumont

1
นี่เป็นเรื่องที่ฉลาดมาก
Theodoros Chatzigiannakis

1
ตอบโจทย์มาก! ฉันมักจะใช้การประกอบสำหรับสิ่งเหล่านี้ แต่มันก็น่ากลัวเสมอที่จะดูแลรักษา (ไม่ต้องพูดถึงพกพาน้อยลง;))
ลีโอ

20

นี่คือตัวเลขที่เป็นรูปธรรมบางส่วนบน Windows ที่ใช้ GCC 4.7.2:

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

int main()
{
  unsigned int result = 0;
  for (int n = -500000000; n != 500000000; n++)
  {
    int d = -1;
    for (int i = 0; i != ITERATIONS; i++)
      d &= rand();

#if CHECK == 0
    if (d == 0) result++;
#elif CHECK == 1
    result += n / d;
#elif CHECK == 2
    result += n / (d + !d);
#elif CHECK == 3
    result += d == 0 ? 0 : n / d;
#elif CHECK == 4
    result += d == 0 ? 1 : n / d;
#elif CHECK == 5
    if (d != 0) result += n / d;
#endif
  }
  printf("%u\n", result);
}

โปรดทราบว่าฉันไม่ได้ตั้งใจโทรsrand()เพื่อให้rand()ผลลัพธ์เหมือนกันทุกประการ โปรดทราบด้วยว่า-DCHECK=0นับแค่ศูนย์เท่านั้นเพื่อให้เห็นได้ชัดว่าปรากฏบ่อยเพียงใด

ตอนนี้รวบรวมและกำหนดเวลาได้หลายวิธี:

$ for it in 0 1 2 3 4 5; do for ch in 0 1 2 3 4 5; do gcc test.cc -o test -O -DITERATIONS=$it -DCHECK=$ch && { time=`time ./test`; echo "Iterations $it, check $ch: exit status $?, output $time"; }; done; done

แสดงผลลัพธ์ที่สามารถสรุปได้ในตาราง:

Iterations  | 0        | 1        | 2        | 3         | 4         | 5
-------------+-------------------------------------------------------------------
Zeroes       | 0        | 1        | 133173   | 1593376   | 135245875 | 373728555
Check 1      | 0m0.612s | -        | -        | -         | -         | -
Check 2      | 0m0.612s | 0m6.527s | 0m9.718s | 0m13.464s | 0m18.422s | 0m22.871s
Check 3      | 0m0.616s | 0m5.601s | 0m8.954s | 0m13.211s | 0m19.579s | 0m25.389s
Check 4      | 0m0.611s | 0m5.570s | 0m9.030s | 0m13.544s | 0m19.393s | 0m25.081s
Check 5      | 0m0.612s | 0m5.627s | 0m9.322s | 0m14.218s | 0m19.576s | 0m25.443s

หากศูนย์หายาก-DCHECK=2เวอร์ชันนั้นจะทำงานได้ไม่ดี เมื่อศูนย์เริ่มปรากฏมากขึ้น-DCHECK=2เคสจะเริ่มทำงานได้ดีขึ้นอย่างมาก จากตัวเลือกอื่น ๆ ไม่มีความแตกต่างกันมากนัก

สำหรับ-O3แม้ว่ามันจะเป็นคนละเรื่อง:

Iterations  | 0        | 1        | 2        | 3         | 4         | 5
-------------+-------------------------------------------------------------------
Zeroes       | 0        | 1        | 133173   | 1593376   | 135245875 | 373728555
Check 1      | 0m0.646s | -        | -        | -         | -         | -
Check 2      | 0m0.654s | 0m5.670s | 0m9.905s | 0m14.238s | 0m17.520s | 0m22.101s
Check 3      | 0m0.647s | 0m5.611s | 0m9.085s | 0m13.626s | 0m18.679s | 0m25.513s
Check 4      | 0m0.649s | 0m5.381s | 0m9.117s | 0m13.692s | 0m18.878s | 0m25.354s
Check 5      | 0m0.649s | 0m6.178s | 0m9.032s | 0m13.783s | 0m18.593s | 0m25.377s

ที่นั่นการตรวจสอบ 2 ไม่มีข้อเสียเปรียบเมื่อเทียบกับการตรวจสอบอื่น ๆ และจะทำให้ผลประโยชน์เป็นศูนย์กลายเป็นเรื่องธรรมดามากขึ้น

คุณควรวัดผลเพื่อดูว่าเกิดอะไรขึ้นกับคอมไพเลอร์และข้อมูลตัวอย่างตัวแทนของคุณ


4
ทำให้ 50% ของรายการเป็นd=0แบบสุ่มแทนที่จะทำให้เกือบตลอดเวลาd!=0และคุณจะเห็นความล้มเหลวในการทำนายสาขาเพิ่มเติม การทำนายสาขาเป็นสิ่งที่ดีหากมีการปฏิบัติตามสาขาหนึ่งเกือบตลอดเวลาหรือหากสาขาต่อไปนี้หรืออีกสาขาหนึ่ง
คลุ้มคลั่ง

@Yakk การdวนซ้ำเป็นวงในดังนั้นd == 0เคสจึงถูกกระจายอย่างเท่าเทียมกัน และทำให้ 50% ของคดีd == 0เป็นจริงหรือไม่?

2
ทำให้0.002%คดีd==0เป็นจริงหรือไม่? มีการแจกจ่ายไปทั่วทุก ๆ 65000 การทำซ้ำที่คุณพบในd==0กรณีของคุณ ในขณะที่50%อาจจะไม่เกิดขึ้นบ่อย10%หรือ1%สามารถจะเกิดขึ้นหรือแม้กระทั่งหรือ90% 99%การทดสอบตามที่ปรากฏจะเป็นการทดสอบจริงๆเท่านั้น "ถ้าโดยพื้นฐานแล้วคุณไม่เคยลงไปที่สาขาการทำนายสาขาจะทำให้การลบสาขานั้นไร้จุดหมายหรือไม่" ซึ่งคำตอบคือ "ใช่ แต่ไม่น่าสนใจ"
Yakk - Adam Nevraumont

1
ไม่เพราะความแตกต่างจะมองไม่เห็นได้อย่างมีประสิทธิภาพเนื่องจากเสียงรบกวน
โจ

3
การแจกแจงของศูนย์ไม่เกี่ยวข้องกับการแจกแจงที่พบในสถานการณ์ของผู้ถาม รูปภาพที่มีอัลฟา 0 ผสมกันและอื่น ๆ มีรูหรือรูปร่างผิดปกติ แต่ (โดยปกติ) จะไม่มีจุดรบกวน การสมมติว่าคุณไม่รู้อะไรเกี่ยวกับข้อมูล (และคิดว่าเป็นเสียงรบกวน) เป็นความผิดพลาด นี่คือแอปพลิเคชั่นในโลกแห่งความจริงที่มีภาพจริงซึ่งอาจมี 0 อัลฟา และเนื่องจากแถวของพิกเซลมีแนวโน้มที่จะมี a = 0 ทั้งหมดหรือ a> 0 ทั้งหมดการใช้ประโยชน์จากการทำนายสาขาอาจเร็วที่สุดโดยเฉพาะอย่างยิ่งเมื่อ a = 0 เกิดขึ้นมากและ (ช้า) ดิวิชั่น (15+ รอบ !) หลีกเลี่ยง
ท.บ.

13

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

(ถือว่าตัวหารอยู่ในecxและเงินปันผลเข้าeax)

mov ebx, ecx
neg ebx
sbb ebx, ebx
add ecx, ebx
div eax, ecx

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


กองอยู่ที่ไหน?
Yakk - Adam Nevraumont

1
สิ่งนี้ไม่ได้ทำการหาร แต่เพียงแค่ทำให้ตัวหารก่อให้เกิดมลพิษดังนั้นการหารด้วยศูนย์จึงเป็นไปไม่ได้
Tyler Durden

@Jens Timmerman ขออภัยฉันเขียนก่อนที่จะเพิ่มคำสั่ง div ฉันได้อัปเดตข้อความแล้ว
Tyler Durden

1

ตามลิงค์นี้คุณสามารถบล็อกสัญญาณ SIGFPE ด้วยsigaction() (ฉันไม่ได้ลองด้วยตัวเอง แต่ฉันเชื่อว่ามันน่าจะใช้ได้)

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

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

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.