ตำแหน่งของบิตที่มีนัยสำคัญน้อยที่สุดที่ตั้งค่าไว้


121

ฉันกำลังมองหาวิธีที่มีประสิทธิภาพในการกำหนดตำแหน่งของบิตที่มีนัยสำคัญน้อยที่สุดที่กำหนดเป็นจำนวนเต็มเช่นสำหรับ 0x0FF0 ซึ่งจะเป็น 4

การใช้งานที่ไม่สำคัญคือ:

unsigned GetLowestBitPos(unsigned value)
{
   assert(value != 0); // handled separately

   unsigned pos = 0;
   while (!(value & 1))
   {
      value >>= 1;
      ++pos;
   }
   return pos;
}

มีความคิดอย่างไรที่จะบีบบางรอบออกจากมัน?

(หมายเหตุ: คำถามนี้มีไว้สำหรับคนที่ชอบสิ่งนั้นไม่ใช่สำหรับคนที่บอกว่า xyzoptimization นั้นชั่วร้าย)

[แก้ไข] ขอบคุณทุกคนสำหรับแนวคิด! ฉันได้เรียนรู้สิ่งอื่น ๆ ด้วยเช่นกัน เย็น!


ในขณะที่ ((ค่า _N >> (++ pos))! = 0);
Thomas

คำตอบ:


170

Bit Twiddling Hacksนำเสนอคอลเลกชันที่ยอดเยี่ยมของแฮ็กที่น่าเบื่อเล็กน้อยพร้อมการอภิปรายเกี่ยวกับประสิทธิภาพ / การเพิ่มประสิทธิภาพ ทางออกที่ฉันชอบสำหรับปัญหาของคุณ (จากไซต์นั้น) คือ«คูณและค้นหา»:

unsigned int v;  // find the number of trailing zeros in 32-bit v 
int r;           // result goes here
static const int MultiplyDeBruijnBitPosition[32] = 
{
  0, 1, 28, 2, 29, 14, 24, 3, 30, 22, 20, 15, 25, 17, 4, 8, 
  31, 27, 13, 23, 21, 19, 16, 7, 26, 12, 18, 6, 11, 5, 10, 9
};
r = MultiplyDeBruijnBitPosition[((uint32_t)((v & -v) * 0x077CB531U)) >> 27];

ข้อมูลอ้างอิงที่เป็นประโยชน์:


18
ทำไมต้องโหวตลง? นี่อาจเป็นการใช้งานที่เร็วที่สุดขึ้นอยู่กับความเร็วของการคูณ แน่นอนว่าโค้ดมีขนาดกะทัดรัดและเคล็ดลับ (v & -v) เป็นสิ่งที่ทุกคนควรเรียนรู้และจดจำ
Adam Davis

2
+1 เจ๋งมากการดำเนินการทวีคูณราคาแพงแค่ไหนเมื่อเทียบกับการดำเนินการ if (X&Y)
Brian R.Bondy

4
มีใครรู้บ้างว่าประสิทธิภาพของสิ่งนี้เมื่อเทียบกับ__builtin_ffslหรือffsl?
Steven Lu

2
@ Jim Balter แต่โมดูโลนั้นช้ามากเมื่อเทียบกับการคูณบนฮาร์ดแวร์สมัยใหม่ ดังนั้นฉันจะไม่เรียกว่าทางออกที่ดีกว่า
Apriori

2
สำหรับฉันแล้วดูเหมือนว่าทั้งค่า 0x01 และ 0x00 ให้ผลลัพธ์เป็นค่า 0 จากอาร์เรย์ เห็นได้ชัดว่าเคล็ดลับนี้จะระบุว่ามีการตั้งค่าบิตต่ำสุดหากส่งผ่าน 0!
abelenky

80

ทำไมไม่ใช้ffsในตัว ? (ฉันคว้า man page จาก Linux แต่มีให้ใช้งานอย่างกว้างขวางกว่านั้น)

ffs (3) - หน้าคน Linux

ชื่อ

ffs - ค้นหาบิตแรกที่กำหนดในคำ

สรุป

#include <strings.h>
int ffs(int i);
#define _GNU_SOURCE
#include <string.h>
int ffsl(long int i);
int ffsll(long long int i);

ลักษณะ

ฟังก์ชัน ffs () จะส่งคืนตำแหน่งของบิตแรก (ที่มีนัยสำคัญน้อยที่สุด) ที่ตั้งค่าไว้ในคำว่า i บิตที่มีนัยสำคัญน้อยที่สุดคือตำแหน่งที่ 1 และตำแหน่งที่สำคัญที่สุดเช่น 32 หรือ 64 ฟังก์ชัน ffsll () และ ffsl () ทำเหมือนกัน แต่รับอาร์กิวเมนต์ที่มีขนาดต่างกัน

ส่งคืนค่า

ฟังก์ชันเหล่านี้จะส่งคืนตำแหน่งของชุดบิตแรกหรือ 0 หากไม่มีการตั้งค่าบิตใน i

สอดคล้องกับ

4.3BSD, POSIX.1-2001

หมายเหตุ

ระบบ BSD มีต้นแบบใน<string.h>.


6
FYI สิ่งนี้ถูกคอมไพล์ตามคำสั่งประกอบที่เกี่ยวข้องเมื่อมี
Jérémie

46

มีคำสั่งประกอบ x86 ( bsf) ที่จะทำ :)

ปรับให้เหมาะสมยิ่งขึ้น?!

หมายเหตุด้านข้าง:

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


20
@dwc: ฉันเข้าใจ แต่ฉันคิดว่าประโยคนี้: "มีความคิดอย่างไรที่จะบีบวงจรบางส่วนออกไป" ทำให้คำตอบดังกล่าวเป็นที่ยอมรับอย่างสมบูรณ์แบบ!
Mehrdad Afshari

5
+1 คำตอบของเขาจำเป็นต้องขึ้นอยู่กับสถาปัตยกรรมของเขาเนื่องจากความอดทนดังนั้นการทิ้งคำแนะนำในการประกอบจึงเป็นคำตอบที่ถูกต้อง
Chris Lutz

3
+1 คำตอบที่ชาญฉลาดใช่มันไม่ใช่ C หรือ C ++ แต่เป็นเครื่องมือที่เหมาะสมสำหรับงาน
Andrew Hare

1
เดี๋ยวก่อนไม่เป็นไร ค่าจริงของจำนวนเต็มไม่สำคัญที่นี่ ขอโทษ
Chris Lutz

2
@Bastian: พวกเขาตั้งค่า ZF = 1 ถ้าตัวถูกดำเนินการเป็นศูนย์
Mehrdad Afshari

43

สถาปัตยกรรมสมัยใหม่ส่วนใหญ่จะมีคำสั่งสำหรับการค้นหาตำแหน่งของบิตเซตต่ำสุดหรือบิตเซตสูงสุดหรือการนับจำนวนศูนย์นำหน้าเป็นต้น

หากคุณมีคำสั่งใดคำสั่งหนึ่งของคลาสนี้คุณสามารถเลียนแบบคนอื่นได้ในราคาถูก

