Cách nhanh nhất / hiệu quả nhất để tìm bit đặt cao nhất (msb) trong một số nguyên trong C là gì?


119

Nếu tôi có một số nguyên n và tôi muốn biết vị trí của bit quan trọng nhất (nghĩa là, nếu bit quan trọng nhất ở bên phải, tôi muốn biết vị trí của bit xa nhất bên trái là 1), phương pháp tìm hiểu nhanh nhất / hiệu quả nhất là gì?

Tôi biết rằng POSIX hỗ trợ một ffs()phương thức trong string.h để tìm bit đặt đầu tiên, nhưng dường như không có fls()phương thức tương ứng .

Có một số cách thực sự rõ ràng để làm điều này mà tôi đang thiếu không?

Còn trong trường hợp bạn không thể sử dụng các chức năng POSIX để di động thì sao?

Chỉnh sửa: Còn về một giải pháp hoạt động trên cả kiến ​​trúc 32 và 64 bit (nhiều danh sách mã có vẻ như chúng chỉ hoạt động trên các int 32 bit).


có một vài cách triển khai ở đây: graphics.stanford.edu/~seander/bithacks.html#ZerosOnRightLinear (Chỉnh sửa: Sau khi đọc lại câu hỏi của bạn, tôi nhận ra rằng liên kết ở trên là để tìm bit đặt ngoài cùng bên phải, không phải ngoài cùng bên trái như bạn yêu cầu, mặc dù không có một ý nghĩa về kích thước từ, đó là một câu trả lời khó)
tiêu


Điều đó đếm số không ở bên phải ; câu hỏi về số không ở bên trái. Ít nhất, trong một đoạn lướt nhanh, tôi không thấy nó ở đó.
Darius Bacon

2
bạn muốn số bit 'n' hay 2 ^ n là đủ?
Alnitak

1
Nhìn vào các thuật toán "Log Base 2" - như Anderson nói trong bài báo: "Log Base 2 của một số nguyên giống như vị trí của tập bit cao nhất (hoặc tập bit quan trọng nhất, MSB)"
Michael Burr

Câu trả lời:


64

GCC có :

 - Hàm tích hợp: int __builtin_clz (unsigned int x)
     Trả về số bit 0 đứng đầu trong X, bắt đầu từ nhiều nhất
     vị trí bit đáng kể. Nếu X là 0, kết quả là không xác định.

 - Hàm tích hợp: int __builtin_clzl (unsigned long)
     Tương tự như `__builtin_clz ', ngoại trừ kiểu đối số là` không dấu
     Dài'.

 - Hàm tích hợp: int __builtin_clzll (dài dài không dấu)
     Tương tự như `__builtin_clz ', ngoại trừ kiểu đối số là` không dấu
     dài dài'.

Tôi hy vọng chúng sẽ được dịch thành một thứ gì đó hiệu quả hợp lý cho nền tảng hiện tại của bạn, cho dù đó là một trong những thuật toán xoắn bit lạ mắt hay một chỉ dẫn duy nhất.


Một mẹo hữu ích nếu đầu vào của bạn thể bằng 0 là __builtin_clz(x | 1): đặt bit thấp một cách vô điều kiện mà không sửa đổi bất kỳ người nào khác tạo đầu ra 31cho x=0mà không thay đổi đầu ra cho bất kỳ đầu vào nào khác.

Để tránh cần phải làm điều đó, tùy chọn khác của bạn là bản chất nền tảng cụ thể như ARM GCC __clz(không cần tiêu đề) hoặc x86 _lzcnt_u32trên các CPU hỗ trợ lzcntlệnh. (Hãy lưu ý rằng lzcntgiải mã như bsrtrên các CPU cũ hơn thay vì lỗi, cung cấp 31-lzcnt cho các đầu vào khác không.)

Thật không may, không có cách nào để tận dụng lợi thế của các lệnh CLZ khác nhau trên các nền tảng không phải x86 xác định kết quả cho đầu vào = 0 là 32 hoặc 64 (theo chiều rộng toán hạng). x86 cũng lzcntlàm điều đó, trong khi bsrtạo ra một chỉ mục bit mà trình biên dịch phải lật trừ khi bạn sử dụng 31-__builtin_clz(x).

("Kết quả không xác định" không phải là Hành vi không xác định C, chỉ là một giá trị không được xác định. Nó thực sự là bất cứ thứ gì có trong thanh ghi đích khi lệnh chạy. AMD ghi lại điều này, Intel thì không, nhưng CPU của Intel thực hiện hành vi đó . Nhưng nó không phải là bất cứ thứ gì trước đây trong biến C mà bạn đang gán, đó thường không phải là cách mọi thứ hoạt động khi gcc biến C thành asm. Xem thêm Tại sao việc phá vỡ "sự phụ thuộc đầu ra" của LZCNT lại quan trọng? )


5
MSVC sẽ có _BitScanReverse
ratchet freak

1
Hành vi undefined-on-zero cho phép chúng biên dịch thành một lệnh BSR duy nhất trên x86, ngay cả khi LZCNT không khả dụng. Đây là một lợi thế lớn đối với __builtin_ctzover ffs, nó biên dịch thành BSF và CMOV để xử lý trường hợp đầu vào-là-không. Trên các kiến ​​trúc không có quá trình triển khai đủ ngắn (ví dụ: ARM cũ không có clzhướng dẫn), gcc phát ra lệnh gọi đến hàm trợ giúp libgcc.
Peter Cordes

41

Giả sử bạn đang sử dụng x86 và chơi trò chơi để tìm kiếm một chút trình biên dịch nội tuyến, Intel sẽ cung cấp một BSRhướng dẫn ("đảo ngược quét bit"). Nó nhanh trên một số x86 (được mã vi mô trên những người khác). Từ sách hướng dẫn:

Tìm kiếm toán hạng nguồn cho bit đặt quan trọng nhất (1 bit). Nếu một bit quan trọng nhất được tìm thấy, chỉ số bit của nó được lưu trữ trong toán hạng đích. Toán hạng nguồn có thể là một thanh ghi hoặc một vị trí bộ nhớ; toán hạng đích là một thanh ghi. Chỉ số bit là một độ lệch không dấu từ bit 0 của toán hạng nguồn. Nếu toán hạng nguồn nội dung là 0, thì nội dung của toán hạng đích là không xác định.

(Nếu bạn đang sử dụng PowerPC, có một lệnh tương tự cntlz("đếm số không ở đầu").)

Mã mẫu cho gcc:

#include <iostream>

int main (int,char**)
{
  int n=1;
  for (;;++n) {
    int msb;
    asm("bsrl %1,%0" : "=r"(msb) : "r"(n));
    std::cout << n << " : " << msb << std::endl;
  }
  return 0;
}

Xem thêm hướng dẫn trình hợp ngữ nội tuyến này , cho thấy (phần 9.4) nó nhanh hơn đáng kể so với mã lặp.


4
Trên thực tế, hướng dẫn này thường được mã hóa vi mô thành một vòng lặp và khá chậm.
rlbond

2
Cái nào ? BSR hay CNTLZ? Khi tôi đọc x86-timing.pdf được tham chiếu ở trên, BSR chỉ chậm trên Netburst Pentiums. Tôi không biết gì về PowerPC mặc dù.
timday

5
... OK, khi kiểm tra kỹ hơn, hãy đảm bảo rằng "BSR chỉ nhanh trên P3 / Pentium-M / Core2 x86s". Chậm trên Netburst và AMD.
timday

1
Lưu ý: Hai liên kết cuối cùng của bạn đã chết.
Baum mit Augen

2
@rlbond: huh, BSR trên P4 Prescott là 2 uops với độ trễ 16 chu kỳ (!), với một uops trên mỗi thông lượng 4c. Nhưng trên Netburst trước đó, độ trễ chỉ là 4 chu kỳ (vẫn là 2 uops) và một trên mỗi 2c thông lượng. (nguồn: agner.org/optimize ). Trên hầu hết các CPU, nó cũng có sự phụ thuộc vào đầu ra của nó mà gcc không tính đến (khi đầu vào bằng 0, hành vi thực tế là giữ nguyên điểm đến). Điều này có thể dẫn đến các sự cố như stackoverflow.com/questions/25078285/… . IDK tại sao gcc lại bỏ lỡ BSR khi sửa lỗi đó.
Peter Cordes

38

Vì 2 ^ N là số nguyên chỉ có tập bit thứ N (1 << N), nên việc tìm vị trí (N) của bit tập cao nhất là số nguyên log cơ số 2 của số nguyên đó.

http://graphics.stanford.edu/~seander/bithacks.html#IntegerLogObvious

unsigned int v;
unsigned r = 0;

while (v >>= 1) {
    r++;
}

Thuật toán "hiển nhiên" này có thể không minh bạch với tất cả mọi người, nhưng khi bạn nhận ra rằng mã dịch chuyển sang phải từng bit liên tục cho đến khi bit ngoài cùng bên trái bị dịch chuyển (lưu ý rằng C coi mọi giá trị khác 0 là đúng) và trả về số thay đổi, nó có ý nghĩa hoàn hảo. Nó cũng có nghĩa là nó hoạt động ngay cả khi nhiều hơn một bit được thiết lập - kết quả luôn dành cho bit quan trọng nhất.

Nếu bạn cuộn xuống trên trang đó, sẽ có những biến thể nhanh hơn, phức tạp hơn. Tuy nhiên, nếu bạn biết rằng bạn đang xử lý các số có nhiều số 0 ở đầu, thì cách tiếp cận đơn giản có thể cung cấp tốc độ chấp nhận được, vì dịch chuyển bit khá nhanh trong C và thuật toán đơn giản không yêu cầu lập chỉ mục một mảng.

LƯU Ý: Khi sử dụng các giá trị 64-bit, hãy cực kỳ thận trọng khi sử dụng các thuật toán cực kỳ thông minh; nhiều trong số chúng chỉ hoạt động chính xác cho các giá trị 32-bit.


2
@Johan Thực hiện qua trình gỡ lỗi có thể giúp giải thích lý do thoát ra khỏi vòng lặp. Về cơ bản, nó 'bởi vì biểu thức trong điều kiện đánh giá là 0 (được coi là sai) sau khi 1 bit cuối cùng bị dịch chuyển sang bên phải.
Quinn Taylor

