Kỹ thuật bit khôn ngoan yêu thích của bạn là gì? [đóng cửa]


14

Vài ngày trước, thành viên StackExchange Anto đã hỏi về việc sử dụng hợp lệ cho các nhà khai thác bit-khôn ngoan. Tôi đã nói rằng dịch chuyển nhanh hơn nhân và chia số nguyên cho lũy thừa của hai. Thành viên của StackExchange, Daemin đã chống lại bằng cách tuyên bố rằng sự thay đổi bên phải có vấn đề với các số âm.

Vào thời điểm đó, tôi chưa bao giờ thực sự nghĩ về việc sử dụng các toán tử thay đổi với các số nguyên đã ký. Tôi chủ yếu sử dụng kỹ thuật này trong phát triển phần mềm cấp thấp; do đó, tôi luôn luôn sử dụng số nguyên không dấu. C thực hiện các thay đổi logic trên các số nguyên không dấu. Không chú ý đến bit dấu khi thực hiện dịch chuyển logic. Các bit được điền được chứa đầy số không. Tuy nhiên, C thực hiện thao tác dịch chuyển số học khi dịch chuyển một số nguyên có chữ ký. Các bit được điền vào được điền với bit dấu. Sự khác biệt này làm cho giá trị âm được làm tròn về vô cực thay vì bị cắt về 0, đây là một hành vi khác với phân chia số nguyên đã ký.

Một vài phút suy nghĩ dẫn đến một giải pháp đầu tiên. Giải pháp có điều kiện chuyển đổi giá trị âm thành giá trị dương trước khi dịch chuyển. Một giá trị được chuyển đổi có điều kiện trở lại dạng âm sau khi thao tác dịch chuyển được thực hiện.

int a = -5;
int n = 1;

int negative = q < 0; 

a = negative ? -a : a; 
a >>= n; 
a = negative ? -a : a; 

Vấn đề với giải pháp này là các câu lệnh gán có điều kiện thường được dịch sang ít nhất một lệnh nhảy và các lệnh nhảy có thể tốn kém trên các bộ xử lý không giải mã cả hai đường dẫn lệnh. Việc phải định lại một đường ống dẫn hai lần sẽ giúp cải thiện hiệu suất đạt được bằng cách chuyển qua chia.

Với những điều đã nói ở trên, tôi thức dậy vào thứ bảy với câu trả lời cho vấn đề chuyển nhượng có điều kiện. Vấn đề làm tròn mà chúng ta gặp phải khi thực hiện thao tác dịch chuyển số học chỉ xảy ra khi làm việc với biểu diễn bổ sung của hai. Nó không xảy ra với đại diện bổ sung của một người. Giải pháp cho vấn đề liên quan đến việc chuyển đổi giá trị bổ sung của hai thành giá trị bổ sung của một người trước khi thực hiện thao tác thay đổi. Sau đó, chúng tôi phải chuyển đổi giá trị bổ sung của một người trở lại giá trị bổ sung của hai. Đáng ngạc nhiên, chúng ta có thể thực hiện bộ hoạt động này mà không cần chuyển đổi có điều kiện các giá trị âm trước khi thực hiện thao tác thay đổi.

int a = -5;
int n = 1;

register int sign = (a >> INT_SIZE_MINUS_1) & 1

a = (a - sign) >> n + sign;   

Giá trị âm bổ sung của hai được chuyển đổi thành giá trị âm bổ sung của một bằng cách trừ đi một giá trị âm. Mặt khác, giá trị âm bổ sung của một người được chuyển đổi thành giá trị âm bổ sung của hai bằng cách thêm một. Mã được liệt kê ở trên hoạt động vì bit dấu được sử dụng để chuyển đổi từ phần bù của hai sang phần bù của một và ngược lại . Chỉ các giá trị âm sẽ có các bit dấu của chúng được đặt; do đó, dấu biến sẽ bằng 0 khi a dương.

Với những điều đã nói ở trên, bạn có thể nghĩ về những vụ hack khôn ngoan khác như vụ lừa đảo ở trên đã biến nó thành túi của bạn không? Hack bit-khôn ngoan yêu thích của bạn là gì? Tôi luôn tìm kiếm các bản hack bit-bit định hướng hiệu suất mới.


3
Câu hỏi này & tên tài khoản của bạn - thế giới có ý nghĩa một lần nữa ...
JK

+1 Câu hỏi thú vị như là một câu hỏi tiếp theo của tôi và nếu không thì cũng vậy;)
Anto

Tôi cũng đã làm một số tính toán chẵn lẻ nhanh một lần. Chẵn lẻ là một chút đau đớn vì theo truyền thống, nó liên quan đến các vòng lặp và đếm nếu một bit được thiết lập, tất cả đều đòi hỏi nhiều bước nhảy. Tính chẵn lẻ có thể được tính bằng cách sử dụng shift và XOR, sau đó một loạt các bước được thực hiện lần lượt để tránh các vòng lặp và nhảy.
quick_now

2
Bạn có biết rằng có cả một cuốn sách về các kỹ thuật này? - Tin tặc Delight amazon.com/Hackers-Delight-Henry-S-Warren/dp/0201914654
nikie