ใช้เวลาสักครู่ในการทำงานบนกระดาษและตระหนักว่าx & (x-1)จะล้างบิตชุดที่ต่ำที่สุดใน x และ( x & ~(x-1) )จะคืนค่าเพียงบิตที่ตั้งไว้ต่ำสุดโดยไม่คำนึงถึงสถาปัตยกรรมความยาวของคำ ฯลฯ เมื่อรู้สิ่งนี้การใช้การนับฮาร์ดแวร์จึงเป็นเรื่องเล็กน้อย -zeroes / maximum-set-bit เพื่อค้นหาบิตที่ตั้งไว้ต่ำสุดหากไม่มีคำสั่งที่ชัดเจนให้ทำ

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


18

Weee มีโซลูชันมากมายและไม่ใช่เกณฑ์มาตรฐานในสายตา คุณคนควรละอายใจตัวเอง ;-)

เครื่องของฉันเป็น Intel i530 (2.9 GHz) ใช้ Windows 7 64 บิต ฉันรวบรวมด้วย MinGW เวอร์ชัน 32 บิต

$ gcc --version
gcc.exe (GCC) 4.7.2

$ gcc bench.c -o bench.exe -std=c99 -Wall -O2
$ bench
Naive loop.         Time = 2.91  (Original questioner)
De Bruijn multiply. Time = 1.16  (Tykhyy)
Lookup table.       Time = 0.36  (Andrew Grant)
FFS instruction.    Time = 0.90  (ephemient)
Branch free mask.   Time = 3.48  (Dan / Jim Balter)
Double hack.        Time = 3.41  (DocMax)

$ gcc bench.c -o bench.exe -std=c99 -Wall -O2 -march=native
$ bench
Naive loop.         Time = 2.92
De Bruijn multiply. Time = 0.47
Lookup table.       Time = 0.35
FFS instruction.    Time = 0.68
Branch free mask.   Time = 3.49
Double hack.        Time = 0.92

รหัสของฉัน:

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


#define ARRAY_SIZE 65536
#define NUM_ITERS 5000  // Number of times to process array


int find_first_bits_naive_loop(unsigned nums[ARRAY_SIZE])
{
    int total = 0; // Prevent compiler from optimizing out the code
    for (int j = 0; j < NUM_ITERS; j++) {
        for (int i = 0; i < ARRAY_SIZE; i++) {
            unsigned value = nums[i];
            if (value == 0)
                continue;
            unsigned pos = 0;
            while (!(value & 1))
            {
                value >>= 1;
                ++pos;
            }
            total += pos + 1;
        }
    }

    return total;
}


int find_first_bits_de_bruijn(unsigned nums[ARRAY_SIZE])
{
    static const int MultiplyDeBruijnBitPosition[32] = 
    {
       1, 2, 29, 3, 30, 15, 25, 4, 31, 23, 21, 16, 26, 18, 5, 9, 
       32, 28, 14, 24, 22, 20, 17, 8, 27, 13, 19, 7, 12, 6, 11, 10
    };

    int total = 0; // Prevent compiler from optimizing out the code
    for (int j = 0; j < NUM_ITERS; j++) {
        for (int i = 0; i < ARRAY_SIZE; i++) {
            unsigned int c = nums[i];
            total += MultiplyDeBruijnBitPosition[((unsigned)((c & -c) * 0x077CB531U)) >> 27];
        }
    }

    return total;
}


unsigned char lowestBitTable[256];
int get_lowest_set_bit(unsigned num) {
    unsigned mask = 1;
    for (int cnt = 1; cnt <= 32; cnt++, mask <<= 1) {
        if (num & mask) {
            return cnt;
        }
    }

    return 0;
}
int find_first_bits_lookup_table(unsigned nums[ARRAY_SIZE])
{
    int total = 0; // Prevent compiler from optimizing out the code
    for (int j = 0; j < NUM_ITERS; j++) {
        for (int i = 0; i < ARRAY_SIZE; i++) {
            unsigned int value = nums[i];
            // note that order to check indices will depend whether you are on a big 
            // or little endian machine. This is for little-endian
            unsigned char *bytes = (unsigned char *)&value;
            if (bytes[0])
                total += lowestBitTable[bytes[0]];
            else if (bytes[1])
              total += lowestBitTable[bytes[1]] + 8;
            else if (bytes[2])
              total += lowestBitTable[bytes[2]] + 16;
            else
              total += lowestBitTable[bytes[3]] + 24;
        }
    }

    return total;
}


int find_first_bits_ffs_instruction(unsigned nums[ARRAY_SIZE])
{
    int total = 0; // Prevent compiler from optimizing out the code
    for (int j = 0; j < NUM_ITERS; j++) {
        for (int i = 0; i < ARRAY_SIZE; i++) {
            total +=  __builtin_ffs(nums[i]);
        }
    }

    return total;
}


int find_first_bits_branch_free_mask(unsigned nums[ARRAY_SIZE])
{
    int total = 0; // Prevent compiler from optimizing out the code
    for (int j = 0; j < NUM_ITERS; j++) {
        for (int i = 0; i < ARRAY_SIZE; i++) {
            unsigned value = nums[i];
            int i16 = !(value & 0xffff) << 4;
            value >>= i16;

            int i8 = !(value & 0xff) << 3;
            value >>= i8;

            int i4 = !(value & 0xf) << 2;
            value >>= i4;

            int i2 = !(value & 0x3) << 1;
            value >>= i2;

            int i1 = !(value & 0x1);

            int i0 = (value >> i1) & 1? 0 : -32;

            total += i16 + i8 + i4 + i2 + i1 + i0 + 1;
        }
    }

    return total;
}


int find_first_bits_double_hack(unsigned nums[ARRAY_SIZE])
{
    int total = 0; // Prevent compiler from optimizing out the code
    for (int j = 0; j < NUM_ITERS; j++) {
        for (int i = 0; i < ARRAY_SIZE; i++) {
            unsigned value = nums[i];
            double d = value ^ (value - !!value); 
            total += (((int*)&d)[1]>>20)-1022; 
        }
    }

    return total;
}


int main() {
    unsigned nums[ARRAY_SIZE];
    for (int i = 0; i < ARRAY_SIZE; i++) {
        nums[i] = rand() + (rand() << 15);
    }

    for (int i = 0; i < 256; i++) {
        lowestBitTable[i] = get_lowest_set_bit(i);
    }


    clock_t start_time, end_time;
    int result;

    start_time = clock();
    result = find_first_bits_naive_loop(nums);
    end_time = clock();
    printf("Naive loop.         Time = %.2f, result = %d\n", 
        (end_time - start_time) / (double)(CLOCKS_PER_SEC), result);

    start_time = clock();
    result = find_first_bits_de_bruijn(nums);
    end_time = clock();
    printf("De Bruijn multiply. Time = %.2f, result = %d\n", 
        (end_time - start_time) / (double)(CLOCKS_PER_SEC), result);

    start_time = clock();
    result = find_first_bits_lookup_table(nums);
    end_time = clock();
    printf("Lookup table.       Time = %.2f, result = %d\n", 
        (end_time - start_time) / (double)(CLOCKS_PER_SEC), result);

    start_time = clock();
    result = find_first_bits_ffs_instruction(nums);
    end_time = clock();
    printf("FFS instruction.    Time = %.2f, result = %d\n", 
        (end_time - start_time) / (double)(CLOCKS_PER_SEC), result);

    start_time = clock();
    result = find_first_bits_branch_free_mask(nums);
    end_time = clock();
    printf("Branch free mask.   Time = %.2f, result = %d\n", 
        (end_time - start_time) / (double)(CLOCKS_PER_SEC), result);

    start_time = clock();
    result = find_first_bits_double_hack(nums);
    end_time = clock();
    printf("Double hack.        Time = %.2f, result = %d\n", 
        (end_time - start_time) / (double)(CLOCKS_PER_SEC), result);
}