2
Ý tưởng tuyệt vời khi sử dụng kết quả cuối cùng như vậy :)
Johan

6
lưu ý: phải không có dấu, đối với số nguyên có dấu, không thể chuyển dịch sang phải đối với số âm.
Xantix

2
Xantix: Sự thay đổi trong C / C ++ là một sự thay đổi hợp lý, vì vậy nó hoạt động tốt. Đối với Java, JavaScript hoặc D, bạn cần sử dụng toán tử dịch chuyển logic >>>. Thêm vào đó có lẽ là bộ so sánh != 0và một số dấu ngoặc đơn không xác định.
Đuổi theo

8
@Chase: Không, không phải. Đó là một sự thay đổi hợp lý cho không dấu . Đối với chữ ký , nó có thể là một sự thay đổi logic (và nó thường là số học).
Tim Čas

17

Điều này sẽ nhanh như chớp:

int msb(unsigned int v) {
  static const int pos[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};
  v |= v >> 1;
  v |= v >> 2;
  v |= v >> 4;
  v |= v >> 8;
  v |= v >> 16;
  v = (v >> 1) + 1;
  return pos[(v * 0x077CB531UL) >> 27];
}

25
7 bit thay đổi, 5 hoặc hướng dẫn, đa nền và bộ nhớ đệm tiềm ẩn bỏ lỡ. :) Bạn đã chuẩn hóa nó, hay nhìn vào trình lắp ráp được tạo ra? Nó có thể kết thúc khá chậm, tùy thuộc vào mức độ mà trình biên dịch có thể loại bỏ.
jalf

5
Tôi là người mới ở đây. Tôi không nhận được phiếu tiêu cực. Tôi đã cung cấp câu trả lời duy nhất với mã nguồn thực sự hoạt động.
Nhân vật chính

9
"Có thể bỏ lỡ bộ nhớ cache" có thể là do mã này yêu cầu quyền truy cập vào bảng tra cứu của nó. Nếu bảng đó không được lưu trong bộ nhớ cache khi điều này được gọi, sẽ có một sự cố trong khi tìm nạp. Điều này có thể làm cho hiệu suất trong trường hợp xấu nhất kém hơn nhiều so với các giải pháp không sử dụng LUT.
thư giãn

13
không hẳn là vấn đề. Nó sử dụng nhiều bộ đệm dữ liệu hơn mức cần thiết (thậm chí nhiều hơn một dòng bộ đệm) và nhiều bộ đệm lệnh hơn mức cần thiết. Bạn có thể sẽ nhận được các lần bỏ lỡ bộ nhớ cache mà có thể tránh được trong lần đầu tiên bạn gọi hàm và nó sẽ gây ô nhiễm bộ nhớ cache nhiều hơn mức cần thiết, vì vậy sau cuộc gọi, mã khác có thể gặp nhiều lần bỏ lỡ hơn mức cần thiết. LUT thường không đáng gặp rắc rối vì việc bỏ sót bộ nhớ cache rất tốn kém. Nhưng tôi chỉ nói rằng đó là thứ tôi muốn làm điểm chuẩn trước khi tôi tuyên bố rằng nó "nhanh như chớp". Không phải nó chắc chắn là một vấn đề.
jalf

6
Bảng có 32 mục nhập và mọi giá trị đều <255 (127), vì vậy hãy xác định bảng là kiểu unsigned char và nó sẽ vừa với một dòng bộ đệm L1 32 byte. Và toàn bộ điều nằm gọn trong hai dòng bộ nhớ cache.
ChuckCottrill

16

Điều này giống như tìm một loại nhật ký số nguyên. Có những thủ thuật lộn xộn, nhưng tôi đã tạo ra công cụ của riêng mình cho việc này. Mục tiêu tất nhiên là tốc độ.

Nhận thức của tôi là CPU đã có một bộ dò bit tự động, được sử dụng để chuyển đổi số nguyên sang số thực! Vì vậy, hãy sử dụng cái đó.

double ff=(double)(v|1);
return ((*(1+(uint32_t *)&ff))>>20)-1023;  // assumes x86 endianness

Phiên bản này chuyển giá trị thành nhân đôi, sau đó đọc số mũ, điều này cho bạn biết vị trí của bit. Phép chuyển và phép trừ ưa thích là trích xuất các phần thích hợp từ giá trị IEEE.

Sử dụng phao nổi sẽ nhanh hơn một chút, nhưng phao chỉ có thể cung cấp cho bạn 24 vị trí bit đầu tiên vì độ chính xác của nó nhỏ hơn.


Để thực hiện việc này một cách an toàn, không có hành vi không xác định trong C ++ hoặc C, hãy sử dụng memcpythay vì ép kiểu con trỏ cho kiểu-punning. Trình biên dịch biết làm thế nào để nội dòng nó một cách hiệu quả.

// static_assert(sizeof(double) == 2 * sizeof(uint32_t), "double isn't 8-byte IEEE binary64");
// and also static_assert something about FLT_ENDIAN?

double ff=(double)(v|1);

uint32_t tmp;
memcpy(&tmp, ((const char*)&ff)+sizeof(uint32_t), sizeof(uint32_t));
return (tmp>>20)-1023;

Hoặc trong C99 trở lên, sử dụng a union {double d; uint32_t u[2];};. Nhưng lưu ý rằng trong C ++, punning type union chỉ được hỗ trợ trên một số trình biên dịch dưới dạng phần mở rộng, không phải trong ISO C ++.


Điều này thường sẽ chậm hơn so với nội tại của nền tảng cụ thể đối với hướng dẫn đếm số không ở đầu, nhưng ISO C di động không có chức năng này. Một số CPU cũng thiếu lệnh đếm số 0 đứng đầu, nhưng một số CPU có thể chuyển đổi số nguyên thành một cách hiệu quả double. Tuy nhiên, việc gõ-punning một mẫu bit FP trở về số nguyên có thể chậm (ví dụ: trên PowerPC, nó yêu cầu lưu trữ / tải lại và thường gây ra tình trạng ngừng tải tại cửa hàng).

Thuật toán này có thể hữu ích cho việc triển khai SIMD, vì ít CPU có SIMD hơn lzcnt. x86 chỉ có một lệnh như vậy với AVX512CD


2
Đúng. Và gcc sẽ làm những điều khó chịu với mã như thế này với -O2 do tối ưu hóa răng cưa kiểu.
MSN

4
đúc giữa số nguyên và dấu chấm động có thể ngạc nhiên đắt tiền trên x86 CPU
jalf

1
Đúng, chi phí FPU cao. Nhưng các phép đo thời gian thực tế cho thấy điều này nhanh hơn so với các hoạt động tất cả các bit hoặc đặc biệt là bất kỳ vòng lặp nào. Hãy thử nó và nhanh nhất luôn luôn là lời khuyên tốt nhất. Tôi không gặp vấn đề với GCC và -O2 với điều này.
SPWorley

1
Đây không phải là hành vi không xác định (đọc một giá trị thông qua một con trỏ của một kiểu không tương thích)?
dreamlax

3
Hacker's Delight giải thích cách sửa lỗi trong số nổi 32 bit trong 5-3 Đếm số 0 hàng đầu. Đây là mã của họ, sử dụng liên hợp ẩn danh để chồng lên asFloat và asInt: k = k & ~ (k >> 1); asFloat = (float) k + 0.5f; n = 158 - (asInt >> 23); (và có, điều này phụ thuộc vào hành vi thực hiện xác định)
D Coetzee

11

Kaz Kylheku đây

Tôi đã đánh giá hai phương pháp tiếp cận cho các số hơn 63 bit này (loại dài dài trên gcc x86_64), tránh xa bit dấu.

(Tôi tình cờ cần "tìm bit cao nhất" này cho một cái gì đó, bạn thấy đấy.)

Tôi đã triển khai tìm kiếm nhị phân theo hướng dữ liệu (dựa trên một trong các câu trả lời ở trên). Tôi cũng đã triển khai một cây quyết định hoàn toàn không được cuộn bằng tay, nó chỉ là mã với các toán hạng ngay lập tức. Không có vòng lặp, không có bảng.

Cây quyết định (cao nhất_bit_unrolled) được chuẩn hóa để nhanh hơn 69%, ngoại trừ trường hợp n = 0 mà tìm kiếm nhị phân có một thử nghiệm rõ ràng.

Kiểm tra đặc biệt của tìm kiếm nhị phân cho trường hợp 0 ​​chỉ nhanh hơn 48% so với cây quyết định, không có kiểm tra đặc biệt.

Trình biên dịch, máy: (GCC 4.5.2, -O3, x86-64, 2867 Mhz Intel Core i5).