Vâng, có một trang web dành cho hoạt động bit là tốt. Tôi quên URL nhưng google sẽ sớm bật nó lên.
quick_now

Câu trả lời:


23

Tôi yêu bản hack của Gosper (HAKMEM # 175), một cách rất tinh vi để lấy một số và lấy số tiếp theo với cùng số bit được đặt. Ví dụ, nó hữu ích trong việc tạo ra các kết hợp các kmục từ n:

int set = (1 << k) - 1;
int limit = (1 << n);
while (set < limit) {
    doStuff(set);

    // Gosper's hack:
    int c = set & -set;
    int r = set + c;
    set = (((r^set) >>> 2) / c) | r;
}

7
+1. Nhưng kể từ bây giờ, tôi sẽ gặp ác mộng về việc tìm thấy cái này trong một phiên gỡ lỗi mà không có bình luận.
nikie

@nikie, muahahahaha! (Tôi có xu hướng sử dụng điều này cho những thứ như các vấn đề của Project Euler - công việc hàng ngày của tôi không liên quan đến nhiều tổ hợp).
Peter Taylor

7

Các nhanh nghịch đảo vuông phương pháp gốc sử dụng hầu hết các kỹ thuật chút cấp kỳ lạ để tính toán nghịch đảo của một căn bậc hai mà tôi đã từng nhìn thấy:

float Q_rsqrt( float number )
{
    long i;
    float x2, y;
    const float threehalfs = 1.5F;

    x2 = number * 0.5F;
    y  = number;
    i  = * ( long * ) &y;                       // evil floating point bit level hacking [sic]
    i  = 0x5f3759df - ( i >> 1 );               // what the fuck? [sic]
    y  = * ( float * ) &i;
    y  = y * ( threehalfs - ( x2 * y * y ) );   // 1st iteration
    //    y  = y * ( threehalfs - ( x2 * y * y ) );   // 2nd iteration, this can be removed

    return y;
}

Sqrt nhanh cũng là tuyệt vời. Carmack có vẻ là một trong những lập trình viên vĩ đại nhất.
BenjaminB

Wikipedia thậm chí còn có các nguồn cũ hơn, ví dụ beyond3d.com/content/articles/15
MSalters

0

Chia cho 3 - mà không cần dùng đến một cuộc gọi thư viện thời gian thực.

Nó chỉ ra rằng phép chia cho 3 (nhờ một gợi ý về Stack Overflow) có thể được xấp xỉ là:

X / 3 = [(x / 4) + (x / 12)]

Và X / 12 là (x / 4) / 3. Có một yếu tố đệ quy đột nhiên xuất hiện ở đây.

Nó cũng chỉ ra rằng nếu bạn giới hạn phạm vi số bạn đang chơi, bạn có thể giới hạn số lần lặp cần thiết.

Và do đó, đối với các số nguyên không dấu <2000, sau đây là thuật toán nhanh và đơn giản / 3. (Đối với số lượng lớn hơn, chỉ cần thêm nhiều bước). Trình biên dịch tối ưu hóa cái quái vật này để kết thúc nhanh và nhỏ:

Short unsign tĩnh FastDivide3 (const unsign short arg)
{
  RunningSum ngắn không dấu;
  không dấu FractionalTwelth ngắn;

  RunningSum = arg >> 2;

  FractionalTwelth = RunningSum >> 2;
  RunningSum + = FractionalTwelth;

  FractionalTwelth >> = 2;
  RunningSum + = FractionalTwelth;

  FractionalTwelth >> = 2;
  RunningSum + = FractionalTwelth;

  FractionalTwelth >> = 2;
  RunningSum + = FractionalTwelth;

  // Lặp lại nhiều hơn 2 dòng trên cho chính xác hơn

  trả lại RunningSum;
}

1
Tất nhiên, điều này chỉ có liên quan trên các vi điều khiển rất tối nghĩa. Bất kỳ CPU thực nào được tạo ra trong hai thập kỷ qua đều không cần thư viện thời gian chạy để phân chia số nguyên.
MSalters

1
Ồ chắc chắn, nhưng micros nhỏ mà không có hệ số nhân phần cứng thực sự rất phổ biến. Và nếu bạn làm việc ở vùng đất nhúng và muốn tiết kiệm 0,10 đô la cho mỗi một triệu sản phẩm được bán thì bạn nên biết một số thủ thuật bẩn. Số tiền đó tiết kiệm = lợi nhuận thêm khiến sếp của bạn rất hạnh phúc.
quick_now

Chà, bẩn ... nó chỉ nhân với .0101010101(khoảng 1/3). Mẹo nhỏ: bạn cũng có thể nhân với .000100010001101(chỉ mất 3 bit, nhưng có xấp xỉ tốt hơn.010101010101
MSalters

Làm thế nào tôi có thể làm điều đó chỉ với số nguyên và không có dấu phẩy động?
quick_now

1
Bitwise, x * 101 = x + x << 2. Tương tự, x * 0,000100010001 là x >> 4 + x >> 8 + x >> 12.
MSalters

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.