9
เกณฑ์มาตรฐานสำหรับทั้ง de Bruijn และการค้นหาอาจทำให้เข้าใจผิด - นั่งอยู่ในวงที่แน่นเช่นนั้นหลังจากการดำเนินการครั้งแรกตารางการค้นหาสำหรับแต่ละประเภทจะถูกตรึงไว้ในแคช L1 จนกว่าจะวนรอบสุดท้าย ซึ่งไม่น่าจะตรงกับการใช้งานจริง
MattW

1
สำหรับอินพุตที่มีค่าเป็นศูนย์ในไบต์ต่ำจะได้รับไบต์ที่สูงขึ้นโดยการจัดเก็บ / โหลดซ้ำแทนการขยับเนื่องจากตัวชี้ - คาสต์ (BTW ที่ไม่จำเป็นโดยสิ้นเชิงและทำให้มันขึ้นอยู่กับ endian ซึ่งแตกต่างจากการเปลี่ยนที่จะไม่ทำ) อย่างไรก็ตามไมโครเบนช์มาร์กไม่เพียง แต่ไม่สมจริงเนื่องจากแคชร้อนเท่านั้น แต่ยังมีตัวทำนายสาขาที่เตรียมไว้และทดสอบอินพุตที่คาดการณ์ได้ดีมากและทำให้ LUT ทำงานน้อยลง กรณีการใช้งานจริงจำนวนมากมีการกระจายผลลัพธ์ที่สม่ำเสมอกว่าไม่ใช่ปัจจัยที่นำเข้า
Peter Cordes

2
ห่วง FFS ของคุณจะชะลอตัวลง แต่น่าเสียดายโดยการพึ่งพาที่ผิดพลาดในการเรียนการสอน BSF ซึ่งคอมไพเลอร์เก่าของคุณดื้อไม่ได้หลีกเลี่ยง ( แต่ GCC ใหม่ควรจะเหมือนกันสำหรับ popcnt / lzcnt / tzcnt . BSFมีการพึ่งพาที่ผิดพลาดในการส่งออก (ตั้งแต่พฤติกรรมจริง เมื่ออินพุต = 0 คือการปล่อยให้เอาต์พุตไม่เปลี่ยนแปลง) gcc น่าเสียดายที่เปลี่ยนสิ่งนี้เป็นการพึ่งพาแบบวนซ้ำโดยไม่ล้างการลงทะเบียนระหว่างการวนซ้ำวนซ้ำดังนั้นลูปควรทำงานที่หนึ่งต่อ 5 รอบคอขวดบน BSF (3) + CMOV (2) เวลาแฝง
Peter Cordes

1
เกณฑ์มาตรฐานของคุณพบว่า LUT มีปริมาณงานเกือบสองเท่าของวิธี FFS ซึ่งตรงกับการคาดการณ์การวิเคราะห์แบบคงที่ของฉันเป็นอย่างดี :) โปรดทราบว่าคุณกำลังวัดปริมาณงานไม่ใช่เวลาในการตอบสนองเนื่องจากการอ้างอิงแบบอนุกรมเพียงอย่างเดียวในลูปของคุณกำลังรวมเป็นผลรวม หากไม่มีการพึ่งพาที่ผิดพลาดffs()ควรมีทรูพุตหนึ่งครั้งต่อนาฬิกา (3 uops, 1 สำหรับ BSF และ 2 สำหรับ CMOV และสามารถทำงานบนพอร์ตต่างๆได้) ด้วยโอเวอร์เฮดแบบลูปเดียวกันมันคือ 7 ALU uops ที่สามารถรัน (บน CPU ของคุณ) ที่ 3 ต่อนาฬิกา ค่าใช้จ่ายครอบงำ! ที่มา: agner.org/optimize
Peter Cordes

1
ใช่การดำเนินการนอกคำสั่งสามารถซ้อนทับการวนซ้ำหลาย ๆ ครั้งของลูปได้หากbsf ecx, [ebx+edx*4]ไม่ถือว่าecxเป็นอินพุตที่ต้องรอ (ECX เขียนครั้งสุดท้ายโดย CMOV ของ iteraton ก่อนหน้านี้) แต่ซีพียูทำงานในลักษณะนั้นเพื่อใช้พฤติกรรม "leave dest unmodified if source is zero" (ดังนั้นจึงไม่เป็นเท็จอย่างแท้จริงเหมือนสำหรับ TZCNT จำเป็นต้องมีการอ้างอิงข้อมูลเนื่องจากไม่มีการแยกสาขา + การดำเนินการเก็งกำไรบนสมมติฐาน ที่อินพุตไม่ใช่ศูนย์) เราสามารถเอาชนะมันได้โดยการเพิ่มxor ecx,ecxก่อนหน้าbsfเพื่อทำลายการพึ่งพา ECX
Peter Cordes

17

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

ตารางของคุณ (256 รายการ 8 บิต) ควรมีดัชนีของ LSB สำหรับแต่ละหมายเลขในช่วง 0-255 คุณตรวจสอบค่าของคุณแต่ละไบต์และหาไบต์ที่ไม่ใช่ศูนย์ต่ำสุดจากนั้นใช้ค่านี้เพื่อค้นหาดัชนีจริง

สิ่งนี้ต้องการหน่วยความจำ 256 ไบต์ แต่ถ้าความเร็วของฟังก์ชันนี้สำคัญมาก 256 ไบต์ก็คุ้มค่า

เช่น

byte lowestBitTable[256] = {
.... // left as an exercise for the reader to generate
};

unsigned GetLowestBitPos(unsigned value)
{
  // note that order to check indices will depend whether you are on a big 
  // or little endian machine. This is for little-endian
  byte* bytes = (byte*)value;
  if (bytes[0])
    return lowestBitTable[bytes[0]];
  else if (bytes[1])
      return lowestBitTable[bytes[1]] + 8;
  else if (bytes[2])
      return lowestBitTable[bytes[2]] + 16;
  else
      return lowestBitTable[bytes[3]] + 24;  
}

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

4
คุณไม่ต้องการ +8, +16, +24 อยู่ที่นั่นสักแห่งหรือ?
Mark Ransom

7
ตารางการค้นหาใด ๆ จะเพิ่มโอกาสในการพลาดแคชและอาจต้องเสียค่าใช้จ่ายในการเข้าถึงหน่วยความจำซึ่งอาจมีขนาดที่สูงกว่าการดำเนินการตามคำสั่งหลายคำสั่ง
Mehrdad Afshari