int highest_bit_unrolled(long long n)
{
  if (n & 0x7FFFFFFF00000000) {
    if (n & 0x7FFF000000000000) {
      if (n & 0x7F00000000000000) {
        if (n & 0x7000000000000000) {
          if (n & 0x4000000000000000)
            return 63;
          else
            return (n & 0x2000000000000000) ? 62 : 61;
        } else {
          if (n & 0x0C00000000000000)
            return (n & 0x0800000000000000) ? 60 : 59;
          else
            return (n & 0x0200000000000000) ? 58 : 57;
        }
      } else {
        if (n & 0x00F0000000000000) {
          if (n & 0x00C0000000000000)
            return (n & 0x0080000000000000) ? 56 : 55;
          else
            return (n & 0x0020000000000000) ? 54 : 53;
        } else {
          if (n & 0x000C000000000000)
            return (n & 0x0008000000000000) ? 52 : 51;
          else
            return (n & 0x0002000000000000) ? 50 : 49;
        }
      }
    } else {
      if (n & 0x0000FF0000000000) {
        if (n & 0x0000F00000000000) {
          if (n & 0x0000C00000000000)
            return (n & 0x0000800000000000) ? 48 : 47;
          else
            return (n & 0x0000200000000000) ? 46 : 45;
        } else {
          if (n & 0x00000C0000000000)
            return (n & 0x0000080000000000) ? 44 : 43;
          else
            return (n & 0x0000020000000000) ? 42 : 41;
        }
      } else {
        if (n & 0x000000F000000000) {
          if (n & 0x000000C000000000)
            return (n & 0x0000008000000000) ? 40 : 39;
          else
            return (n & 0x0000002000000000) ? 38 : 37;
        } else {
          if (n & 0x0000000C00000000)
            return (n & 0x0000000800000000) ? 36 : 35;
          else
            return (n & 0x0000000200000000) ? 34 : 33;
        }
      }
    }
  } else {
    if (n & 0x00000000FFFF0000) {
      if (n & 0x00000000FF000000) {
        if (n & 0x00000000F0000000) {
          if (n & 0x00000000C0000000)
            return (n & 0x0000000080000000) ? 32 : 31;
          else
            return (n & 0x0000000020000000) ? 30 : 29;
        } else {
          if (n & 0x000000000C000000)
            return (n & 0x0000000008000000) ? 28 : 27;
          else
            return (n & 0x0000000002000000) ? 26 : 25;
        }
      } else {
        if (n & 0x0000000000F00000) {
          if (n & 0x0000000000C00000)
            return (n & 0x0000000000800000) ? 24 : 23;
          else
            return (n & 0x0000000000200000) ? 22 : 21;
        } else {
          if (n & 0x00000000000C0000)
            return (n & 0x0000000000080000) ? 20 : 19;
          else
            return (n & 0x0000000000020000) ? 18 : 17;
        }
      }
    } else {
      if (n & 0x000000000000FF00) {
        if (n & 0x000000000000F000) {
          if (n & 0x000000000000C000)
            return (n & 0x0000000000008000) ? 16 : 15;
          else
            return (n & 0x0000000000002000) ? 14 : 13;
        } else {
          if (n & 0x0000000000000C00)
            return (n & 0x0000000000000800) ? 12 : 11;
          else
            return (n & 0x0000000000000200) ? 10 : 9;
        }
      } else {
        if (n & 0x00000000000000F0) {
          if (n & 0x00000000000000C0)
            return (n & 0x0000000000000080) ? 8 : 7;
          else
            return (n & 0x0000000000000020) ? 6 : 5;
        } else {
          if (n & 0x000000000000000C)
            return (n & 0x0000000000000008) ? 4 : 3;
          else
            return (n & 0x0000000000000002) ? 2 : (n ? 1 : 0);
        }
      }
    }
  }
}

int highest_bit(long long n)
{
  const long long mask[] = {
    0x000000007FFFFFFF,
    0x000000000000FFFF,
    0x00000000000000FF,
    0x000000000000000F,
    0x0000000000000003,
    0x0000000000000001
  };
  int hi = 64;
  int lo = 0;
  int i = 0;

  if (n == 0)
    return 0;

  for (i = 0; i < sizeof mask / sizeof mask[0]; i++) {
    int mi = lo + (hi - lo) / 2;

    if ((n >> mi) != 0)
      lo = mi;
    else if ((n & (mask[i] << lo)) != 0)
      hi = mi;
  }

  return lo + 1;
}

Chương trình kiểm tra nhanh và bẩn:

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

int highest_bit_unrolled(long long n);
int highest_bit(long long n);

main(int argc, char **argv)
{
  long long n = strtoull(argv[1], NULL, 0);
  int b1, b2;
  long i;
  clock_t start = clock(), mid, end;

  for (i = 0; i < 1000000000; i++)
    b1 = highest_bit_unrolled(n);

  mid = clock();

  for (i = 0; i < 1000000000; i++)
    b2 = highest_bit(n);

  end = clock();

  printf("highest bit of 0x%llx/%lld = %d, %d\n", n, n, b1, b2);

  printf("time1 = %d\n", (int) (mid - start));
  printf("time2 = %d\n", (int) (end - mid));
  return 0;
}

Chỉ sử dụng -O2, sự khác biệt trở nên lớn hơn. Cây quyết định nhanh hơn gần bốn lần.

Tôi cũng đã so sánh với mã dịch chuyển bit ngây thơ:

int highest_bit_shift(long long n)
{
  int i = 0;
  for (; n; n >>= 1, i++)
    ; /* empty */
  return i;
}

Điều này chỉ nhanh đối với số lượng nhỏ, như người ta mong đợi. Khi xác định rằng bit cao nhất là 1 cho n == 1, nó đã định chuẩn nhanh hơn 80%. Tuy nhiên, một nửa số được chọn ngẫu nhiên trong không gian 63 bit có bit thứ 63!

Trên đầu vào 0x3FFFFFFFFFFFFFFF, phiên bản cây quyết định nhanh hơn một chút so với phiên bản 1 và nhanh hơn 1120% (12,2 lần) so với bộ dịch chuyển bit.

Tôi cũng sẽ đánh giá chuẩn cây quyết định so với nội trang GCC và cũng thử kết hợp các đầu vào thay vì lặp lại với cùng một số. Có thể có một số dự đoán nhánh dính đang xảy ra và có lẽ một số tình huống bộ nhớ đệm không thực tế khiến nó nhanh hơn một cách giả tạo khi lặp lại.


9
Tôi không nói điều này là không tốt, nhưng Chương trình thử nghiệm của bạn ở đây chỉ thử nghiệm trên cùng một số, sau 2-3 lần lặp lại sẽ đặt các dự đoán nhánh ở vị trí cuối cùng và sau đó chúng sẽ đưa ra dự đoán nhánh hoàn hảo. Điều tốt là với phân phối hoàn toàn ngẫu nhiên, một nửa số sẽ có dự đoán gần với dự đoán hoàn hảo, cụ thể là bit63.
Surt


6
unsigned int
msb32(register unsigned int x)
{
        x |= (x >> 1);
        x |= (x >> 2);
        x |= (x >> 4);
        x |= (x >> 8);
        x |= (x >> 16);
        return(x & ~(x >> 1));
}

1 thanh ghi, 13 hướng dẫn. Tin hay không thì tùy, điều này thường nhanh hơn lệnh BSR được đề cập ở trên, hoạt động theo thời gian tuyến tính. Đây là thời gian logarit.

Từ http://aggregate.org/MAGIC/#Most%20Significant%201%20Bit


7
Đoạn mã trên không trả lời câu hỏi. Nó trả về một số nguyên không dấu trong đó bit quan trọng nhất trong x vẫn được bật và tất cả các bit khác bị tắt. Câu hỏi đặt ra là trả lại vị trí của bit quan trọng nhất.
Nhân vật chính

3
Sau đó, bạn có thể sử dụng phương pháp tiếp cận trình tự De Bruijn để tìm chỉ số của bit được đặt. :-)
R .. GitHub NGỪNG TRỢ GIÚP ICE

5
@Prot Character, anh ấy nói trong một bình luận rằng một trong hai điều đó là đủ.
rlbond

Cái này (từ cùng một trang đó) sẽ làm những gì bạn cần, nhưng nó yêu cầu một chức năng bổ sung. tổng
Quinn Taylor

1
BSR nhanh trên CPU Intel kể từ ít nhất là Core2. LZCNT nhanh trên CPU AMD và gcc sử dụng nó __builtin_clznếu nó được kích hoạt với -march=nativehoặc thứ gì đó (vì nó nhanh trên mọi CPU hỗ trợ nó). Ngay cả trên các CPU như AMD Bulldozer-family nơi BSR "chậm", nó không chậm đến mức: 7 m-ops với độ trễ 4 chu kỳ và một thông lượng trên mỗi 4c. Trên Atom, BSR thực sự rất chậm: 16 chu kỳ. Trên Silvermont, đó là 10 uops với độ trễ 10 chu kỳ. Đây có thể là độ trễ thấp hơn một chút so với BSR trên Silvermont, nhưng IDK.
Peter Cordes

6

Dưới đây là một số điểm chuẩn (đơn giản) của các thuật toán hiện được đưa ra trên trang này ...

Các thuật toán chưa được kiểm tra trên tất cả các đầu vào của int unsigned; vì vậy hãy kiểm tra điều đó trước, trước khi sử dụng thứ gì đó một cách mù quáng;)

Trên máy của tôi, clz (__builtin_clz) và asm hoạt động tốt nhất. asm dường như thậm chí còn nhanh hơn sau đó clz ... nhưng nó có thể là do điểm chuẩn đơn giản ...

//////// go.c ///////////////////////////////
// compile with:  gcc go.c -o go -lm
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

/***************** math ********************/

#define POS_OF_HIGHESTBITmath(a) /* 0th position is the Least-Signif-Bit */    \
  ((unsigned) log2(a))         /* thus: do not use if a <= 0 */  

#define NUM_OF_HIGHESTBITmath(a) ((a)               \
                  ? (1U << POS_OF_HIGHESTBITmath(a))    \
                  : 0)



/***************** clz ********************/

unsigned NUM_BITS_U = ((sizeof(unsigned) << 3) - 1);
#define POS_OF_HIGHESTBITclz(a) (NUM_BITS_U - __builtin_clz(a)) /* only works for a != 0 */

#define NUM_OF_HIGHESTBITclz(a) ((a)                    \
                 ? (1U << POS_OF_HIGHESTBITclz(a))  \
                 : 0)


/***************** i2f ********************/

double FF;
#define POS_OF_HIGHESTBITi2f(a) (FF = (double)(ui|1), ((*(1+(unsigned*)&FF))>>20)-1023)


#define NUM_OF_HIGHESTBITi2f(a) ((a)                    \
                 ? (1U << POS_OF_HIGHESTBITi2f(a))  \
                 : 0)




/***************** asm ********************/

unsigned OUT;
#define POS_OF_HIGHESTBITasm(a) (({asm("bsrl %1,%0" : "=r"(OUT) : "r"(a));}), OUT)

#define NUM_OF_HIGHESTBITasm(a) ((a)                    \
                 ? (1U << POS_OF_HIGHESTBITasm(a))  \
                 : 0)




/***************** bitshift1 ********************/

#define NUM_OF_HIGHESTBITbitshift1(a) (({   \
  OUT = a;                  \
  OUT |= (OUT >> 1);                \
  OUT |= (OUT >> 2);                \
  OUT |= (OUT >> 4);                \
  OUT |= (OUT >> 8);                \
  OUT |= (OUT >> 16);               \
      }), (OUT & ~(OUT >> 1)))          \



/***************** bitshift2 ********************/
int POS[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};