1
ฉันจะใช้บิตกะด้วยซ้ำ (เลื่อนทีละ 8 ครั้ง) สามารถทำได้ทั้งหมดโดยใช้การลงทะเบียนจากนั้น โดยใช้พอยน์เตอร์คุณจะต้องเข้าถึงหน่วยความจำ
Johannes Schaub - litb

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

13

OMG มีแค่นี้ spiraled

สิ่งที่ตัวอย่างเหล่านี้ส่วนใหญ่ขาดไปคือความเข้าใจเล็กน้อยเกี่ยวกับการทำงานของฮาร์ดแวร์ทั้งหมด

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

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

จำนวนรอบของ CPU ที่สูญเสียไปจะแตกต่างกันอย่างมากในแต่ละประเภทของโปรเซสเซอร์ถัดไป แต่คุณสามารถคาดหวังได้ระหว่าง 20 ถึง 150 รอบ CPU ที่หายไป

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

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

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

ขออภัยหากครอบคลุมเรื่องนี้แล้ว

คอมไพเลอร์ทุกตัวที่ฉันใช้ยกเว้น XCODE AFAIK มีส่วนประกอบภายในของคอมไพเลอร์สำหรับทั้งบิตสแกนไปข้างหน้าและบิตสแกนย้อนกลับ สิ่งเหล่านี้จะรวบรวมเป็นคำสั่งการประกอบเพียงชุดเดียวบนฮาร์ดแวร์ส่วนใหญ่ที่ไม่มี Cache Miss ไม่มี Branch Miss-Prediction และไม่มีโปรแกรมเมอร์คนอื่น ๆ ที่สร้างบล็อกที่สะดุด

สำหรับคอมไพเลอร์ของ Microsoft ให้ใช้ _BitScanForward & _BitScanReverse
สำหรับ GCC ให้ใช้ __builtin_ffs, __builtin_clz, __builtin_ctz

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

ขออภัยฉันลืมให้วิธีแก้ไขโดยสิ้นเชิง .. นี่คือรหัสที่ฉันใช้บน IPAD ซึ่งไม่มีคำสั่งระดับแอสเซมบลีสำหรับงาน:

unsigned BitScanLow_BranchFree(unsigned value)
{
    bool bwl = (value & 0x0000ffff) == 0;
    unsigned I1 = (bwl * 15);
    value = (value >> I1) & 0x0000ffff;

    bool bbl = (value & 0x00ff00ff) == 0;
    unsigned I2 = (bbl * 7);
    value = (value >> I2) & 0x00ff00ff;

    bool bnl = (value & 0x0f0f0f0f) == 0;
    unsigned I3 = (bnl * 3);
    value = (value >> I3) & 0x0f0f0f0f;

    bool bsl = (value & 0x33333333) == 0;
    unsigned I4 = (bsl * 1);
    value = (value >> I4) & 0x33333333;

    unsigned result = value + I1 + I2 + I3 + I4 - 1;

    return result;
}

สิ่งที่ต้องเข้าใจก็คือไม่ใช่การเปรียบเทียบที่มีราคาแพง แต่เป็นสาขาที่เกิดขึ้นหลังจากการเปรียบเทียบ การเปรียบเทียบในกรณีนี้บังคับให้มีค่าเป็น 0 หรือ 1 ด้วย .. == 0 และผลลัพธ์จะใช้เพื่อรวมการคำนวณที่จะเกิดขึ้นที่ด้านใดด้านหนึ่งของสาขา

แก้ไข:

โค้ดด้านบนเสียทั้งหมด รหัสนี้ใช้งานได้และยังไม่มีสาขา (หากปรับให้เหมาะสม):

int BitScanLow_BranchFree(ui value)
{
    int i16 = !(value & 0xffff) << 4;
    value >>= i16;

    int i8 = !(value & 0xff) << 3;
    value >>= i8;

    int i4 = !(value & 0xf) << 2;
    value >>= i4;

    int i2 = !(value & 0x3) << 1;
    value >>= i2;

    int i1 = !(value & 0x1);

    int i0 = (value >> i1) & 1? 0 : -32;

    return i16 + i8 + i4 + i2 + i1 + i0;
}

สิ่งนี้จะคืนค่า -1 ถ้าให้ 0 หากคุณไม่สนใจ 0 หรือพอใจที่จะได้ 31 สำหรับ 0 ให้ลบการคำนวณ i0 ออกเพื่อประหยัดเวลา


3
ฉันแก้ไขให้คุณ อย่าลืมทดสอบสิ่งที่คุณโพสต์
Jim Balter

5
คุณจะเรียกมันว่า "ไม่มีสาขา" ได้อย่างไรในเมื่อมันมีโอเปอเรเตอร์ ternary อยู่ในนั้น
BoltBait

2
มันเป็นการเคลื่อนไหวตามเงื่อนไข คำสั่งภาษา Assembly เดียวที่รับค่าที่เป็นไปได้ทั้งสองค่าเป็นพารามิเตอร์และดำเนินการ mov ตามการประเมินเงื่อนไข และเป็น "สาขาฟรี" ไม่มีการข้ามไปยังที่อยู่อื่นที่ไม่รู้จักหรืออาจไม่ถูกต้อง
แดน

FWIW gcc สร้างสาขาได้แม้ใน-O3 godbolt.org/z/gcsUHd
Qix - MONICA ถูกหมอกใน

7

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

unsigned GetLowestBitPos(unsigned value)
{
   double d = value ^ (value - !!value); 
   return (((int*)&d)[1]>>20)-1023; 
}

ข้อดี:

  • ไม่มีลูป
  • ไม่มีการแตกแขนง
  • ทำงานในเวลาคงที่
  • จัดการค่า = 0 โดยส่งคืนผลลัพธ์ที่ไม่อยู่ในขอบเขต
  • โค้ดเพียงสองบรรทัด

จุดด้อย:

  • ถือว่า endianness เพียงเล็กน้อยเป็นรหัส (สามารถแก้ไขได้โดยการเปลี่ยนค่าคงที่)
  • ถือว่าสองเท่าเป็นลอยจริง * 8 IEEE (IEEE 754)

อัปเดต: ตามที่ระบุไว้ในความคิดเห็นสหภาพคือการใช้งานที่สะอาดกว่า (สำหรับ C อย่างน้อย) และจะมีลักษณะดังนี้:

unsigned GetLowestBitPos(unsigned value)
{
    union {
        int i[2];
        double d;
    } temp = { .d = value ^ (value - !!value) };
    return (temp.i[1] >> 20) - 1023;
}

สิ่งนี้ถือว่า ints 32 บิตพร้อมที่เก็บข้อมูล endian น้อยสำหรับทุกสิ่ง (คิดว่าโปรเซสเซอร์ x86)


1
น่าสนใจ - ฉันยังกลัวที่จะใช้การ
คูณ

การใช้ frexp () อาจทำให้พกพาสะดวกขึ้นเล็กน้อย
aka.nice

1
การพิมพ์เจาะโดยการหล่อตัวชี้ไม่ปลอดภัยใน C หรือ C ++ ใช้ memcpy ใน C ++ หรือยูเนี่ยนใน C (หรือยูเนี่ยนใน C ++ หากคอมไพเลอร์ของคุณรับรองว่าปลอดภัยตัวอย่างเช่นส่วนขยาย GNU ไปยัง C ++ (รองรับโดยคอมไพเลอร์จำนวนมาก) รับประกันว่าการพิมพ์ยูเนี่ยนจะปลอดภัย)
ปีเตอร์ Cordes

1
gcc รุ่นเก่ายังสร้างโค้ดที่ดีกว่าด้วยการรวมกันแทนที่จะเป็นพอยเตอร์คาสต์: มันย้ายโดยตรงจาก FP reg (xmm0) ไปยัง rax (พร้อม movq) แทนการจัดเก็บ / โหลดซ้ำ gcc และ clang ใหม่กว่าใช้ movq สำหรับทั้งสองวิธี ดูgodbolt.org/g/x7JBiLสำหรับเวอร์ชันสหภาพ เป็นเจตนาที่คุณกำลังทำการเปลี่ยนแปลงเลขคณิต 20 หรือไม่? สมมติฐานของคุณควรยังมีรายการที่intเป็นint32_tและว่าการเปลี่ยนแปลงทางขวาลงนามเป็นกะเลขคณิต (ใน C ++ มันดำเนินงานกำหนด)
ปีเตอร์ Cordes

1
นอกจากนี้ BTW, Visual Studio (อย่างน้อย 2013) ยังใช้วิธีการทดสอบ / setcc / ย่อย ฉันชอบ cmp / adc ตัวเองดีกว่า
DocMax

5

สามารถทำได้ในกรณีที่แย่ที่สุดที่มีการดำเนินการน้อยกว่า 32 รายการ:

หลักการ: การตรวจสอบ 2 บิตขึ้นไปมีประสิทธิภาพพอ ๆ กับการตรวจสอบ 1 บิต

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

ดังนั้น ...
หากคุณตรวจสอบครั้งละ 2 บิตคุณมีในกรณีที่เลวร้ายที่สุด (Nbits / 2) + 1 เช็คทั้งหมด
หากคุณตรวจสอบครั้งละ 3 บิตคุณมีในกรณีที่เลวร้ายที่สุด (Nbits / 3) + 2 เช็คทั้งหมด
...

ที่ดีที่สุดคือการตรวจสอบในกลุ่ม 4 ซึ่งจะต้องใช้ในกรณีที่เลวร้ายที่สุด 11 การดำเนินการแทนที่จะเป็น 32 ของคุณ

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

หมายเหตุ: ฉันเขียนแบบเต็มแทนที่จะใช้ลูปเพราะมันมีประสิทธิภาพมากกว่า

int getLowestBitPos(unsigned int value)
{
    //Group 1: Bits 0-3
    if(value&0xf)
    {
        if(value&0x1)
            return 0;
        else if(value&0x2)
            return 1;
        else if(value&0x4)
            return 2;
        else
            return 3;
    }

    //Group 2: Bits 4-7
    if(value&0xf0)
    {
        if(value&0x10)
            return 4;
        else if(value&0x20)
            return 5;
        else if(value&0x40)
            return 6;
        else
            return 7;
    }

    //Group 3: Bits 8-11
    if(value&0xf00)
    {
        if(value&0x100)
            return 8;
        else if(value&0x200)
            return 9;
        else if(value&0x400)
            return 10;
        else
            return 11;
    }

    //Group 4: Bits 12-15
    if(value&0xf000)
    {
        if(value&0x1000)
            return 12;
        else if(value&0x2000)
            return 13;
        else if(value&0x4000)
            return 14;
        else
            return 15;
    }

    //Group 5: Bits 16-19
    if(value&0xf0000)
    {
        if(value&0x10000)
            return 16;
        else if(value&0x20000)
            return 17;
        else if(value&0x40000)
            return 18;
        else
            return 19;
    }

    //Group 6: Bits 20-23
    if(value&0xf00000)
    {
        if(value&0x100000)
            return 20;
        else if(value&0x200000)
            return 21;
        else if(value&0x400000)
            return 22;
        else
            return 23;
    }

    //Group 7: Bits 24-27
    if(value&0xf000000)
    {
        if(value&0x1000000)
            return 24;
        else if(value&0x2000000)
            return 25;
        else if(value&0x4000000)
            return 26;
        else
            return 27;
    }

    //Group 8: Bits 28-31
    if(value&0xf0000000)
    {
        if(value&0x10000000)
            return 28;
        else if(value&0x20000000)
            return 29;
        else if(value&0x40000000)
            return 30;
        else
            return 31;
    }

    return -1;
}

+1 จากฉัน มันไม่เร็วที่สุด แต่เร็วกว่าของเดิมซึ่งเป็นประเด็น ...
Andrew Grant

@ onebyone.livejournal.com: แม้ว่าจะมีข้อผิดพลาดในโค้ด แต่แนวคิดของการจัดกลุ่มก็เป็นจุดที่ฉันพยายามจะข้ามไป ตัวอย่างโค้ดจริงไม่สำคัญมากนักและสามารถทำให้กะทัดรัดกว่า แต่มีประสิทธิภาพน้อยกว่า
Brian R.Bondy

ฉันแค่สงสัยว่าคำตอบของฉันมีส่วนที่ไม่ดีจริง ๆ หรือถ้าคนไม่ชอบที่ฉันเขียนเต็ม?
Brian R.Bondy

@ onebyone.livejournal.com: เมื่อคุณเปรียบเทียบ 2 อัลกอริทึมคุณควรเปรียบเทียบกับอัลกอริทึมเหมือนกันโดยไม่คิดว่าขั้นตอนการเพิ่มประสิทธิภาพจะถูกแปลงอย่างน่าอัศจรรย์ ฉันไม่เคยอ้างว่าอัลกอริทึมของฉัน "เร็วกว่า" ด้วย เพียงแต่ว่าเป็นการดำเนินการน้อย
Brian R.Bondy

@ onebyone.livejournal.com: ... ฉันไม่จำเป็นต้องกำหนดโปรไฟล์โค้ดด้านบนเพื่อให้รู้ว่ามันทำงานน้อย ฉันสามารถเห็นสิ่งนั้นได้อย่างชัดเจน ฉันไม่เคยเรียกร้องใด ๆ ที่ต้องมีการทำโปรไฟล์
Brian R.Bondy

4

ทำไมไม่ใช้การค้นหาแบบไบนารี ? สิ่งนี้จะเสร็จสมบูรณ์หลังจากการดำเนินการ 5 ครั้ง (สมมติว่าขนาด int เท่ากับ 4 ไบต์):