#define POS_OF_HIGHESTBITbitshift2(a) (({   \
  OUT = a;                  \
  OUT |= OUT >> 1;              \
  OUT |= OUT >> 2;              \
  OUT |= OUT >> 4;              \
  OUT |= OUT >> 8;              \
  OUT |= OUT >> 16;             \
  OUT = (OUT >> 1) + 1;             \
      }), POS[(OUT * 0x077CB531UL) >> 27])

#define NUM_OF_HIGHESTBITbitshift2(a) ((a)              \
                       ? (1U << POS_OF_HIGHESTBITbitshift2(a)) \
                       : 0)



#define LOOPS 100000000U

int main()
{
  time_t start, end;
  unsigned ui;
  unsigned n;

  /********* Checking the first few unsigned values (you'll need to check all if you want to use an algorithm here) **************/
  printf("math\n");
  for (ui = 0U; ui < 18; ++ui)
    printf("%i\t%i\n", ui, NUM_OF_HIGHESTBITmath(ui));

  printf("\n\n");

  printf("clz\n");
  for (ui = 0U; ui < 18U; ++ui)
    printf("%i\t%i\n", ui, NUM_OF_HIGHESTBITclz(ui));

  printf("\n\n");

  printf("i2f\n");
  for (ui = 0U; ui < 18U; ++ui)
    printf("%i\t%i\n", ui, NUM_OF_HIGHESTBITi2f(ui));

  printf("\n\n");

  printf("asm\n");
  for (ui = 0U; ui < 18U; ++ui) {
    printf("%i\t%i\n", ui, NUM_OF_HIGHESTBITasm(ui));
  }

  printf("\n\n");

  printf("bitshift1\n");
  for (ui = 0U; ui < 18U; ++ui) {
    printf("%i\t%i\n", ui, NUM_OF_HIGHESTBITbitshift1(ui));
  }

  printf("\n\n");

  printf("bitshift2\n");
  for (ui = 0U; ui < 18U; ++ui) {
    printf("%i\t%i\n", ui, NUM_OF_HIGHESTBITbitshift2(ui));
  }

  printf("\n\nPlease wait...\n\n");


  /************************* Simple clock() benchmark ******************/
  start = clock();
  for (ui = 0; ui < LOOPS; ++ui)
    n = NUM_OF_HIGHESTBITmath(ui);
  end = clock();
  printf("math:\t%e\n", (double)(end-start)/CLOCKS_PER_SEC);

  start = clock();
  for (ui = 0; ui < LOOPS; ++ui)
    n = NUM_OF_HIGHESTBITclz(ui);
  end = clock();
  printf("clz:\t%e\n", (double)(end-start)/CLOCKS_PER_SEC);

  start = clock();
  for (ui = 0; ui < LOOPS; ++ui)
    n = NUM_OF_HIGHESTBITi2f(ui);
  end = clock();
  printf("i2f:\t%e\n", (double)(end-start)/CLOCKS_PER_SEC);

  start = clock();
  for (ui = 0; ui < LOOPS; ++ui)
    n = NUM_OF_HIGHESTBITasm(ui);
  end = clock();
  printf("asm:\t%e\n", (double)(end-start)/CLOCKS_PER_SEC);

  start = clock();
  for (ui = 0; ui < LOOPS; ++ui)
    n = NUM_OF_HIGHESTBITbitshift1(ui);
  end = clock();
  printf("bitshift1:\t%e\n", (double)(end-start)/CLOCKS_PER_SEC);

  start = clock();
  for (ui = 0; ui < LOOPS; ++ui)
    n = NUM_OF_HIGHESTBITbitshift2(ui);
  end = clock();
  printf("bitshift2\t%e\n", (double)(end-start)/CLOCKS_PER_SEC);

  printf("\nThe lower, the better. Take note that a negative exponent is good! ;)\n");

  return EXIT_SUCCESS;
}

6

Mặc dù tôi có thể sẽ chỉ sử dụng phương pháp này nếu tôi thực sự yêu cầu hiệu suất tốt nhất có thể (ví dụ: để viết một số loại trò chơi hội đồng AI liên quan đến bitboard), giải pháp hiệu quả nhất là sử dụng ASM nội tuyến. Xem phần Tối ưu của bài đăng trên blog này để biết mã có giải thích.

[...], lệnh hợp bsrlngữ tính toán vị trí của bit quan trọng nhất. Do đó, chúng ta có thể sử dụng asmcâu lệnh này :

asm ("bsrl %1, %0" 
     : "=r" (position) 
     : "r" (number));

Để mở rộng: giải pháp vòng lặp tiêu chuẩn (dịch chuyển sang trái và kiểm tra MSB) có lẽ là dễ đọc nhất. Như trong tất cả các trường hợp liên quan đến việc xoay bit, tốc độ của ASM không thể bị đánh bại, mặc dù không có lý do gì làm xáo trộn mã của bạn trừ khi cần thiết. Hack là một giải pháp xen kẽ - đi theo cách này hay cách khác.
Noldorin

Tôi muốn nói lấy logarit sẽ là một giải pháp hoàn hảo có thể đọc được (kiểm tra asm tạo ra để xem nếu trình biên dịch có thể tối ưu hóa nó để sử dụng hướng dẫn asm này)
jalf

Đôi khi giải pháp ASM nội tuyến chậm hơn, tùy thuộc vào việc triển khai trong vi mã CPU.
rlbond

5
@rlbound: Tôi khó có thể tin được điều đó, mặc dù tôi có thể nhầm. Trên bất kỳ CPU hiện đại người ta sẽ nghĩ rằng nó sẽ được dịch sang một hướng dẫn đơn ....
Noldorin

3
@Noldorin hơi muộn nhưng .. Theo định nghĩa thì đó là một lệnh duy nhất, nhưng nếu nó được mã hóa vi mô như rlbond gợi ý thì lệnh đơn đó có thể giải mã thành một loạt các µops bên trong. Điều đó có xu hướng xảy ra trên vi kiến ​​trúc AMD và Intel Atom, nhưng trên vi kiến ​​trúc Intel bình thường, đó là một hoạt động duy nhất.
harold

4

Tôi cần có một thói quen để làm điều này và trước khi tìm kiếm trên web (và tìm trang này), tôi đã đưa ra giải pháp của riêng mình dựa trên tìm kiếm nhị phân. Mặc dù tôi chắc chắn rằng ai đó đã làm điều này trước đây! Nó chạy trong thời gian liên tục và có thể nhanh hơn so với giải pháp "hiển nhiên" đã đăng, mặc dù tôi không đưa ra bất kỳ tuyên bố lớn nào, chỉ đăng nó vì sự quan tâm.

int highest_bit(unsigned int a) {
  static const unsigned int maskv[] = { 0xffff, 0xff, 0xf, 0x3, 0x1 };
  const unsigned int *mask = maskv;
  int l, h;

  if (a == 0) return -1;

  l = 0;
  h = 32;

  do {
    int m = l + (h - l) / 2;

    if ((a >> m) != 0) l = m;
    else if ((a & (*mask << l)) != 0) h = m;

    mask++;
  } while (l < h - 1);

  return l;
}

4

đó là một số loại tìm kiếm nhị phân, nó hoạt động với tất cả các loại số nguyên (không dấu!)

#include <climits>
#define UINT (unsigned int)
#define UINT_BIT (CHAR_BIT*sizeof(UINT))

int msb(UINT x)
{
    if(0 == x)
        return -1;

    int c = 0;

    for(UINT i=UINT_BIT>>1; 0<i; i>>=1)
    if(static_cast<UINT>(x >> i))
    {
        x >>= i;
        c |= i;
    }

    return c;
}

để hoàn thành:

#include <climits>
#define UINT unsigned int
#define UINT_BIT (CHAR_BIT*sizeof(UINT))

int lsb(UINT x)
{
    if(0 == x)
        return -1;

    int c = UINT_BIT-1;

    for(UINT i=UINT_BIT>>1; 0<i; i>>=1)
    if(static_cast<UINT>(x << i))
    {
        x <<= i;
        c ^= i;
    }

    return c;
}

4
Vui lòng xem xét không sử dụng ALL_CAPS cho typedefs hoặc thực sự là bất cứ điều gì ngoại trừ macro bộ xử lý trước. Đây là một quy ước được chấp nhận rộng rãi.
underscore_d

4

Một số câu trả lời quá phức tạp ở đây. Kỹ thuật Debruin chỉ nên được sử dụng khi đầu vào đã là lũy thừa của hai, nếu không sẽ có một cách tốt hơn. Đối với sức mạnh của 2 đầu vào, Debruin là nhanh nhất tuyệt đối, thậm chí nhanh hơn _BitScanReversebất kỳ bộ xử lý nào mà tôi đã thử nghiệm. Tuy nhiên, trong trường hợp chung,_BitScanReverse (hoặc bất cứ thứ gì nội tại được gọi trong trình biên dịch của bạn) là nhanh nhất (trên một số CPU nhất định, nó có thể được mã hóa vi mô).

Nếu chức năng nội tại không phải là một tùy chọn, thì đây là một giải pháp phần mềm tối ưu để xử lý các đầu vào chung.

u8  inline log2 (u32 val)  {
    u8  k = 0;
    if (val > 0x0000FFFFu) { val >>= 16; k  = 16; }
    if (val > 0x000000FFu) { val >>= 8;  k |= 8;  }
    if (val > 0x0000000Fu) { val >>= 4;  k |= 4;  }
    if (val > 0x00000003u) { val >>= 2;  k |= 2;  }
    k |= (val & 2) >> 1;
    return k;
}

Lưu ý rằng phiên bản này không yêu cầu tra cứu Debruin ở cuối, không giống như hầu hết các câu trả lời khác. Nó tính toán vị trí tại chỗ.

Mặc dù vậy, bảng có thể được ưu tiên hơn, nếu bạn gọi nó liên tục đủ lần, nguy cơ bỏ lỡ bộ nhớ cache sẽ bị lu mờ bởi tốc độ tăng tốc của bảng.

u8 kTableLog2[256] = {
0,0,1,1,2,2,2,2,3,3,3,3,3,3,3,3,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,
5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,
6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,
6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,
7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,
7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,
7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,
7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7
};