if (0x0000FFFF & value) {
    if (0x000000FF & value) {
        if (0x0000000F & value) {
            if (0x00000003 & value) {
                if (0x00000001 & value) {
                    return 1;
                } else {
                    return 2;
                }
            } else {
                if (0x0000004 & value) {
                    return 3;
                } else {
                    return 4;
                }
            }
        } else { ...
    } else { ...
} else { ...

+1 นี่คล้ายกับคำตอบของฉันมาก เวลาเรียกใช้กรณีที่ดีที่สุดแย่กว่าข้อเสนอแนะของฉัน แต่เวลาทำงานในกรณีที่เลวร้ายที่สุดจะดีกว่า
Brian R.Bondy

2

อีกวิธีหนึ่ง (การแบ่งโมดูลัสและการค้นหา) สมควรได้รับการกล่าวถึงเป็นพิเศษที่นี่จากลิงก์เดียวกันที่มีให้โดย @ anton-tykhyy วิธีนี้มีประสิทธิภาพใกล้เคียงกับ DeBruijn คูณและวิธีการค้นหาโดยมีข้อแตกต่างเล็กน้อย แต่สำคัญ

การแบ่งโมดูลัสและการค้นหา

 unsigned int v;  // find the number of trailing zeros in v
    int r;           // put the result in r
    static const int Mod37BitPosition[] = // map a bit value mod 37 to its position
    {
      32, 0, 1, 26, 2, 23, 27, 0, 3, 16, 24, 30, 28, 11, 0, 13, 4,
      7, 17, 0, 25, 22, 31, 15, 29, 10, 12, 6, 0, 21, 14, 9, 5,
      20, 8, 19, 18
    };
    r = Mod37BitPosition[(-v & v) % 37];

การหารโมดูลัสและวิธีการค้นหาจะส่งคืนค่าที่แตกต่างกันสำหรับ v = 0x00000000 และ v = FFFFFFFF ในขณะที่วิธีการคูณและการค้นหา DeBruijn จะส่งกลับค่าศูนย์ในอินพุตทั้งสอง

ทดสอบ:-

unsigned int n1=0x00000000, n2=0xFFFFFFFF;

MultiplyDeBruijnBitPosition[((unsigned int )((n1 & -n1) * 0x077CB531U)) >> 27]); /* returns 0 */
MultiplyDeBruijnBitPosition[((unsigned int )((n2 & -n2) * 0x077CB531U)) >> 27]); /* returns 0 */
Mod37BitPosition[(((-(n1) & (n1))) % 37)]); /* returns 32 */
Mod37BitPosition[(((-(n2) & (n2))) % 37)]); /* returns 0 */

1
modช้า แต่คุณสามารถใช้วิธีการเดิมและค้นหาคูณและลบ!vจากrการจัดการกับกรณีขอบ
Eitan T

3
@EitanT เครื่องมือเพิ่มประสิทธิภาพอาจเปลี่ยน mod นั้นให้เป็นการคูณที่รวดเร็วเหมือนในความสุขของแฮ็กเกอร์
phuclv

2

ตามหน้า BitScan การเขียนโปรแกรมหมากรุกและการวัดของฉันเองการลบและ xor นั้นเร็วกว่าการลบและมาสก์

(สังเกตว่าคุณจะนับเลขศูนย์ต่อท้าย0วิธีที่ฉันส่งกลับมา63ในขณะที่ค่าลบและมาสก์จะส่งกลับ0)

นี่คือการลบ 64 บิตและ xor:

unsigned long v;  // find the number of trailing zeros in 64-bit v 
int r;            // result goes here
static const int MultiplyDeBruijnBitPosition[64] = 
{
  0, 47, 1, 56, 48, 27, 2, 60, 57, 49, 41, 37, 28, 16, 3, 61,
  54, 58, 35, 52, 50, 42, 21, 44, 38, 32, 29, 23, 17, 11, 4, 62,
  46, 55, 26, 59, 40, 36, 15, 53, 34, 51, 20, 43, 31, 22, 10, 45,
  25, 39, 14, 33, 19, 30, 9, 24, 13, 18, 8, 12, 7, 6, 5, 63
};
r = MultiplyDeBruijnBitPosition[((uint32_t)((v ^ (v-1)) * 0x03F79D71B4CB0A89U)) >> 58];

สำหรับการอ้างอิงนี่คือเวอร์ชัน 64 บิตของวิธีลบและมาสก์:

unsigned long v;  // find the number of trailing zeros in 64-bit v 
int r;            // result goes here
static const int MultiplyDeBruijnBitPosition[64] = 
{
  0, 1, 48, 2, 57, 49, 28, 3, 61, 58, 50, 42, 38, 29, 17, 4,
  62, 55, 59, 36, 53, 51, 43, 22, 45, 39, 33, 30, 24, 18, 12, 5,
  63, 47, 56, 27, 60, 41, 37, 16, 54, 35, 52, 21, 44, 32, 23, 11,
  46, 26, 40, 15, 34, 20, 31, 10, 25, 14, 19, 9, 13, 8, 7, 6
};
r = MultiplyDeBruijnBitPosition[((uint32_t)((v & -v) * 0x03F79D71B4CB0A89U)) >> 58];

นี้ผลงานที่จัดไว้ให้(v ^ (v-1)) v != 0ในกรณีที่v == 0มันส่งกลับ 0xFF .... FF ในขณะที่(v & -v)ให้ศูนย์ (ซึ่งก็ผิดเช่นกัน buf อย่างน้อยมันก็นำไปสู่ผลลัพธ์ที่สมเหตุสมผล)
CiaPan

@CiaPan: นั่นเป็นจุดที่ดีฉันจะพูดถึงมัน ฉันเดาว่ามีหมายเลข De Bruijn อื่นที่สามารถแก้ไขปัญหานี้ได้โดยใส่ 0 ในดัชนีที่ 63
jnm2

Duh นั่นไม่ใช่ปัญหา 0 และ 0x8000000000000000 ทั้งคู่ส่งผลให้เป็น 0xFFFFFFFFFFFFFFFFFF หลังจากv ^ (v-1)นั้นจึงไม่มีการบอกแยกกัน ในสถานการณ์ของฉันจะไม่มีการป้อนค่าศูนย์
jnm2

1

คุณสามารถตรวจสอบว่ามีการตั้งค่าบิตลำดับที่ต่ำกว่าหรือไม่ ถ้าเป็นเช่นนั้นให้ดูลำดับล่างของบิตที่เหลือ เช่น,:

32 บิต int - ตรวจสอบว่ามีการตั้งค่า 16 รายการแรกหรือไม่ ในกรณีนี้ให้ตรวจสอบว่ามีการตั้งค่า 8 รายการแรกไว้หรือไม่ ถ้าเป็นเช่นนั้น ....

ถ้าไม่ให้ตรวจสอบว่ามี 16 ตัวบนหรือไม่ ..

โดยพื้นฐานแล้วเป็นการค้นหาแบบไบนารี


1

ดูคำตอบของฉันที่นี่สำหรับวิธีดำเนินการด้วยคำสั่ง x86 เพียงคำสั่งเดียวยกเว้นว่าหากต้องการค้นหาบิตชุดที่มีนัยสำคัญน้อยที่สุดคุณจะต้องใช้คำสั่งBSF("bit scan forward") แทนที่จะBSRอธิบายไว้ที่นั่น


1

อีกวิธีหนึ่งไม่ใช่วิธีที่เร็วที่สุด แต่ดูเหมือนจะค่อนข้างดี
อย่างน้อยมันก็ไม่มีสาขา ;)

uint32 x = ...;  // 0x00000001  0x0405a0c0  0x00602000
x |= x <<  1;    // 0x00000003  0x0c0fe1c0  0x00e06000
x |= x <<  2;    // 0x0000000f  0x3c3fe7c0  0x03e1e000
x |= x <<  4;    // 0x000000ff  0xffffffc0  0x3fffe000
x |= x <<  8;    // 0x0000ffff  0xffffffc0  0xffffe000
x |= x << 16;    // 0xffffffff  0xffffffc0  0xffffe000

// now x is filled with '1' from the least significant '1' to bit 31

x = ~x;          // 0x00000000  0x0000003f  0x00001fff

// now we have 1's below the original least significant 1
// let's count them

x = x & 0x55555555 + (x >>  1) & 0x55555555;
                 // 0x00000000  0x0000002a  0x00001aaa

x = x & 0x33333333 + (x >>  2) & 0x33333333;
                 // 0x00000000  0x00000024  0x00001444

x = x & 0x0f0f0f0f + (x >>  4) & 0x0f0f0f0f;
                 // 0x00000000  0x00000006  0x00000508

x = x & 0x00ff00ff + (x >>  8) & 0x00ff00ff;
                 // 0x00000000  0x00000006  0x0000000d

x = x & 0x0000ffff + (x >> 16) & 0x0000ffff;
                 // 0x00000000  0x00000006  0x0000000d
// least sign.bit pos. was:  0           6          13

เพื่อให้ได้ทั้งหมด1จาก 1 ที่มีนัยสำคัญน้อยที่สุดให้ใช้((x & -x) - 1) << 1แทน
phuclv

วิธีที่เร็วยิ่งขึ้น:x ^ (x-1)
phuclv

1
unsigned GetLowestBitPos(unsigned value)
{
    if (value & 1) return 1;
    if (value & 2) return 2;
    if (value & 4) return 3;
    if (value & 8) return 4;
    if (value & 16) return 5;
    if (value & 32) return 6;
    if (value & 64) return 7;
    if (value & 128) return 8;
    if (value & 256) return 9;
    if (value & 512) return 10;
    if (value & 1024) return 11;
    if (value & 2048) return 12;
    if (value & 4096) return 13;
    if (value & 8192) return 14;
    if (value & 16384) return 15;
    if (value & 32768) return 16;
    if (value & 65536) return 17;
    if (value & 131072) return 18;
    if (value & 262144) return 19;
    if (value & 524288) return 20;
    if (value & 1048576) return 21;
    if (value & 2097152) return 22;
    if (value & 4194304) return 23;
    if (value & 8388608) return 24;
    if (value & 16777216) return 25;
    if (value & 33554432) return 26;
    if (value & 67108864) return 27;
    if (value & 134217728) return 28;
    if (value & 268435456) return 29;
    if (value & 536870912) return 30;
    return 31;
}

50% ของตัวเลขทั้งหมดจะกลับมาในบรรทัดแรกของโค้ด

75% ของตัวเลขทั้งหมดจะกลับมาใน 2 บรรทัดแรกของโค้ด

87% ของตัวเลขทั้งหมดจะกลับมาใน 3 บรรทัดแรกของรหัส

94% ของตัวเลขทั้งหมดจะกลับมาใน 4 บรรทัดแรกของรหัส

97% ของตัวเลขทั้งหมดจะกลับมาใน 5 บรรทัดแรกของรหัส

เป็นต้น

ฉันคิดว่าคนที่บ่นว่าไม่มีประสิทธิภาพในกรณีที่เลวร้ายที่สุดสำหรับรหัสนี้ไม่เข้าใจว่าเงื่อนไขนั้นจะเกิดขึ้นได้ยากเพียงใด


3
และเป็นกรณีที่เลวร้ายที่สุดของการ

1
ไม่ได้นี้อาจอย่างน้อยทำให้เป็นสวิทช์ ... ?
Steven Lu

“ อย่างน้อยก็ไม่สามารถเปลี่ยนเป็นสวิตช์ได้ ... ?” คุณได้ลองทำก่อนที่จะบอกเป็นนัยว่าเป็นไปได้หรือไม่? ตั้งแต่เมื่อใดที่คุณสามารถคำนวณได้ในกรณีของสวิตช์? มันเป็นตารางการค้นหาไม่ใช่ชั้นเรียน
j riv

1

พบเคล็ดลับอันชาญฉลาดนี้โดยใช้ 'มาสก์วิเศษ' ใน "ศิลปะการเขียนโปรแกรมตอนที่ 4" ซึ่งทำในเวลา O (log (n)) สำหรับตัวเลข n-bit [พร้อม log (n) ช่องว่างพิเศษ] วิธีแก้ปัญหาโดยทั่วไปการตรวจสอบ set bit คือ O (n) หรือต้องการ O (n) พื้นที่เพิ่มเติมสำหรับการค้นหาตารางดังนั้นนี่จึงเป็นการประนีประนอมที่ดี

หน้ากากวิเศษ:

m0 = (...............01010101)  
m1 = (...............00110011)
m2 = (...............00001111)  
m3 = (.......0000000011111111)
....

แนวคิดหลัก: จำนวนศูนย์ต่อท้ายใน x = 1 * [(x & m0) = 0] + 2 * [(x & m1) = 0] + 4 * [(x & m2) = 0] + ...

int lastSetBitPos(const uint64_t x) {
    if (x == 0)  return -1;

    //For 64 bit number, log2(64)-1, ie; 5 masks needed
    int steps = log2(sizeof(x) * 8); assert(steps == 6);
    //magic masks
    uint64_t m[] = { 0x5555555555555555, //     .... 010101
                     0x3333333333333333, //     .....110011
                     0x0f0f0f0f0f0f0f0f, //     ...00001111
                     0x00ff00ff00ff00ff, //0000000011111111 
                     0x0000ffff0000ffff, 
                     0x00000000ffffffff };

    //Firstly extract only the last set bit
    uint64_t y = x & -x;

    int trailZeros = 0, i = 0 , factor = 0;
    while (i < steps) {
        factor = ((y & m[i]) == 0 ) ? 1 : 0;
        trailZeros += factor * pow(2,i);
        ++i;
    }
    return (trailZeros+1);
}

1

หาก C ++ 11 พร้อมใช้งานสำหรับคุณบางครั้งคอมไพเลอร์สามารถทำงานให้คุณได้ :)

constexpr std::uint64_t lssb(const std::uint64_t value)
{
    return !value ? 0 : (value % 2 ? 1 : lssb(value >> 1) + 1);
}

ผลลัพธ์คือดัชนี 1 ฐาน


1
ฉลาด แต่คอมไพล์ไปยังแอสเซมบลีที่ไม่ดีอย่างย่อยยับเมื่ออินพุตไม่ใช่ค่าคงที่เวลาคอมไพล์ godbolt.org/g/7ajMyT . (การวนซ้ำที่เป็นใบ้เหนือบิตด้วย gcc หรือการเรียกใช้ฟังก์ชันแบบวนซ้ำจริงด้วยเสียงดัง) gcc / clang สามารถประเมินได้ffs()ในเวลาคอมไพล์ดังนั้นคุณไม่จำเป็นต้องใช้สิ่งนี้เพื่อให้การแพร่กระจายคงที่จึงจะทำงานได้ (แน่นอนคุณต้องหลีกเลี่ยง inline-asm) หากคุณต้องการสิ่งที่ทำงานเป็น C ++ 11 จริงๆconstexprคุณยังสามารถใช้ GNU C __builtin_ffsได้
Peter Cordes