u8 log2_table(u32 val)  {
    u8  k = 0;
    if (val > 0x0000FFFFuL) { val >>= 16; k  = 16; }
    if (val > 0x000000FFuL) { val >>=  8; k |=  8; }
    k |= kTableLog2[val]; // precompute the Log2 of the low byte

    return k;
}

Điều này sẽ tạo ra thông lượng cao nhất trong số các câu trả lời phần mềm được đưa ra ở đây, nhưng nếu bạn chỉ thỉnh thoảng gọi nó, hãy thích một giải pháp không cần bảng như đoạn mã đầu tiên của tôi.


1
Một số câu trả lời là không có nhánh, nhưng điều này có thể sẽ được biên dịch với các nhánh có điều kiện. Bạn chỉ định chuẩn lặp lại với cùng một giá trị hay một mẫu đơn giản hay điều gì đó? Nghi ngờ chi nhánh là một kẻ giết người cho hiệu suất. stackoverflow.com/questions/11227809/…
Peter Cordes

3

Như các câu trả lời ở trên đã chỉ ra, có một số cách để xác định bit quan trọng nhất. Tuy nhiên, như cũng đã chỉ ra, các phương thức có thể là duy nhất cho thanh ghi 32bit hoặc 64bit. Các trang bithacks stanford.edu cung cấp giải pháp mà làm việc cho cả 32bit và 64bit máy tính. Với một chút công việc, chúng có thể được kết hợp để cung cấp một phương pháp tiếp cận đa kiến ​​trúc vững chắc để lấy MSB. Giải pháp mà tôi đã đến để biên dịch / hoạt động trên các máy tính 64 & 32 bit là:

#if defined(__LP64__) || defined(_LP64)
# define BUILD_64   1
#endif

#include <stdio.h>
#include <stdint.h>  /* for uint32_t */

/* CHAR_BIT  (or include limits.h) */
#ifndef CHAR_BIT
#define CHAR_BIT  8
#endif  /* CHAR_BIT */

/* 
 * Find the log base 2 of an integer with the MSB N set in O(N)
 * operations. (on 64bit & 32bit architectures)
 */
int
getmsb (uint32_t word)
{
    int r = 0;
    if (word < 1)
        return 0;
#ifdef BUILD_64
    union { uint32_t u[2]; double d; } t;  // temp
    t.u[__FLOAT_WORD_ORDER==LITTLE_ENDIAN] = 0x43300000;
    t.u[__FLOAT_WORD_ORDER!=LITTLE_ENDIAN] = word;
    t.d -= 4503599627370496.0;
    r = (t.u[__FLOAT_WORD_ORDER==LITTLE_ENDIAN] >> 20) - 0x3FF;
#else
    while (word >>= 1)
    {
        r++;
    }
#endif  /* BUILD_64 */
    return r;
}

Không phải là int r; ban đầu được xác định trên #ifdef BUILD_64lá cờ? Trong trường hợp đó, nó sẽ không cần xác định lại trong điều kiện.
David C. Rankin

3

Một phiên bản trong C sử dụng phép gần đúng liên tiếp:

unsigned int getMsb(unsigned int n)
{
  unsigned int msb  = sizeof(n) * 4;
  unsigned int step = msb;
  while (step > 1)
 {
    step /=2;
    if (n>>msb)
     msb += step;
   else
     msb -= step;
 }
  if (n>>msb)
    msb++;
  return (msb - 1);
}

Ưu điểm: thời gian chạy không đổi bất kể số lượng được cung cấp, vì số lượng vòng lặp luôn bằng nhau. (4 vòng khi sử dụng "unsigned int")


Nếu bạn viết nó bằng toán tử bậc ba ( msb += (n>>msb) ? step : -step;), nhiều trình biên dịch có khả năng làm cho asm không phân nhánh, tránh sai sót về nhánh ở mỗi bước ( stackoverflow.com/questions/11227809/… ).
Peter Cordes

3

Tôi biết câu hỏi này rất cũ, nhưng chỉ cần tự mình triển khai một hàm msb () , tôi thấy rằng hầu hết các giải pháp được trình bày ở đây và trên các trang web khác không nhất thiết là hiệu quả nhất - ít nhất là đối với định nghĩa của cá nhân tôi về hiệu quả (xem thêm Cập nhật bên dưới ). Đây là lý do tại sao:

Hầu hết các giải pháp (đặc biệt là những giải pháp sử dụng một số loại lược đồ tìm kiếm nhị phân hoặc cách tiếp cận ngây thơ quét tuyến tính từ phải sang trái) dường như bỏ qua thực tế rằng đối với các số nhị phân tùy ý, không có nhiều giải pháp bắt đầu bằng một chuỗi rất dài số không. Trên thực tế, đối với bất kỳ chiều rộng bit nào, một nửa số nguyên bắt đầu bằng 1 và một phần tư trong số chúng bắt đầu bằng 01 . Xem tôi đang ở đâu? Lập luận của tôi là quá trình quét tuyến tính bắt đầu từ vị trí bit quan trọng nhất đến ít quan trọng nhất (từ trái sang phải) không quá "tuyến tính" như thoạt nhìn có thể giống như vậy.

Có thể chỉ ra 1 , rằng đối với bất kỳ độ rộng bit nào, số bit trung bình cần được kiểm tra nhiều nhất là 2. Điều này có nghĩa là độ phức tạp thời gian được phân bổ theo thời gian là O (1) đối với số bit (!) .

Tất nhiên, trường hợp xấu nhất vẫn là O (n) , tệ hơn là O (log (n)) mà bạn nhận được với các phương pháp tiếp cận giống như tìm kiếm nhị phân, nhưng vì có rất ít trường hợp xấu nhất nên chúng không đáng kể đối với hầu hết các ứng dụng ( Cập nhật : not khá: Có thể có ít, nhưng chúng có thể xảy ra với xác suất cao - xem Cập nhật bên dưới).

Đây là cách tiếp cận "ngây thơ" mà tôi đã đưa ra, ít nhất trên máy của tôi đánh bại hầu hết các cách tiếp cận khác (các lược đồ tìm kiếm nhị phân cho int 32 bit luôn yêu cầu log 2 (32) = 5 bước, trong khi thuật toán ngớ ngẩn này yêu cầu ít hơn trung bình hơn 2) - xin lỗi vì đây là C ++ và không phải C thuần túy:

template <typename T>
auto msb(T n) -> int
{
    static_assert(std::is_integral<T>::value && !std::is_signed<T>::value,
        "msb<T>(): T must be an unsigned integral type.");

    for (T i = std::numeric_limits<T>::digits - 1, mask = 1 << i; i >= 0; --i, mask >>= 1)
    {
        if ((n & mask) != 0)
            return i;
    }

    return 0;
}