0

นี่คือคำตอบของ @Anton Tykhyy

นี่คือการใช้งาน C ++ 11 constexpr ของฉันโดยใช้ casts และลบคำเตือนใน VC ++ 17 โดยตัดทอนผลลัพธ์ 64 บิตเป็น 32 บิต:

constexpr uint32_t DeBruijnSequence[32] =
{
    0, 1, 28, 2, 29, 14, 24, 3, 30, 22, 20, 15, 25, 17, 4, 8,
    31, 27, 13, 23, 21, 19, 16, 7, 26, 12, 18, 6, 11, 5, 10, 9
};
constexpr uint32_t ffs ( uint32_t value )
{
    return  DeBruijnSequence[ 
        (( ( value & ( -static_cast<int32_t>(value) ) ) * 0x077CB531ULL ) & 0xFFFFFFFF)
            >> 27];
}

ในการแก้ไขปัญหาของ 0x1 และ 0x0 ทั้งสองส่งคืน 0 คุณสามารถทำได้:

constexpr uint32_t ffs ( uint32_t value )
{
    return (!value) ? 32 : DeBruijnSequence[ 
        (( ( value & ( -static_cast<int32_t>(value) ) ) * 0x077CB531ULL ) & 0xFFFFFFFF)
            >> 27];
}

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

สุดท้ายหากสนใจนี่คือรายการของการยืนยันแบบคงที่เพื่อตรวจสอบว่ารหัสทำในสิ่งที่ตั้งใจจะ:

static_assert (ffs(0x1) == 0, "Find First Bit Set Failure.");
static_assert (ffs(0x2) == 1, "Find First Bit Set Failure.");
static_assert (ffs(0x4) == 2, "Find First Bit Set Failure.");
static_assert (ffs(0x8) == 3, "Find First Bit Set Failure.");
static_assert (ffs(0x10) == 4, "Find First Bit Set Failure.");
static_assert (ffs(0x20) == 5, "Find First Bit Set Failure.");
static_assert (ffs(0x40) == 6, "Find First Bit Set Failure.");
static_assert (ffs(0x80) == 7, "Find First Bit Set Failure.");
static_assert (ffs(0x100) == 8, "Find First Bit Set Failure.");
static_assert (ffs(0x200) == 9, "Find First Bit Set Failure.");
static_assert (ffs(0x400) == 10, "Find First Bit Set Failure.");
static_assert (ffs(0x800) == 11, "Find First Bit Set Failure.");
static_assert (ffs(0x1000) == 12, "Find First Bit Set Failure.");
static_assert (ffs(0x2000) == 13, "Find First Bit Set Failure.");
static_assert (ffs(0x4000) == 14, "Find First Bit Set Failure.");
static_assert (ffs(0x8000) == 15, "Find First Bit Set Failure.");
static_assert (ffs(0x10000) == 16, "Find First Bit Set Failure.");
static_assert (ffs(0x20000) == 17, "Find First Bit Set Failure.");
static_assert (ffs(0x40000) == 18, "Find First Bit Set Failure.");
static_assert (ffs(0x80000) == 19, "Find First Bit Set Failure.");
static_assert (ffs(0x100000) == 20, "Find First Bit Set Failure.");
static_assert (ffs(0x200000) == 21, "Find First Bit Set Failure.");
static_assert (ffs(0x400000) == 22, "Find First Bit Set Failure.");
static_assert (ffs(0x800000) == 23, "Find First Bit Set Failure.");
static_assert (ffs(0x1000000) == 24, "Find First Bit Set Failure.");
static_assert (ffs(0x2000000) == 25, "Find First Bit Set Failure.");
static_assert (ffs(0x4000000) == 26, "Find First Bit Set Failure.");
static_assert (ffs(0x8000000) == 27, "Find First Bit Set Failure.");
static_assert (ffs(0x10000000) == 28, "Find First Bit Set Failure.");
static_assert (ffs(0x20000000) == 29, "Find First Bit Set Failure.");
static_assert (ffs(0x40000000) == 30, "Find First Bit Set Failure.");
static_assert (ffs(0x80000000) == 31, "Find First Bit Set Failure.");

0

นี่เป็นทางเลือกง่ายๆทางหนึ่งแม้ว่าการค้นหาบันทึกจะมีค่าใช้จ่ายค่อนข้างสูง

if(n == 0)
  return 0;
return log2(n & -n)+1;   //Assuming the bit index starts from 1

-3

เมื่อเร็ว ๆ นี้ฉันเห็นว่านายกรัฐมนตรีของสิงคโปร์โพสต์โปรแกรมที่เขาเขียนบน facebook มีหนึ่งบรรทัดที่จะพูดถึงมัน ..

ตรรกะเป็นเพียง "value & -value" สมมติว่าคุณมี 0x0FF0 แล้ว 0FF0 & (F00F + 1) ซึ่งเท่ากับ 0x0010 นั่นหมายความว่า 1 ต่ำสุดอยู่ในบิตที่ 4 .. :)


1
สิ่งนี้จะแยกบิตต่ำสุด แต่ไม่ได้ให้ตำแหน่งซึ่งเป็นสิ่งที่คำถามนี้ต้องการ
rhashimoto

ฉันไม่คิดว่ามันจะใช้ได้กับการค้นหาบิตสุดท้ายเช่นกัน
yyny

ค่า & ~ ค่าคือ 0
khw

อ๊ะตาจะแย่แล้ว ฉันเข้าใจผิดว่าเครื่องหมายลบสำหรับทิลเดอ ไม่สนใจความคิดเห็นของฉัน
khw

-8

หากคุณมีทรัพยากรคุณสามารถสละหน่วยความจำเพื่อปรับปรุงความเร็ว:

static const unsigned bitPositions[MAX_INT] = { 0, 0, 1, 0, 2, /* ... */ };

unsigned GetLowestBitPos(unsigned value)
{
    assert(value != 0); // handled separately
    return bitPositions[value];
}

หมายเหตุ:ตารางนี้จะใช้อย่างน้อย 4 GB (16 GB ถ้าเราปล่อยให้ประเภทการส่งคืนเป็นunsigned) นี่คือตัวอย่างของการซื้อขายทรัพยากรที่ จำกัด หนึ่ง (RAM) สำหรับอีกทรัพยากรหนึ่ง (ความเร็วในการดำเนินการ)

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


1
ช่วงของอินพุตถูกระบุไว้แล้วโดยประเภทพารามิเตอร์ - 'unsigned' เป็นค่า 32 บิตดังนั้นไม่คุณไม่เป็นไร
Brian

3
อืม ... ระบบและ OS ที่เป็นตำนานของคุณมีแนวคิดเรื่องหน่วยความจำเพจหรือไม่? ต้องเสียเวลาเท่าไหร่?
Mikeage

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

3
เสียสละความทรงจำเพื่อความเร็ว? ตารางค้นหา 4GB + จะไม่พอดีกับแคชในเครื่องใด ๆ ที่มีอยู่ในปัจจุบันดังนั้นฉันคิดว่ามันอาจจะช้ากว่าคำตอบอื่น ๆ เกือบทั้งหมดที่นี่

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