Cập nhật : Mặc dù những gì tôi đã viết ở đây hoàn toàn đúng với các số nguyên tùy ý , trong đó mọi sự kết hợp của các bit đều có thể xảy ra như nhau (kiểm tra tốc độ của tôi chỉ đơn giản là đo khoảng thời gian để xác định MSB cho). Trong trường hợp này, giải pháp của tôi thực sự sẽ hoạt động kém hơn so với cách tiếp cận tìm kiếm nhị phân - vì vậy cách tiếp cận sau có lẽ nên được ưu tiên hơn, mặc dù giải pháp của tôi sẽ lặp lại nhanh hơn qua tất cả các số nguyên. TL; DR: Các số nguyên trong đời thực có thể sẽ có xu hướng đối với trường hợp xấu nhất của thuật toán đơn giản này, điều này sẽ làm cho nó hoạt động kém hơn cuối cùng - mặc dù thực tế là nó được khấu hao tất cả các số nguyên 32 bit), số nguyên trong cuộc sống thực, cho mà một hàm như vậy sẽ được gọi, thường tuân theo một mẫu khác: Ví dụ: trong mã của tôi, hàm này được sử dụng để xác định xem kích thước đối tượng là lũy thừa của 2 hay để tìm lũy thừa tiếp theo của 2 lớn hơn hoặc bằng kích thước đối tượng . Tôi đoán rằng hầu hết các ứng dụng sử dụng MSB đều liên quan đến các số nhỏ hơn nhiều so với số tối đa mà một số nguyên có thể biểu diễn (kích thước đối tượng hiếm khi sử dụng tất cả các bit trong một size_t
O (1) cho các số nguyên thực sự tùy ý.

1 Đối số như sau (bản nháp thô): Gọi n là số bit (chiều rộng bit). Có tổng cộng 2 n số nguyên có thể được biểu diễn bằng n bit. Có 2 n - 1 số nguyên bắt đầu bằng 1 ( 1 đầu tiên là cố định, n - 1 bit còn lại có thể là bất kỳ thứ gì). Những số nguyên đó chỉ yêu cầu một số nguyên của vòng lặp để xác định MSB. Hơn nữa, có 2 n - 2 số nguyên bắt đầu bằng 01 , yêu cầu 2 lần lặp, 2 n - 3 số nguyên bắt đầu bằng 001 , yêu cầu 3 lần lặp, v.v.

Nếu chúng ta tính tổng tất cả các lần lặp cần thiết cho tất cả các số nguyên có thể và chia chúng cho 2 n , tổng số các số nguyên, chúng ta sẽ nhận được số lần lặp trung bình cần thiết để xác định MSB cho số nguyên n -bit:

(1 * 2 n - 1 + 2 * 2 n - 2 + 3 * 2 n - 3 + ... + n) / 2 n

Chuỗi số lần lặp trung bình này thực sự là hội tụ và có giới hạn là 2 đối với n về phía vô hạn

Do đó, thuật toán từ trái sang phải ngây thơ thực sự có độ phức tạp theo thời gian không đổi được khấu haoO (1) cho bất kỳ số bit nào.


2
Tôi không nghĩ rằng đó nhất thiết phải là một giả định công bằng rằng các đầu vào cho các hàm msb có xu hướng được phân phối đồng đều. Trong thực tế, các đầu vào này có xu hướng là thanh ghi ngắt hoặc bảng bit hoặc một số cấu trúc dữ liệu khác với các giá trị được phân phối không đồng đều. Đối với một tiêu chuẩn công bằng, tôi nghĩ sẽ an toàn hơn nếu giả định rằng các đầu ra (không phải đầu vào) sẽ được phân bổ đồng đều.
johnwbyrd

3

đã cho chúng tôi log2. Điều này loại bỏ sự cần thiết của tất cả các log2triển khai nước sốt đặc biệt mà bạn thấy trên trang này. Bạn có thể sử dụng cách log2triển khai của tiêu chuẩn như sau:

const auto n = 13UL;
const auto Index = (unsigned long)log2(n);

printf("MSB is: %u\n", Index); // Prints 3 (zero offset)

Một ntrong những cũng 0ULcần phải được đề phòng, bởi vì:

-∞ được trả lại và FE_DIVBYZERO được nâng lên

Tôi đã viết một ví dụ với kiểm tra rằng bộ tùy tiện Indexđể ULONG_MAXở đây: https://ideone.com/u26vsi


Các hệ quả cho câu trả lời duy nhất của gcc là:

const auto n = 13UL;
unsigned long Index;

_BitScanReverse(&Index, n);
printf("MSB is: %u\n", Index); // Prints 3 (zero offset)

Tài liệu cho _BitScanReverse các trạng thái đó Indexlà:

Đã tải với vị trí bit của bit đặt đầu tiên (1) được tìm thấy

Trên thực tế tôi đã tìm thấy rằng nếu n0ULđó Indexđược thiết lập để0UL , cũng giống như nó sẽ là một ntrong 1UL. Nhưng điều duy nhất được bảo đảm trong các tài liệu trong trường hợp của một nsố0UL là sự trở lại là:

0 nếu không tìm thấy bit đặt

Do đó, tương tự như việc log2triển khai thích hợp ở trên, trả về nên được kiểm tra cài đặt Indexthành giá trị được gắn cờ trong trường hợp này. Tôi lại viết một ví dụ về cách sử dụng ULONG_MAXcho giá trị cờ này tại đây: http://rextester.com/GCU61409


Không, _BitScanReversetrả về 0 chỉ nếu đầu vào là 0. Đây giống như lệnh của x86, BSRchỉ đặt ZF dựa trên đầu vào chứ không phải đầu ra. Điều thú vị là MS nói các tài liệu là indexkhông được đặt khi không 1tìm thấy bit nào ; phù hợp với hành vi asm x86 của bsr. (AMD ghi lại nó là để thanh ghi đích không được sửa đổi trên src = 0, nhưng Intel chỉ nói đầu ra không xác định mặc dù CPU của họ thực hiện hành vi không sửa đổi.) Điều này không giống như x86 lzcnt, cho phép 32không tìm thấy.
Peter Cordes

@PeterCordes _BitScanReversesử dụng lập chỉ mục dựa trên 0, do đó nếu nlà 1 thì chỉ số của bit đặt trên thực tế là 0. Thật không may, như bạn nói nếu nlà 0 thì đầu ra cũng là 0 :( Điều này có nghĩa là không có cách nào để sử dụng trả về phân biệt giữa một ntrong tổng số 1 hoặc 0. đó là những gì tôi đã cố gắng để giao tiếp bạn có nghĩ rằng có một cách tốt hơn để nói điều này.?
Jonathan Mee

Tôi nghĩ bạn đang nói về cách nó thiết lập Index. Đó không phải là giá trị trả lại . Nó trả về một boolean sai nếu đầu vào là 0 (và đây là lý do tại sao Chỉ mục được chuyển bằng tham chiếu thay vì được trả về bình thường). godbolt.org/g/gQKJdE . Và tôi đã kiểm tra: mặc dù có từ ngữ trong tài liệu của MS, _BitScanReversenhưng không để Index không được bật n==0: bạn chỉ nhận được bất kỳ giá trị nào trong sổ đăng ký mà nó đã sử dụng. (Trong trường hợp của bạn có thể là cùng một sổ đăng ký mà nó được sử dụng Indexsau này, dẫn đến việc bạn thấy a 0).
Peter Cordes

Câu hỏi này không được gắn thẻ c ++.
technosaurus

@technosaurus Cảm ơn, tôi đã quên bản thân mình. Cho rằng câu hỏi là C, chúng tôi đã thực sự có log2từ C99.
Jonathan Mee

2

Hãy suy nghĩ các toán tử bitwise.

Tôi đã hiểu sai câu hỏi lần đầu tiên. Bạn nên tạo ra một int với tập bit ngoài cùng bên trái (các bit khác bằng 0). Giả sử cmp được đặt thành giá trị đó:

position = sizeof(int)*8
while(!(n & cmp)){ 
   n <<=1;
   position--;
}

Ý bạn là gì khi chuyển đổi thành một chuỗi? Định nghĩa của ffs nhận một int và trả về một int. Chuyển đổi sẽ ở đâu? Và mục đích chuyển đổi sẽ phục vụ nếu chúng ta đang tìm kiếm các bit trong một từ?
dreamlax

Tôi không biết về chức năng đó.
Vasil

Điều 8nên được CHAR_BIT. Đây rất khó có thể là cách nhanh nhất, bởi vì việc kiểm tra sai nhánh sẽ xảy ra khi thoát khỏi vòng lặp trừ khi điều này được sử dụng lặp lại với cùng một đầu vào. Ngoài ra, đối với các đầu vào nhỏ (nhiều số không), nó phải lặp lại rất nhiều. Điều này giống như cách dự phòng mà bạn sẽ sử dụng làm phiên bản dễ xác minh trong thử nghiệm đơn vị để so sánh với các phiên bản được tối ưu hóa.
Peter Cordes

2

Mở rộng trên điểm chuẩn của Josh ... người ta có thể cải thiện clz như sau

/***************** clz2 ********************/

#define NUM_OF_HIGHESTBITclz2(a) ((a)                              \
                  ? (((1U) << (sizeof(unsigned)*8-1)) >> __builtin_clz(a)) \
                  : 0)

Về asm: lưu ý là có bsr và bsrl (đây là bản "dài"). bình thường có thể nhanh hơn một chút.


1

Lưu ý rằng những gì bạn đang cố gắng làm là tính toán số nguyên log2 của một số nguyên,

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

unsigned int
Log2(unsigned long x)
{
    unsigned long n = x;
    int bits = sizeof(x)*8;
    int step = 1; int k=0;
    for( step = 1; step < bits; ) {
        n |= (n >> step);
        step *= 2; ++k;
    }
    //printf("%ld %ld\n",x, (x - (n >> 1)) );
    return(x - (n >> 1));
}

Quan sát rằng bạn có thể cố gắng tìm kiếm nhiều hơn 1 bit cùng một lúc.

unsigned int
Log2_a(unsigned long x)
{
    unsigned long n = x;
    int bits = sizeof(x)*8;
    int step = 1;
    int step2 = 0;
    //observe that you can move 8 bits at a time, and there is a pattern...
    //if( x>1<<step2+8 ) { step2+=8;
        //if( x>1<<step2+8 ) { step2+=8;
            //if( x>1<<step2+8 ) { step2+=8;
            //}
        //}
    //}
    for( step2=0; x>1L<<step2+8; ) {
        step2+=8;
    }
    //printf("step2 %d\n",step2);
    for( step = 0; x>1L<<(step+step2); ) {
        step+=1;
        //printf("step %d\n",step+step2);
    }
    printf("log2(%ld) %d\n",x,step+step2);
    return(step+step2);
}

Cách tiếp cận này sử dụng tìm kiếm nhị phân

unsigned int
Log2_b(unsigned long x)
{
    unsigned long n = x;
    unsigned int bits = sizeof(x)*8;
    unsigned int hbit = bits-1;
    unsigned int lbit = 0;
    unsigned long guess = bits/2;
    int found = 0;

    while ( hbit-lbit>1 ) {
        //printf("log2(%ld) %d<%d<%d\n",x,lbit,guess,hbit);
        //when value between guess..lbit
        if( (x<=(1L<<guess)) ) {
           //printf("%ld < 1<<%d %ld\n",x,guess,1L<<guess);
            hbit=guess;
            guess=(hbit+lbit)/2;
            //printf("log2(%ld) %d<%d<%d\n",x,lbit,guess,hbit);
        }
        //when value between hbit..guess
        //else
        if( (x>(1L<<guess)) ) {
            //printf("%ld > 1<<%d %ld\n",x,guess,1L<<guess);
            lbit=guess;
            guess=(hbit+lbit)/2;
            //printf("log2(%ld) %d<%d<%d\n",x,lbit,guess,hbit);
        }
    }
    if( (x>(1L<<guess)) ) ++guess;
    printf("log2(x%ld)=r%d\n",x,guess);
    return(guess);
}

Một phương pháp tìm kiếm nhị phân khác, có lẽ dễ đọc hơn,

unsigned int
Log2_c(unsigned long x)
{
    unsigned long v = x;
    unsigned int bits = sizeof(x)*8;
    unsigned int step = bits;
    unsigned int res = 0;
    for( step = bits/2; step>0; )
    {
        //printf("log2(%ld) v %d >> step %d = %ld\n",x,v,step,v>>step);
        while ( v>>step ) {
            v>>=step;
            res+=step;
            //printf("log2(%ld) step %d res %d v>>step %ld\n",x,step,res,v);
        }
        step /= 2;
    }
    if( (x>(1L<<res)) ) ++res;
    printf("log2(x%ld)=r%ld\n",x,res);
    return(res);
}

Và bởi vì bạn sẽ muốn kiểm tra những điều này,

int main()
{
    unsigned long int x = 3;
    for( x=2; x<1000000000; x*=2 ) {
        //printf("x %ld, x+1 %ld, log2(x+1) %d\n",x,x+1,Log2(x+1));
        printf("x %ld, x+1 %ld, log2_a(x+1) %d\n",x,x+1,Log2_a(x+1));
        printf("x %ld, x+1 %ld, log2_b(x+1) %d\n",x,x+1,Log2_b(x+1));
        printf("x %ld, x+1 %ld, log2_c(x+1) %d\n",x,x+1,Log2_c(x+1));
    }
    return(0);
}

1

Đưa điều này vào vì nó là một cách tiếp cận 'chưa khác', có vẻ khác với những cách khác đã được đưa ra.

trả về -1nếu x==0, nếu không floor( log2(x)) (kết quả tối đa 31)

Giảm vấn đề từ 32 xuống 4 bit, sau đó sử dụng một bảng. Có lẽ không lịch sự, nhưng thực dụng.

Đây là những gì tôi sử dụng khi tôi không muốn sử dụng __builtin_clzvì các vấn đề về tính di động.

Để làm cho nó nhỏ gọn hơn, thay vào đó, người ta có thể sử dụng một vòng lặp để giảm bớt, thêm 4 thành r mỗi lần, tối đa 7 lần lặp. Hoặc một số kết hợp, chẳng hạn như (cho 64 bit): vòng lặp để giảm xuống 8, kiểm tra để giảm xuống 4.

int log2floor( unsigned x ){
   static const signed char wtab[16] = {-1,0,1,1, 2,2,2,2, 3,3,3,3,3,3,3,3};
   int r = 0;
   unsigned xk = x >> 16;
   if( xk != 0 ){
       r = 16;
       x = xk;
   }
   // x is 0 .. 0xFFFF
   xk = x >> 8;
   if( xk != 0){
       r += 8;
       x = xk;
   }
   // x is 0 .. 0xFF
   xk = x >> 4;
   if( xk != 0){
       r += 4;
       x = xk;
   }
   // now x is 0..15; x=0 only if originally zero.
   return r + wtab[x];
}

1

Woaw, đó là nhiều câu trả lời. Tôi không xin lỗi vì đã trả lời một câu hỏi cũ.

int result = 0;//could be a char or int8_t instead
if(value){//this assumes the value is 64bit
    if(0xFFFFFFFF00000000&value){  value>>=(1<<5); result|=(1<<5);  }//if it is 32bit then remove this line
    if(0x00000000FFFF0000&value){  value>>=(1<<4); result|=(1<<4);  }//and remove the 32msb
    if(0x000000000000FF00&value){  value>>=(1<<3); result|=(1<<3);  }
    if(0x00000000000000F0&value){  value>>=(1<<2); result|=(1<<2);  }
    if(0x000000000000000C&value){  value>>=(1<<1); result|=(1<<1);  }
    if(0x0000000000000002&value){  result|=(1<<0);  }
}else{
  result=-1;
}

Câu trả lời này khá giống với một câu trả lời khác ... ồ ồ.


Viết số tiền dịch chuyển như 1<<klà một liên lạc tốt đẹp. Còn những chiếc mặt nạ thì sao? (1 << (1<<k-1)-1<< (1<<k-1)? ( most optimal? Bạn so sánh bậc nhất?)
lọ

@greybeard Nếu bạn xem các chỉnh sửa của câu hỏi này, bạn sẽ thấy khi tôi thêm phần "tối ưu". Tôi đã quên xóa nó khi tôi thay đổi câu trả lời của mình. Ngoài ra tôi không chắc chắn lý do tại sao bạn đang đang nói về những mặt nạ? (? Có gì mặt nạ Tôi không theo dõi bạn)
Harry Svensson

( (Bit) mặt nạ là những giá trị sử dụng để chọn / bit rõ ràng một cách chọn lọc / sử dụng trong &&~.) Bạn có thể thay thế các hằng số hex bởi những cái tên như ((type)1<<(1<<k))-1<<(1<<k).
greybeard,

Ồ đúng rồi, tôi đang dùng mặt nạ, tôi hoàn toàn quên mất điều đó. Tôi đã trả lời câu này vài tháng trước ... - Hmmm, vì nó được đánh giá trong thời gian biên dịch, tôi nói nó tương đương với các giá trị hex. Tuy nhiên, một là khó hiểu và một là hệ thập lục phân.
Harry Svensson

0

Mật mã:

    // x>=1;
    unsigned func(unsigned x) {
    double d = x ;
    int p= (*reinterpret_cast<long long*>(&d) >> 52) - 1023;
    printf( "The left-most non zero bit of %d is bit %d\n", x, p);
    }

Hoặc lấy phần nguyên của lệnh FPU FYL2X (Y * Log2 X) bằng cách đặt Y = 1


uhhhhh. gì? chức năng này như thế nào? nó có di động theo cách nào không?
underscore_d

Các mã trong cửa sổ có tính di động. Hàm FYL2X () là một lệnh fpu, nhưng có thể được chuyển và có thể được tìm thấy trong một số thư viện FPU / math.
jemin

@underscore_d Nó hoạt động vì các số dấu phẩy động được chuẩn hóa ... chuyển đổi sang dịch chuyển kép các bit định trị để loại bỏ các số 0 ở đầu và mã này trích xuất số mũ và điều chỉnh nó để xác định số bit được dịch chuyển. Nó chắc chắn không độc lập với kiến ​​trúc, nhưng nó có thể sẽ hoạt động trên bất kỳ máy nào bạn gặp.
Jim Balter

Đây là phiên bản thay thế của câu trả lời này , hãy xem ở đó để biết nhận xét về hiệu suất và tính di động. (Cụ thể là tính không di động của đúc con trỏ đối với kiểu-punning.) Nó sử dụng toán học địa chỉ để chỉ tải lại 32 bit cao của doubletệp, điều này có thể tốt nếu nó thực sự lưu trữ / tải lại thay vì kiểu chơi chữ theo cách khác, ví dụ: với một movqhướng dẫn như bạn có thể nhận được ở đây trên x86.
Peter Cordes

Cũng lưu ý [nhận xét cho câu trả lời đó] của tôi, nơi tôi đưa ra cảnh báo nghiêm trọng rằng phương pháp này đưa ra câu trả lời sai cho các giá trị trong (ít nhất) phạm vi [7FFFFFFFFFFFFE00 - 7FFFFFFFFFFFFFFF].
Glenn Slayden

0

Một người đăng khác đã cung cấp một bảng tra cứu bằng cách sử dụng tra cứu toàn byte . Trong trường hợp bạn muốn tăng hiệu suất hơn một chút (với chi phí 32K bộ nhớ thay vì chỉ 256 mục tra cứu), đây là một giải pháp sử dụng bảng tra cứu 15 bit , trong C # 7 cho .NET .

Phần thú vị là khởi tạo bảng. Vì đó là một khối tương đối nhỏ mà chúng tôi muốn trong suốt thời gian tồn tại của quá trình, tôi phân bổ bộ nhớ không được quản lý cho khối này bằng cách sử dụng Marshal.AllocHGlobal. Như bạn có thể thấy, để có hiệu suất tối đa, toàn bộ ví dụ được viết dưới dạng gốc:

readonly static byte[] msb_tab_15;

// Initialize a table of 32768 bytes with the bit position (counting from LSB=0)
// of the highest 'set' (non-zero) bit of its corresponding 16-bit index value.
// The table is compressed by half, so use (value >> 1) for indexing.
static MyStaticInit()
{
    var p = new byte[0x8000];

    for (byte n = 0; n < 16; n++)
        for (int c = (1 << n) >> 1, i = 0; i < c; i++)
            p[c + i] = n;

    msb_tab_15 = p;
}

Bảng yêu cầu khởi tạo một lần qua đoạn mã trên. Nó ở chế độ chỉ đọc nên một bản sao chung duy nhất có thể được chia sẻ để truy cập đồng thời. Với bảng này, bạn có thể nhanh chóng tra cứu nhật ký số nguyên 2 , đó là những gì chúng tôi đang tìm kiếm ở đây, cho tất cả các độ rộng số nguyên khác nhau (8, 16, 32 và 64 bit).

Lưu ý rằng mục nhập bảng 0, số nguyên duy nhất mà khái niệm 'bit đặt cao nhất' không được xác định, được cung cấp giá trị -1. Sự phân biệt này là cần thiết để xử lý thích hợp các từ trên có giá trị 0 trong đoạn mã dưới đây. Không cần thêm lời khuyên nào nữa, đây là mã cho từng số nguyên gốc khác nhau:

Phiên bản ulong (64-bit)

/// <summary> Index of the highest set bit in 'v', or -1 for value '0' </summary>
public static int HighestOne(this ulong v)
{
    if ((long)v <= 0)
        return (int)((v >> 57) & 0x40) - 1;      // handles cases v==0 and MSB==63

    int j = /**/ (int)((0xFFFFFFFFU - v /****/) >> 58) & 0x20;
    j |= /*****/ (int)((0x0000FFFFU - (v >> j)) >> 59) & 0x10;
    return j + msb_tab_15[v >> (j + 1)];
}

Phiên bản uint (32-bit)

/// <summary> Index of the highest set bit in 'v', or -1 for value '0' </summary>
public static int HighestOne(uint v)
{
    if ((int)v <= 0)
        return (int)((v >> 26) & 0x20) - 1;     // handles cases v==0 and MSB==31

    int j = (int)((0x0000FFFFU - v) >> 27) & 0x10;
    return j + msb_tab_15[v >> (j + 1)];
}

Quá tải khác nhau cho những điều trên

public static int HighestOne(long v) => HighestOne((ulong)v);
public static int HighestOne(int v) => HighestOne((uint)v);
public static int HighestOne(ushort v) => msb_tab_15[v >> 1];
public static int HighestOne(short v) => msb_tab_15[(ushort)v >> 1];
public static int HighestOne(char ch) => msb_tab_15[ch >> 1];
public static int HighestOne(sbyte v) => msb_tab_15[(byte)v >> 1];
public static int HighestOne(byte v) => msb_tab_15[v >> 1];

Đây là một giải pháp hoàn chỉnh, hoạt động, thể hiện hiệu suất tốt nhất trên .NET 4.7.2 cho nhiều lựa chọn thay thế mà tôi đã so sánh với khai thác kiểm tra hiệu suất chuyên dụng. Một số trong số này được đề cập dưới đây. Các tham số thử nghiệm là mật độ đồng nhất của tất cả các vị trí 65 bit, tức là, giá trị cộng 0 ... 31/630 (tạo ra kết quả -1). Các bit bên dưới vị trí chỉ mục đích được điền ngẫu nhiên. Các thử nghiệm chỉ là x64 , chế độ phát hành, với tính năng tối ưu hóa JIT được bật.




Đó là phần cuối của câu trả lời chính thức của tôi ở đây; sau đây là một số ghi chú thông thường và liên kết đến mã nguồn cho các ứng cử viên thử nghiệm thay thế được liên kết với thử nghiệm tôi đã chạy để xác thực hiệu suất và tính đúng đắn của mã trên.


Phiên bản được cung cấp ở trên, được mã hóa là Tab16A là phiên bản chiến thắng nhất quán qua nhiều lần chạy. Các ứng viên khác nhau này, ở dạng làm việc tích cực / dạng cào, có thể được tìm thấy tại đây , tại đâytại đây .

 1 ứng cử viên.HighestOne_Tab16A 622.496
 2 ứng cử viên.HighestOne_Tab16C 628,234
 3 ứng cử viên.HighestOne_Tab8A 649,146
 4 ứng cử viên.HighestOne_Tab8B 656,847
 5 ứng cử viên.HighestOne_Tab16B 657,147
 6 ứng cử viên.HighestOne_Tab16D 659,650
 7 _highest_one_bit_UNMANAGED.HighestOne_U 702,900
 8 de_Bruijn.IndexOfMSB 709,672
 9 _old_2.HighestOne_Old2 715,810
10 _test_A.HighestOne8 757,188
11 _old_1.HighestOne_Old1 757,925
12 _test_A.HighestOne5 (không an toàn) 760,387
13 _test_B.HighestOne8 (không an toàn) 763.904
14 _test_A.HighestOne3 (không an toàn) 766,433
15 _test_A.HighestOne1 (không an toàn) 767,321
16 _test_A.HighestOne4 (không an toàn) 771.702
17 _test_B.HighestOne2 (không an toàn) 772,136
18 _test_B.HighestOne1 (không an toàn) 772,527
19 _test_B.HighestOne3 (không an toàn) 774,140
20 _test_A.HighestOne7 (không an toàn) 774,581
21 _test_B.HighestOne7 (không an toàn) 775,463
22 _test_A.HighestOne2 (không an toàn) 776,865
23 ứng cử viên.HighestOne_NoTab 777,698
24 _test_B.HighestOne6 (không an toàn) 779,481
25 _test_A.HighestOne6 (không an toàn) 781,553
26 _test_B.HighestOne4 (không an toàn) 785,504
27 _test_B.HighestOne5 (không an toàn) 789,797
28 _test_A.HighestOne0 (không an toàn) 809,566
29 _test_B.HighestOne0 (không an toàn) 814,990
30 _highest_one_bit.HighestOne 824.345
30 _bitarray_ext.RtlFindMostSignificantBit 894.069
31 ứng cử viên.HighestOne_Naive 898,865

Đáng chú ý là hiệu suất khủng khiếp của ntdll.dll!RtlFindMostSignificantBitthông qua P / Invoke:

[DllImport("ntdll.dll"), SuppressUnmanagedCodeSecurity, SecuritySafeCritical]
public static extern int RtlFindMostSignificantBit(ulong ul);

Nó thực sự quá tệ, bởi vì đây là toàn bộ chức năng thực tế:

    RtlFindMostSignificantBit:
        bsr rdx, rcx  
        mov eax,0FFFFFFFFh  
        movzx ecx, dl  
        cmovne      eax,ecx  
        ret

Tôi không thể tưởng tượng được hiệu suất kém bắt nguồn từ năm dòng này, vì vậy các hình phạt chuyển đổi được quản lý / bản địa phải là nguyên nhân. Tôi cũng ngạc nhiên rằng thử nghiệm thực sự ưu tiên các shortbảng tra cứu trực tiếp 32KB (và 64KB) (16-bit) hơn các bảng tra cứu 128 byte (và 256-byte) byte(8-bit). Tôi nghĩ phần sau sẽ cạnh tranh hơn với các tra cứu 16 bit, nhưng phần sau luôn làm tốt hơn điều này:

public static int HighestOne_Tab8A(ulong v)
{
    if ((long)v <= 0)
        return (int)((v >> 57) & 64) - 1;

    int j;
    j =  /**/ (int)((0xFFFFFFFFU - v) >> 58) & 32;
    j += /**/ (int)((0x0000FFFFU - (v >> j)) >> 59) & 16;
    j += /**/ (int)((0x000000FFU - (v >> j)) >> 60) & 8;
    return j + msb_tab_8[v >> j];
}

Điều cuối cùng tôi sẽ chỉ ra là tôi khá sốc khi phương pháp deBruijn của tôi không tốt hơn. Đây là phương pháp mà trước đây tôi đã sử dụng phổ biến:

const ulong N_bsf64 = 0x07EDD5E59A4E28C2,
            N_bsr64 = 0x03F79D71B4CB0A89;

readonly public static sbyte[]
bsf64 =
{
    63,  0, 58,  1, 59, 47, 53,  2, 60, 39, 48, 27, 54, 33, 42,  3,
    61, 51, 37, 40, 49, 18, 28, 20, 55, 30, 34, 11, 43, 14, 22,  4,
    62, 57, 46, 52, 38, 26, 32, 41, 50, 36, 17, 19, 29, 10, 13, 21,
    56, 45, 25, 31, 35, 16,  9, 12, 44, 24, 15,  8, 23,  7,  6,  5,
},
bsr64 =
{
     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,
};

public static int IndexOfLSB(ulong v) =>
    v != 0 ? bsf64[((v & (ulong)-(long)v) * N_bsf64) >> 58] : -1;

public static int IndexOfMSB(ulong v)
{
    if ((long)v <= 0)
        return (int)((v >> 57) & 64) - 1;

    v |= v >> 1; v |= v >> 2;  v |= v >> 4;   // does anybody know a better
    v |= v >> 8; v |= v >> 16; v |= v >> 32;  // way than these 12 ops?
    return bsr64[(v * N_bsr64) >> 58];
}

Có nhiều cuộc thảo luận về các phương pháp deBruijn ưu việt và tuyệt vời như thế nào trong câu hỏi SO này , và tôi đã có xu hướng đồng ý. Suy đoán của tôi là, trong khi cả phương pháp bảng tra cứu deBruijn và trực tiếp (mà tôi thấy là nhanh nhất) đều phải thực hiện tra cứu bảng và cả hai đều có sự phân nhánh rất tối thiểu, chỉ deBruijn có hoạt động nhân 64 bit. Tôi chỉ thử nghiệm các IndexOfMSBchức năng ở đây - không phải deBruijn - IndexOfLSBnhưng tôi hy vọng cái sau sẽ có cơ hội tốt hơn nhiều vì nó có rất nhiều thao tác hơn (xem ở trên) và tôi có thể sẽ tiếp tục sử dụng nó cho LSB.


1
Bộ nhớ đệm L1D trên các CPU x86 hiện đại chỉ là 32kiB. Một LUT lớn có khả năng kém hơn một LUT nhỏ trừ khi bạn đang sử dụng các giá trị giống nhau nhiều lần. Nếu không, bạn sẽ thường xuyên bị bỏ lỡ bộ nhớ cache.
Peter Cordes

0

Phương pháp khiêm tốn của tôi rất đơn giản:

MSB (x) = INT [Nhật ký (x) / Nhật ký (2)]

Dịch: MSB của x là giá trị nguyên của (Bản ghi của cơ sở x chia cho Bản ghi của cơ sở 2).

Điều này có thể dễ dàng và nhanh chóng được điều chỉnh cho bất kỳ ngôn ngữ lập trình nào. Hãy thử nó trên máy tính của bạn để tự mình thấy rằng nó hoạt động.


Điều đó hoạt động nếu tất cả những gì bạn quan tâm là hiệu quả của nhà phát triển. Nếu bạn muốn hiệu quả thời gian chạy, bạn cần có thuật toán thay thế.
Mikko Rantalainen

Điều này có thể không thành công do lỗi làm tròn. Ví dụ: trong CPython 2 và 3, int(math.log((1 << 48) - 1) / math.log(2))là 48.
benrg

0

Đây là một giải pháp nhanh cho C hoạt động trong GCCClang ; sẵn sàng để được sao chép và dán.

#include <limits.h>

unsigned int fls(const unsigned int value)
{
    return (unsigned int)1 << ((sizeof(unsigned int) * CHAR_BIT) - __builtin_clz(value) - 1);
}

unsigned long flsl(const unsigned long value)
{
    return (unsigned long)1 << ((sizeof(unsigned long) * CHAR_BIT) - __builtin_clzl(value) - 1);
}

unsigned long long flsll(const unsigned long long value)
{
    return (unsigned long long)1 << ((sizeof(unsigned long long) * CHAR_BIT) - __builtin_clzll(value) - 1);
}

Và một phiên bản cải tiến nhỏ cho C ++ .

#include <climits>

constexpr unsigned int fls(const unsigned int value)
{
    return (unsigned int)1 << ((sizeof(unsigned int) * CHAR_BIT) - __builtin_clz(value) - 1);
}

constexpr unsigned long fls(const unsigned long value)
{
    return (unsigned long)1 << ((sizeof(unsigned long) * CHAR_BIT) - __builtin_clzl(value) - 1);
}

constexpr unsigned long long fls(const unsigned long long value)
{
    return (unsigned long long)1 << ((sizeof(unsigned long long) * CHAR_BIT) - __builtin_clzll(value) - 1);
}

Mã giả định rằng valuesẽ không 0. Nếu bạn muốn cho phép 0, bạn cần phải sửa đổi nó.


0

Tôi giả sử câu hỏi của bạn dành cho một số nguyên (được gọi là v bên dưới) chứ không phải một số nguyên không dấu.

int v = 612635685; // whatever value you wish

unsigned int get_msb(int v)
{
    int r = 31;                         // maximum number of iteration until integer has been totally left shifted out, considering that first bit is index 0. Also we could use (sizeof(int)) << 3 - 1 instead of 31 to make it work on any platform.

    while (!(v & 0x80000000) && r--) {   // mask of the highest bit
        v <<= 1;                        // multiply integer by 2.
    }
    return r;                           // will even return -1 if no bit was set, allowing error catch
}

Nếu bạn muốn làm cho nó hoạt động mà không tính đến dấu hiệu, bạn có thể thêm một dấu 'v << = 1;' trước vòng lặp (và thay đổi giá trị r thành 30 cho phù hợp). Vui lòng cho tôi biết nếu tôi quên bất cứ điều gì. Tôi đã không thử nghiệm nó nhưng nó sẽ hoạt động tốt.


v <<= 1hành vi không xác định (UB) khi v < 0.
chux - Phục hồi Monica

0x8000000, có thể bạn có nghĩa là thêm một số 0 ở đó.
MM
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.