Thuật toán hiệu quả cho đảo ngược bit (từ MSB-> LSB đến LSB-> MSB) trong C


243

Thuật toán hiệu quả nhất để đạt được những điều sau đây là gì:

0010 0000 => 0000 0100

Việc chuyển đổi là từ MSB-> LSB sang LSB-> MSB. Tất cả các bit phải được đảo ngược; đó là, đây không phải là sự hoán đổi cuối cùng.


1
Tôi nghĩ rằng tên thích hợp là một hoạt động bitwise.
Kredns

5
Tôi nghĩ bạn có nghĩa là đảo ngược, không xoay vòng.
Juliano

2
Hầu hết các bộ xử lý ARM có một hoạt động tích hợp cho điều đó. ARM Cortex-M0 không có, và tôi thấy rằng sử dụng bảng mỗi byte để trao đổi bit là cách tiếp cận nhanh nhất.
starblue

2
Cũng xem Hacks Twiddling Bit của Sean Eron Anderson .
jww

2
Vui lòng xác định "tốt nhất"
Lee Taylor

Câu trả lời:


497

LƯU Ý : Tất cả các thuật toán bên dưới đều bằng C, nhưng nên có thể chuyển sang ngôn ngữ bạn chọn (chỉ cần không nhìn vào tôi khi chúng không nhanh như vậy :)

Tùy chọn

Bộ nhớ thấp (máy 32 bit int, 32 bit) (từ đây ):

unsigned int
reverse(register unsigned int x)
{
    x = (((x & 0xaaaaaaaa) >> 1) | ((x & 0x55555555) << 1));
    x = (((x & 0xcccccccc) >> 2) | ((x & 0x33333333) << 2));
    x = (((x & 0xf0f0f0f0) >> 4) | ((x & 0x0f0f0f0f) << 4));
    x = (((x & 0xff00ff00) >> 8) | ((x & 0x00ff00ff) << 8));
    return((x >> 16) | (x << 16));

}

Từ trang Bit Twiddling Hacks nổi tiếng :

Nhanh nhất (bảng tra cứu) :

static const unsigned char BitReverseTable256[] = 
{
  0x00, 0x80, 0x40, 0xC0, 0x20, 0xA0, 0x60, 0xE0, 0x10, 0x90, 0x50, 0xD0, 0x30, 0xB0, 0x70, 0xF0, 
  0x08, 0x88, 0x48, 0xC8, 0x28, 0xA8, 0x68, 0xE8, 0x18, 0x98, 0x58, 0xD8, 0x38, 0xB8, 0x78, 0xF8, 
  0x04, 0x84, 0x44, 0xC4, 0x24, 0xA4, 0x64, 0xE4, 0x14, 0x94, 0x54, 0xD4, 0x34, 0xB4, 0x74, 0xF4, 
  0x0C, 0x8C, 0x4C, 0xCC, 0x2C, 0xAC, 0x6C, 0xEC, 0x1C, 0x9C, 0x5C, 0xDC, 0x3C, 0xBC, 0x7C, 0xFC, 
  0x02, 0x82, 0x42, 0xC2, 0x22, 0xA2, 0x62, 0xE2, 0x12, 0x92, 0x52, 0xD2, 0x32, 0xB2, 0x72, 0xF2, 
  0x0A, 0x8A, 0x4A, 0xCA, 0x2A, 0xAA, 0x6A, 0xEA, 0x1A, 0x9A, 0x5A, 0xDA, 0x3A, 0xBA, 0x7A, 0xFA,
  0x06, 0x86, 0x46, 0xC6, 0x26, 0xA6, 0x66, 0xE6, 0x16, 0x96, 0x56, 0xD6, 0x36, 0xB6, 0x76, 0xF6, 
  0x0E, 0x8E, 0x4E, 0xCE, 0x2E, 0xAE, 0x6E, 0xEE, 0x1E, 0x9E, 0x5E, 0xDE, 0x3E, 0xBE, 0x7E, 0xFE,
  0x01, 0x81, 0x41, 0xC1, 0x21, 0xA1, 0x61, 0xE1, 0x11, 0x91, 0x51, 0xD1, 0x31, 0xB1, 0x71, 0xF1,
  0x09, 0x89, 0x49, 0xC9, 0x29, 0xA9, 0x69, 0xE9, 0x19, 0x99, 0x59, 0xD9, 0x39, 0xB9, 0x79, 0xF9, 
  0x05, 0x85, 0x45, 0xC5, 0x25, 0xA5, 0x65, 0xE5, 0x15, 0x95, 0x55, 0xD5, 0x35, 0xB5, 0x75, 0xF5,
  0x0D, 0x8D, 0x4D, 0xCD, 0x2D, 0xAD, 0x6D, 0xED, 0x1D, 0x9D, 0x5D, 0xDD, 0x3D, 0xBD, 0x7D, 0xFD,
  0x03, 0x83, 0x43, 0xC3, 0x23, 0xA3, 0x63, 0xE3, 0x13, 0x93, 0x53, 0xD3, 0x33, 0xB3, 0x73, 0xF3, 
  0x0B, 0x8B, 0x4B, 0xCB, 0x2B, 0xAB, 0x6B, 0xEB, 0x1B, 0x9B, 0x5B, 0xDB, 0x3B, 0xBB, 0x7B, 0xFB,
  0x07, 0x87, 0x47, 0xC7, 0x27, 0xA7, 0x67, 0xE7, 0x17, 0x97, 0x57, 0xD7, 0x37, 0xB7, 0x77, 0xF7, 
  0x0F, 0x8F, 0x4F, 0xCF, 0x2F, 0xAF, 0x6F, 0xEF, 0x1F, 0x9F, 0x5F, 0xDF, 0x3F, 0xBF, 0x7F, 0xFF
};

unsigned int v; // reverse 32-bit value, 8 bits at time
unsigned int c; // c will get v reversed

// Option 1:
c = (BitReverseTable256[v & 0xff] << 24) | 
    (BitReverseTable256[(v >> 8) & 0xff] << 16) | 
    (BitReverseTable256[(v >> 16) & 0xff] << 8) |
    (BitReverseTable256[(v >> 24) & 0xff]);

// Option 2:
unsigned char * p = (unsigned char *) &v;
unsigned char * q = (unsigned char *) &c;
q[3] = BitReverseTable256[p[0]]; 
q[2] = BitReverseTable256[p[1]]; 
q[1] = BitReverseTable256[p[2]]; 
q[0] = BitReverseTable256[p[3]];

Bạn có thể mở rộng ý tưởng này thành 64 bit inthoặc đánh đổi bộ nhớ để lấy tốc độ (giả sử Bộ đệm dữ liệu L1 của bạn đủ lớn) và đảo ngược 16 bit mỗi lần với bảng tra cứu mục nhập 64K.


Khác

Đơn giản

unsigned int v;     // input bits to be reversed
unsigned int r = v & 1; // r will be reversed bits of v; first get LSB of v
int s = sizeof(v) * CHAR_BIT - 1; // extra shift needed at end

for (v >>= 1; v; v >>= 1)
{   
  r <<= 1;
  r |= v & 1;
  s--;
}
r <<= s; // shift when v's highest bits are zero

Nhanh hơn (bộ xử lý 32 bit)

unsigned char b = x;
b = ((b * 0x0802LU & 0x22110LU) | (b * 0x8020LU & 0x88440LU)) * 0x10101LU >> 16; 

Nhanh hơn (bộ xử lý 64 bit)

unsigned char b; // reverse this (8-bit) byte
b = (b * 0x0202020202ULL & 0x010884422010ULL) % 1023;

Nếu bạn muốn làm điều này trên 32 bit int, chỉ cần đảo ngược các bit trong mỗi byte và đảo ngược thứ tự của các byte. Đó là:

unsigned int toReverse;
unsigned int reversed;
unsigned char inByte0 = (toReverse & 0xFF);
unsigned char inByte1 = (toReverse & 0xFF00) >> 8;
unsigned char inByte2 = (toReverse & 0xFF0000) >> 16;
unsigned char inByte3 = (toReverse & 0xFF000000) >> 24;
reversed = (reverseBits(inByte0) << 24) | (reverseBits(inByte1) << 16) | (reverseBits(inByte2) << 8) | (reverseBits(inByte3);

Các kết quả

Tôi đã điểm chuẩn hai giải pháp hứa hẹn nhất là bảng tra cứu và bitwise-AND (giải pháp đầu tiên). Máy thử nghiệm là máy tính xách tay w / 4GB DDR2-800 và Core 2 Duo T7500 @ 2.4GHz, Cache 4MB L2; YMMV. Tôi đã sử dụng gcc 4.3.2 trên Linux 64 bit. OpenMP (và các ràng buộc GCC) đã được sử dụng cho bộ định thời độ phân giải cao.

đảo ngược

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

unsigned int
reverse(register unsigned int x)
{
    x = (((x & 0xaaaaaaaa) >> 1) | ((x & 0x55555555) << 1));
    x = (((x & 0xcccccccc) >> 2) | ((x & 0x33333333) << 2));
    x = (((x & 0xf0f0f0f0) >> 4) | ((x & 0x0f0f0f0f) << 4));
    x = (((x & 0xff00ff00) >> 8) | ((x & 0x00ff00ff) << 8));
    return((x >> 16) | (x << 16));

}

int main()
{
    unsigned int *ints = malloc(100000000*sizeof(unsigned int));
    unsigned int *ints2 = malloc(100000000*sizeof(unsigned int));
    for(unsigned int i = 0; i < 100000000; i++)
      ints[i] = rand();

    unsigned int *inptr = ints;
    unsigned int *outptr = ints2;
    unsigned int *endptr = ints + 100000000;
    // Starting the time measurement
    double start = omp_get_wtime();
    // Computations to be measured
    while(inptr != endptr)
    {
      (*outptr) = reverse(*inptr);
      inptr++;
      outptr++;
    }
    // Measuring the elapsed time
    double end = omp_get_wtime();
    // Time calculation (in seconds)
    printf("Time: %f seconds\n", end-start);

    free(ints);
    free(ints2);

    return 0;
}

đảo ngược_lookup.c

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

static const unsigned char BitReverseTable256[] = 
{
  0x00, 0x80, 0x40, 0xC0, 0x20, 0xA0, 0x60, 0xE0, 0x10, 0x90, 0x50, 0xD0, 0x30, 0xB0, 0x70, 0xF0, 
  0x08, 0x88, 0x48, 0xC8, 0x28, 0xA8, 0x68, 0xE8, 0x18, 0x98, 0x58, 0xD8, 0x38, 0xB8, 0x78, 0xF8, 
  0x04, 0x84, 0x44, 0xC4, 0x24, 0xA4, 0x64, 0xE4, 0x14, 0x94, 0x54, 0xD4, 0x34, 0xB4, 0x74, 0xF4, 
  0x0C, 0x8C, 0x4C, 0xCC, 0x2C, 0xAC, 0x6C, 0xEC, 0x1C, 0x9C, 0x5C, 0xDC, 0x3C, 0xBC, 0x7C, 0xFC, 
  0x02, 0x82, 0x42, 0xC2, 0x22, 0xA2, 0x62, 0xE2, 0x12, 0x92, 0x52, 0xD2, 0x32, 0xB2, 0x72, 0xF2, 
  0x0A, 0x8A, 0x4A, 0xCA, 0x2A, 0xAA, 0x6A, 0xEA, 0x1A, 0x9A, 0x5A, 0xDA, 0x3A, 0xBA, 0x7A, 0xFA,
  0x06, 0x86, 0x46, 0xC6, 0x26, 0xA6, 0x66, 0xE6, 0x16, 0x96, 0x56, 0xD6, 0x36, 0xB6, 0x76, 0xF6, 
  0x0E, 0x8E, 0x4E, 0xCE, 0x2E, 0xAE, 0x6E, 0xEE, 0x1E, 0x9E, 0x5E, 0xDE, 0x3E, 0xBE, 0x7E, 0xFE,
  0x01, 0x81, 0x41, 0xC1, 0x21, 0xA1, 0x61, 0xE1, 0x11, 0x91, 0x51, 0xD1, 0x31, 0xB1, 0x71, 0xF1,
  0x09, 0x89, 0x49, 0xC9, 0x29, 0xA9, 0x69, 0xE9, 0x19, 0x99, 0x59, 0xD9, 0x39, 0xB9, 0x79, 0xF9, 
  0x05, 0x85, 0x45, 0xC5, 0x25, 0xA5, 0x65, 0xE5, 0x15, 0x95, 0x55, 0xD5, 0x35, 0xB5, 0x75, 0xF5,
  0x0D, 0x8D, 0x4D, 0xCD, 0x2D, 0xAD, 0x6D, 0xED, 0x1D, 0x9D, 0x5D, 0xDD, 0x3D, 0xBD, 0x7D, 0xFD,
  0x03, 0x83, 0x43, 0xC3, 0x23, 0xA3, 0x63, 0xE3, 0x13, 0x93, 0x53, 0xD3, 0x33, 0xB3, 0x73, 0xF3, 
  0x0B, 0x8B, 0x4B, 0xCB, 0x2B, 0xAB, 0x6B, 0xEB, 0x1B, 0x9B, 0x5B, 0xDB, 0x3B, 0xBB, 0x7B, 0xFB,
  0x07, 0x87, 0x47, 0xC7, 0x27, 0xA7, 0x67, 0xE7, 0x17, 0x97, 0x57, 0xD7, 0x37, 0xB7, 0x77, 0xF7, 
  0x0F, 0x8F, 0x4F, 0xCF, 0x2F, 0xAF, 0x6F, 0xEF, 0x1F, 0x9F, 0x5F, 0xDF, 0x3F, 0xBF, 0x7F, 0xFF
};

int main()
{
    unsigned int *ints = malloc(100000000*sizeof(unsigned int));
    unsigned int *ints2 = malloc(100000000*sizeof(unsigned int));
    for(unsigned int i = 0; i < 100000000; i++)
      ints[i] = rand();

    unsigned int *inptr = ints;
    unsigned int *outptr = ints2;
    unsigned int *endptr = ints + 100000000;
    // Starting the time measurement
    double start = omp_get_wtime();
    // Computations to be measured
    while(inptr != endptr)
    {
    unsigned int in = *inptr;  

    // Option 1:
    //*outptr = (BitReverseTable256[in & 0xff] << 24) | 
    //    (BitReverseTable256[(in >> 8) & 0xff] << 16) | 
    //    (BitReverseTable256[(in >> 16) & 0xff] << 8) |
    //    (BitReverseTable256[(in >> 24) & 0xff]);

    // Option 2:
    unsigned char * p = (unsigned char *) &(*inptr);
    unsigned char * q = (unsigned char *) &(*outptr);
    q[3] = BitReverseTable256[p[0]]; 
    q[2] = BitReverseTable256[p[1]]; 
    q[1] = BitReverseTable256[p[2]]; 
    q[0] = BitReverseTable256[p[3]];

      inptr++;
      outptr++;
    }
    // Measuring the elapsed time
    double end = omp_get_wtime();
    // Time calculation (in seconds)
    printf("Time: %f seconds\n", end-start);

    free(ints);
    free(ints2);

    return 0;
}

Tôi đã thử cả hai cách tiếp cận ở một số tối ưu hóa khác nhau, chạy 3 thử nghiệm ở mỗi cấp độ và mỗi thử nghiệm đảo ngược 100 triệu ngẫu nhiên unsigned ints. Đối với tùy chọn bảng tra cứu, tôi đã thử cả hai lược đồ (tùy chọn 1 và 2) được đưa ra trên trang hack bitwise. Kết quả được hiển thị dưới đây.

Bitwise VÀ

mrj10@mjlap:~/code$ gcc -fopenmp -std=c99 -o reverse reverse.c
mrj10@mjlap:~/code$ ./reverse
Time: 2.000593 seconds
mrj10@mjlap:~/code$ ./reverse
Time: 1.938893 seconds
mrj10@mjlap:~/code$ ./reverse
Time: 1.936365 seconds
mrj10@mjlap:~/code$ gcc -fopenmp -std=c99 -O2 -o reverse reverse.c
mrj10@mjlap:~/code$ ./reverse
Time: 0.942709 seconds
mrj10@mjlap:~/code$ ./reverse
Time: 0.991104 seconds
mrj10@mjlap:~/code$ ./reverse
Time: 0.947203 seconds
mrj10@mjlap:~/code$ gcc -fopenmp -std=c99 -O3 -o reverse reverse.c
mrj10@mjlap:~/code$ ./reverse
Time: 0.922639 seconds
mrj10@mjlap:~/code$ ./reverse
Time: 0.892372 seconds
mrj10@mjlap:~/code$ ./reverse
Time: 0.891688 seconds

Bảng tra cứu (tùy chọn 1)

mrj10@mjlap:~/code$ gcc -fopenmp -std=c99 -o reverse_lookup reverse_lookup.c
mrj10@mjlap:~/code$ ./reverse_lookup
Time: 1.201127 seconds              
mrj10@mjlap:~/code$ ./reverse_lookup
Time: 1.196129 seconds              
mrj10@mjlap:~/code$ ./reverse_lookup
Time: 1.235972 seconds              
mrj10@mjlap:~/code$ gcc -fopenmp -std=c99 -O2 -o reverse_lookup reverse_lookup.c
mrj10@mjlap:~/code$ ./reverse_lookup
Time: 0.633042 seconds              
mrj10@mjlap:~/code$ ./reverse_lookup
Time: 0.655880 seconds              
mrj10@mjlap:~/code$ ./reverse_lookup
Time: 0.633390 seconds              
mrj10@mjlap:~/code$ gcc -fopenmp -std=c99 -O3 -o reverse_lookup reverse_lookup.c
mrj10@mjlap:~/code$ ./reverse_lookup
Time: 0.652322 seconds              
mrj10@mjlap:~/code$ ./reverse_lookup
Time: 0.631739 seconds              
mrj10@mjlap:~/code$ ./reverse_lookup
Time: 0.652431 seconds  

Bảng tra cứu (tùy chọn 2)

mrj10@mjlap:~/code$ gcc -fopenmp -std=c99 -o reverse_lookup reverse_lookup.c
mrj10@mjlap:~/code$ ./reverse_lookup
Time: 1.671537 seconds
mrj10@mjlap:~/code$ ./reverse_lookup
Time: 1.688173 seconds
mrj10@mjlap:~/code$ ./reverse_lookup
Time: 1.664662 seconds
mrj10@mjlap:~/code$ gcc -fopenmp -std=c99 -O2 -o reverse_lookup reverse_lookup.c
mrj10@mjlap:~/code$ ./reverse_lookup
Time: 1.049851 seconds
mrj10@mjlap:~/code$ ./reverse_lookup
Time: 1.048403 seconds
mrj10@mjlap:~/code$ ./reverse_lookup
Time: 1.085086 seconds
mrj10@mjlap:~/code$ gcc -fopenmp -std=c99 -O3 -o reverse_lookup reverse_lookup.c
mrj10@mjlap:~/code$ ./reverse_lookup
Time: 1.082223 seconds
mrj10@mjlap:~/code$ ./reverse_lookup
Time: 1.053431 seconds
mrj10@mjlap:~/code$ ./reverse_lookup
Time: 1.081224 seconds

Phần kết luận

Sử dụng bảng tra cứu, với tùy chọn 1 (địa chỉ byte chậm không đáng ngạc nhiên) nếu bạn quan tâm đến hiệu suất. Nếu bạn cần vắt từng byte bộ nhớ cuối cùng ra khỏi hệ thống của mình (và bạn có thể, nếu bạn quan tâm đến hiệu suất đảo ngược bit), các phiên bản tối ưu hóa của phương pháp bitwise-AND cũng không quá tồi tệ.

Hãy cẩn thận

Vâng, tôi biết mã điểm chuẩn là một bản hack hoàn chỉnh. Đề xuất về cách cải thiện nó được chào đón nhiều hơn. Những điều tôi biết về:

  • Tôi không có quyền truy cập vào ICC. Điều này có thể nhanh hơn (vui lòng trả lời trong một bình luận nếu bạn có thể kiểm tra điều này).
  • Một bảng tra cứu 64K có thể làm tốt trên một số kiến ​​trúc vi mô hiện đại với L1D lớn.
  • -mtune = bản địa không hoạt động cho -O2 / -O3 ( ldxuất hiện một số lỗi xác định lại biểu tượng điên rồ), vì vậy tôi không tin rằng mã được tạo được điều chỉnh cho kiến ​​trúc vi mô của tôi.
  • Có thể có một cách để làm điều này nhanh hơn một chút với SSE. Tôi không biết làm thế nào, nhưng với sự sao chép nhanh chóng, được đóng gói theo bit và các hướng dẫn lộn xộn, đã có một cái gì đó ở đó.
  • Tôi chỉ biết lắp ráp x86 đủ để gây nguy hiểm; đây là mã GCC được tạo trên -O3 cho tùy chọn 1, vì vậy ai đó hiểu biết hơn tôi có thể kiểm tra nó:

32-bit

.L3:
movl    (%r12,%rsi), %ecx
movzbl  %cl, %eax
movzbl  BitReverseTable256(%rax), %edx
movl    %ecx, %eax
shrl    $24, %eax
mov     %eax, %eax
movzbl  BitReverseTable256(%rax), %eax
sall    $24, %edx
orl     %eax, %edx
movzbl  %ch, %eax
shrl    $16, %ecx
movzbl  BitReverseTable256(%rax), %eax
movzbl  %cl, %ecx
sall    $16, %eax
orl     %eax, %edx
movzbl  BitReverseTable256(%rcx), %eax
sall    $8, %eax
orl     %eax, %edx
movl    %edx, (%r13,%rsi)
addq    $4, %rsi
cmpq    $400000000, %rsi
jne     .L3

EDIT: Tôi cũng đã thử sử dụng uint64_tcác loại trên máy của mình để xem liệu có bất kỳ tăng hiệu suất nào không. Hiệu suất nhanh hơn khoảng 10% so với 32 bit và gần như giống hệt nhau cho dù bạn chỉ sử dụng loại 64 bit để đảo ngược bit trên hai loại 32 bit intmột lần hay bạn thực sự đảo ngược bit trong một nửa số 64- giá trị bit. Mã lắp ráp được hiển thị bên dưới (đối với trường hợp trước, đảo ngược bit cho hai loại 32 bit intcùng một lúc):

.L3:
movq    (%r12,%rsi), %rdx
movq    %rdx, %rax
shrq    $24, %rax
andl    $255, %eax
movzbl  BitReverseTable256(%rax), %ecx
movzbq  %dl,%rax
movzbl  BitReverseTable256(%rax), %eax
salq    $24, %rax
orq     %rax, %rcx
movq    %rdx, %rax
shrq    $56, %rax
movzbl  BitReverseTable256(%rax), %eax
salq    $32, %rax
orq     %rax, %rcx
movzbl  %dh, %eax
shrq    $16, %rdx
movzbl  BitReverseTable256(%rax), %eax
salq    $16, %rax
orq     %rax, %rcx
movzbq  %dl,%rax
shrq    $16, %rdx
movzbl  BitReverseTable256(%rax), %eax
salq    $8, %rax
orq     %rax, %rcx
movzbq  %dl,%rax
shrq    $8, %rdx
movzbl  BitReverseTable256(%rax), %eax
salq    $56, %rax
orq     %rax, %rcx
movzbq  %dl,%rax
shrq    $8, %rdx
movzbl  BitReverseTable256(%rax), %eax
andl    $255, %edx
salq    $48, %rax
orq     %rax, %rcx
movzbl  BitReverseTable256(%rdx), %eax
salq    $40, %rax
orq     %rax, %rcx
movq    %rcx, (%r13,%rsi)
addq    $8, %rsi
cmpq    $400000000, %rsi
jne     .L3

2
-1 cho bài viết quá chi tiết và kỹ lưỡng. j / k. +1.
mpen

8
Đó là một bài tập thú vị, nếu không muốn nói là hoàn thành. Nếu không có gì khác, tôi hy vọng thấy quá trình này mang tính xây dựng cho người khác, những người có thể muốn điểm chuẩn một cái gì đó có công hơn :)
Matt J

5
Chúa tôi! Tôi nghĩ rằng tôi đã tìm thấy ... những gì rất có thể là ... một đặc điểm THẬT. Tôi sẽ phải tham khảo tài liệu của mình và nghiên cứu thêm, nhưng có điều gì đó cho tôi biết (Chúa ơi, hãy giúp tôi), đây là câu trả lời tuyệt vời nhất, thấu đáo và hữu ích nhất cho đến nay. Ngay cả John Skeet cũng sẽ kinh hoàng và ấn tượng!
zeboidlund

3
Hãy nhớ rằng một lỗ hổng đặc biệt của microbenchmarking (trong số một danh sách của nhiều người khác) là nó có xu hướng ủng hộ giả tạo các giải pháp dựa trên bảng tra cứu. Vì điểm chuẩn đang lặp lại một thao tác trong một vòng lặp, nên thường sẽ thấy rằng sử dụng bảng tra cứu vừa với L1 là nhanh nhất, bởi vì mọi thứ sẽ chạm vào L1 mỗi lần vì không có áp lực bộ đệm. Trong trường hợp sử dụng thực tế, hoạt động thường sẽ được xen kẽ với các hoạt động khác gây ra một số áp lực bộ đệm. Việc bỏ lỡ RAM có thể lâu hơn 10 hoặc 100 lần so với thông thường, nhưng điều này bị bỏ qua trong điểm chuẩn.
BeeOnRope

2
Kết quả cuối cùng là nếu hai giải pháp gần nhau, tôi thường chọn giải pháp không LUT (hoặc giải pháp có LUT nhỏ hơn) vì tác động trong thế giới thực của LUT có thể nghiêm trọng. Thậm chí tốt hơn là đánh giá từng giải pháp "in situ" - nơi nó thực sự được sử dụng trong ứng dụng lớn hơn, với đầu vào thực tế. Tất nhiên, chúng tôi không phải lúc nào cũng có thời gian cho việc đó và chúng tôi không luôn biết đầu vào thực tế là gì.
BeeOnRope

80

Chủ đề này đã thu hút sự chú ý của tôi vì nó giải quyết một vấn đề đơn giản đòi hỏi nhiều công việc (chu kỳ CPU) ngay cả đối với một CPU hiện đại. Và một ngày, tôi cũng đứng đó với cùng một vấn đề ¤ #% "#". Tôi đã phải lật hàng triệu byte. Tuy nhiên, tôi biết tất cả các hệ thống mục tiêu của mình đều dựa trên Intel hiện đại, vì vậy hãy bắt đầu tối ưu hóa đến cùng cực !!!

Vì vậy, tôi đã sử dụng mã tra cứu của Matt J làm cơ sở. hệ thống tôi đang chuẩn là i7 haswell 4700eq.

Tra cứu của Matt J bitflipping 400 000 000 byte: Khoảng 0,272 giây.

Sau đó, tôi đã tiếp tục và cố gắng xem liệu trình biên dịch ISPC của Intel có thể véc tơ các số liệu trong đảo ngược không.

Tôi sẽ không làm bạn nhàm chán với những phát hiện của tôi ở đây vì tôi đã cố gắng rất nhiều để giúp trình biên dịch tìm kiếm công cụ, dù sao tôi cũng kết thúc với hiệu suất khoảng 0,15 giây để bitflip 400 000 000 byte. Đó là một mức giảm tuyệt vời nhưng đối với ứng dụng của tôi vẫn còn quá chậm ..

Vì vậy, mọi người hãy để tôi trình bày bitflipper dựa trên Intel nhanh nhất trên thế giới. Đồng hồ tại:

Thời gian để bitflip 400000000 byte: 0,050082 giây !!!!!

// Bitflip using AVX2 - The fastest Intel based bitflip in the world!!
// Made by Anders Cedronius 2014 (anders.cedronius (you know what) gmail.com)

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

using namespace std;

#define DISPLAY_HEIGHT  4
#define DISPLAY_WIDTH   32
#define NUM_DATA_BYTES  400000000

// Constants (first we got the mask, then the high order nibble look up table and last we got the low order nibble lookup table)
__attribute__ ((aligned(32))) static unsigned char k1[32*3]={
        0x0f,0x0f,0x0f,0x0f,0x0f,0x0f,0x0f,0x0f,0x0f,0x0f,0x0f,0x0f,0x0f,0x0f,0x0f,0x0f,0x0f,0x0f,0x0f,0x0f,0x0f,0x0f,0x0f,0x0f,0x0f,0x0f,0x0f,0x0f,0x0f,0x0f,0x0f,0x0f,
        0x00,0x08,0x04,0x0c,0x02,0x0a,0x06,0x0e,0x01,0x09,0x05,0x0d,0x03,0x0b,0x07,0x0f,0x00,0x08,0x04,0x0c,0x02,0x0a,0x06,0x0e,0x01,0x09,0x05,0x0d,0x03,0x0b,0x07,0x0f,
        0x00,0x80,0x40,0xc0,0x20,0xa0,0x60,0xe0,0x10,0x90,0x50,0xd0,0x30,0xb0,0x70,0xf0,0x00,0x80,0x40,0xc0,0x20,0xa0,0x60,0xe0,0x10,0x90,0x50,0xd0,0x30,0xb0,0x70,0xf0
};

// The data to be bitflipped (+32 to avoid the quantization out of memory problem)
__attribute__ ((aligned(32))) static unsigned char data[NUM_DATA_BYTES+32]={};

extern "C" {
void bitflipbyte(unsigned char[],unsigned int,unsigned char[]);
}

int main()
{

    for(unsigned int i = 0; i < NUM_DATA_BYTES; i++)
    {
        data[i] = rand();
    }

    printf ("\r\nData in(start):\r\n");
    for (unsigned int j = 0; j < 4; j++)
    {
        for (unsigned int i = 0; i < DISPLAY_WIDTH; i++)
        {
            printf ("0x%02x,",data[i+(j*DISPLAY_WIDTH)]);
        }
        printf ("\r\n");
    }

    printf ("\r\nNumber of 32-byte chunks to convert: %d\r\n",(unsigned int)ceil(NUM_DATA_BYTES/32.0));

    double start_time = omp_get_wtime();
    bitflipbyte(data,(unsigned int)ceil(NUM_DATA_BYTES/32.0),k1);
    double end_time = omp_get_wtime();

    printf ("\r\nData out:\r\n");
    for (unsigned int j = 0; j < 4; j++)
    {
        for (unsigned int i = 0; i < DISPLAY_WIDTH; i++)
        {
            printf ("0x%02x,",data[i+(j*DISPLAY_WIDTH)]);
        }
        printf ("\r\n");
    }
    printf("\r\n\r\nTime to bitflip %d bytes: %f seconds\r\n\r\n",NUM_DATA_BYTES, end_time-start_time);

    // return with no errors
    return 0;
}

Các printf là để gỡ lỗi ..

Đây là đặc điểm:

bits 64
global bitflipbyte

bitflipbyte:    
        vmovdqa     ymm2, [rdx]
        add         rdx, 20h
        vmovdqa     ymm3, [rdx]
        add         rdx, 20h
        vmovdqa     ymm4, [rdx]
bitflipp_loop:
        vmovdqa     ymm0, [rdi] 
        vpand       ymm1, ymm2, ymm0 
        vpandn      ymm0, ymm2, ymm0 
        vpsrld      ymm0, ymm0, 4h 
        vpshufb     ymm1, ymm4, ymm1 
        vpshufb     ymm0, ymm3, ymm0         
        vpor        ymm0, ymm0, ymm1
        vmovdqa     [rdi], ymm0
        add     rdi, 20h
        dec     rsi
        jnz     bitflipp_loop
        ret

Mã này lấy 32 byte sau đó che dấu các nibble. Nibble cao được dịch chuyển sang phải 4. Sau đó, tôi sử dụng vpshufb và ymm4 / ymm3 làm bảng tra cứu. Tôi có thể sử dụng một bảng tra cứu duy nhất nhưng sau đó tôi sẽ phải dịch chuyển sang trái trước khi OR lại các nibble lại với nhau.

Thậm chí có những cách nhanh hơn để lật các bit. Nhưng tôi bị ràng buộc với một luồng và CPU nên đây là tốc độ nhanh nhất tôi có thể đạt được. Bạn có thể làm một phiên bản nhanh hơn?

Vui lòng không bình luận về việc sử dụng các lệnh Tương đương nội tại của Trình biên dịch Intel C / C ++ ...


2
Bạn xứng đáng nhận được nhiều hơn FAR hơn thế này. Tôi biết rằng điều này nên được thực hiện với pshub, bởi vì sau tất cả các popcount tốt nhất cũng được thực hiện với nó! Tôi đã viết nó ở đây nếu không phải cho bạn. Thanh danh.
Idillotexist Idonotexist

3
Cảm ơn! 'popcnt' là một chủ đề yêu thích khác của tôi;) Kiểm tra phiên bản BMI2 của tôi: result = __ tzcnt_u64 (~ _pext_u64 (data [i], data [i]));
Anders Cedronius

3
Đặt tên cho tệp asm: bitflip_asm.s sau đó: yasm -f elf64 bitflip_asm.s Đặt tên cho tệp c: bitflip.c sau đó: g ++ -fopenmp bitflip.c bitflip_asm.o -o bitflip Thats nó.
Anders Cedronius

4
CPU Intel có các đơn vị thi công cho popcnt, tzcntpexttất cả trên cổng 1. Vì vậy, mỗi pexthoặc tzcntchi phí bạn một popcntsố thông. Nếu dữ liệu của bạn nóng trong bộ đệm L1D, cách nhanh nhất để đếm một mảng trên CPU Intel là với AVX2 pshufb. (Ryzen có 4 mỗi đồng hồ popcntthông vì vậy đó là lẽ tối ưu, nhưng Bulldozer gia đình có một mỗi 4 đồng hồ popcnt r64,r64thông ... agner.org/optimize ).
Peter Cordes

4
Tôi đang sử dụng một phiên bản nội tại bản thân mình. Tuy nhiên khi tôi trả lời tôi đã đăng những gì tôi có và tôi biết từ những bài viết trước đó rằng ngay khi tôi viết trình biên dịch, một người thông minh luôn chỉ ra rằng tôi nên làm điều đó trong nội tại. Khi tôi phát triển tôi viết trình biên dịch trước, khi tôi thích kết quả tôi chuyển sang nội tại .. Đó là tôi .. Tôi chỉ tình cờ đăng câu trả lời của mình khi tôi chỉ có phiên bản trình biên dịch 'thử nghiệm'.
Anders Cedronius

16

Đây là một giải pháp khác cho những người yêu thích đệ quy.

Ý tưởng rất đơn giản. Chia một nửa đầu vào và hoán đổi hai nửa, tiếp tục cho đến khi đạt được một bit.

Illustrated in the example below.

Ex : If Input is 00101010   ==> Expected output is 01010100

1. Divide the input into 2 halves 
    0010 --- 1010

2. Swap the 2 Halves
    1010     0010

3. Repeat the same for each half.
    10 -- 10 ---  00 -- 10
    10    10      10    00

    1-0 -- 1-0 --- 1-0 -- 0-0
    0 1    0 1     0 1    0 0

Done! Output is 01010100

Đây là một hàm đệ quy để giải quyết nó. (Lưu ý tôi đã sử dụng số nguyên không dấu, vì vậy nó có thể hoạt động cho các đầu vào lên đến sizeof (số nguyên không dấu) * 8 bit.

Hàm đệ quy có 2 tham số - Giá trị mà các bit cần được đảo ngược và số bit trong giá trị.

int reverse_bits_recursive(unsigned int num, unsigned int numBits)
{
    unsigned int reversedNum;;
    unsigned int mask = 0;

    mask = (0x1 << (numBits/2)) - 1;

    if (numBits == 1) return num;
    reversedNum = reverse_bits_recursive(num >> numBits/2, numBits/2) |
                   reverse_bits_recursive((num & mask), numBits/2) << numBits/2;
    return reversedNum;
}

int main()
{
    unsigned int reversedNum;
    unsigned int num;

    num = 0x55;
    reversedNum = reverse_bits_recursive(num, 8);
    printf ("Bit Reversal Input = 0x%x Output = 0x%x\n", num, reversedNum);

    num = 0xabcd;
    reversedNum = reverse_bits_recursive(num, 16);
    printf ("Bit Reversal Input = 0x%x Output = 0x%x\n", num, reversedNum);

    num = 0x123456;
    reversedNum = reverse_bits_recursive(num, 24);
    printf ("Bit Reversal Input = 0x%x Output = 0x%x\n", num, reversedNum);

    num = 0x11223344;
    reversedNum = reverse_bits_recursive(num,32);
    printf ("Bit Reversal Input = 0x%x Output = 0x%x\n", num, reversedNum);
}

Đây là đầu ra:

Bit Reversal Input = 0x55 Output = 0xaa
Bit Reversal Input = 0xabcd Output = 0xb3d5
Bit Reversal Input = 0x123456 Output = 0x651690
Bit Reversal Input = 0x11223344 Output = 0x22cc4488

Cách tiếp cận này không hoạt động trên ví dụ 24 bit (thứ 3)? Tôi không hoàn toàn quen thuộc với các toán tử C và bitwise nhưng từ lời giải thích của bạn về cách tiếp cận, tôi đoán 24-> 12-> 6-> 3 (3 bit không đồng đều để phân chia). Như numBitsint, khi bạn chia 3 cho 2 cho hàm param thì nó sẽ được làm tròn xuống 1?
Brennan

13

Chà, đây chắc chắn sẽ không phải là một câu trả lời như của Matt J nhưng hy vọng nó vẫn hữu ích.

size_t reverse(size_t n, unsigned int bytes)
{
    __asm__("BSWAP %0" : "=r"(n) : "0"(n));
    n >>= ((sizeof(size_t) - bytes) * 8);
    n = ((n & 0xaaaaaaaaaaaaaaaa) >> 1) | ((n & 0x5555555555555555) << 1);
    n = ((n & 0xcccccccccccccccc) >> 2) | ((n & 0x3333333333333333) << 2);
    n = ((n & 0xf0f0f0f0f0f0f0f0) >> 4) | ((n & 0x0f0f0f0f0f0f0f0f) << 4);
    return n;
}

Đây chính xác là ý tưởng tương tự như thuật toán tốt nhất của Matt ngoại trừ việc có một lệnh nhỏ này được gọi là BSWAP, hoán đổi các byte (không phải các bit) của một số 64 bit. Vậy b7, b6, b5, b4, b3, b2, b1, b0 trở thành b0, b1, b2, b3, b4, b5, b6, b7. Vì chúng tôi đang làm việc với số 32 bit, chúng tôi cần chuyển số byte được hoán đổi xuống 32 bit. Điều này chỉ để lại cho chúng ta nhiệm vụ hoán đổi 8 bit của mỗi byte được thực hiện và thì đấy! đã được thực hiện.

Thời gian: trên máy của tôi, thuật toán của Matt chạy trong ~ 0,52 giây mỗi lần dùng thử. Tôi chạy trong khoảng 0,42 giây mỗi thử nghiệm. 20% nhanh hơn không phải là xấu tôi nghĩ.

Nếu bạn lo lắng về tính khả dụng của hướng dẫn BSWAP Wikipedia liệt kê hướng dẫn BSWAP được thêm vào với 80846 được phát hành vào năm 1989. Cần lưu ý rằng Wikipedia cũng chỉ ra rằng hướng dẫn này chỉ hoạt động trên các thanh ghi 32 bit rõ ràng không phải là Trường hợp trên máy của tôi, nó chỉ hoạt động rất nhiều trên các thanh ghi 64 bit.

Phương thức này sẽ hoạt động tốt như nhau đối với bất kỳ kiểu dữ liệu tích phân nào, vì vậy phương thức có thể được khái quát hóa một cách tầm thường bằng cách chuyển số byte mong muốn:

    size_t reverse(size_t n, unsigned int bytes)
    {
        __asm__("BSWAP %0" : "=r"(n) : "0"(n));
        n >>= ((sizeof(size_t) - bytes) * 8);
        n = ((n & 0xaaaaaaaaaaaaaaaa) >> 1) | ((n & 0x5555555555555555) << 1);
        n = ((n & 0xcccccccccccccccc) >> 2) | ((n & 0x3333333333333333) << 2);
        n = ((n & 0xf0f0f0f0f0f0f0f0) >> 4) | ((n & 0x0f0f0f0f0f0f0f0f) << 4);
        return n;
    }

mà sau đó có thể được gọi là:

    n = reverse(n, sizeof(char));//only reverse 8 bits
    n = reverse(n, sizeof(short));//reverse 16 bits
    n = reverse(n, sizeof(int));//reverse 32 bits
    n = reverse(n, sizeof(size_t));//reverse 64 bits

Trình biên dịch sẽ có thể tối ưu hóa tham số phụ đi (giả sử trình biên dịch nội tuyến hàm) và trong sizeof(size_t)trường hợp dịch chuyển phải sẽ bị loại bỏ hoàn toàn. Lưu ý rằng ít nhất GCC không thể xóa BSWAP và dịch chuyển sang phải nếu được thông qua sizeof(char).


2
Theo Tập tham khảo Tập lệnh Intel Tập 2A ( intel.com/content/www/us/en/ Processors / Giả), có hai hướng dẫn BSWAP: BSWAP r32 (làm việc trên các thanh ghi 32 bit), được mã hóa thành 0F C8 + rd và BSWAP r64 (làm việc trên các thanh ghi 64 bit), được mã hóa thành REX.W + 0F C8 + rd.
Nubok

Bạn nói rằng nó có thể được sử dụng như thế này: "n = Reverse (n, sizeof (size_t)); // đảo ngược 64 bit" tuy nhiên điều này sẽ chỉ cho 32 bit kết quả trừ khi tất cả các hằng số được mở rộng đến 64 bit, sau đó nó hoạt động.
rajkosto

@rajkosto kể từ C ++ 11, các loại số nguyên được phép bao gồm unsigned long long intít nhất 64 bit, theo đâytại đây
SirGuy

Đồng ý? Tôi chỉ nói rằng nếu bạn muốn điều này hoạt động trên các giá trị 64 bit, bạn phải mở rộng nghĩa đen của mình (ví dụ: 0xf0f0f0f0f0f0f0f0ull), nếu không, 32 bit cao của kết quả sẽ là 0.
rajkosto

@rajkosto Ah, tôi đã hiểu nhầm ý kiến ​​đầu tiên của bạn, tôi đã sửa nó ngay bây giờ
SirGuy

13

Câu trả lời của Anders Cedronius cung cấp một giải pháp tuyệt vời cho những người có CPU x86 có hỗ trợ AVX2. Đối với các nền tảng x86 không có hỗ trợ AVX hoặc các nền tảng không phải là x86, một trong hai cách triển khai sau sẽ hoạt động tốt.

Mã đầu tiên là một biến thể của phương pháp phân vùng nhị phân cổ điển, được mã hóa để tối đa hóa việc sử dụng thành ngữ shift-plus-logic hữu ích trên các bộ xử lý ARM khác nhau. Ngoài ra, nó sử dụng việc tạo mặt nạ đang hoạt động có thể có lợi cho các bộ xử lý RISC, nếu không yêu cầu nhiều hướng dẫn để tải từng giá trị mặt nạ 32 bit. Trình biên dịch cho nền tảng x86 nên sử dụng việc truyền liên tục để tính toán tất cả các mặt nạ tại thời gian biên dịch thay vì thời gian chạy.

/* Classic binary partitioning algorithm */
inline uint32_t brev_classic (uint32_t a)
{
    uint32_t m;
    a = (a >> 16) | (a << 16);                            // swap halfwords
    m = 0x00ff00ff; a = ((a >> 8) & m) | ((a << 8) & ~m); // swap bytes
    m = m^(m << 4); a = ((a >> 4) & m) | ((a << 4) & ~m); // swap nibbles
    m = m^(m << 2); a = ((a >> 2) & m) | ((a << 2) & ~m);
    m = m^(m << 1); a = ((a >> 1) & m) | ((a << 1) & ~m);
    return a;
}

Trong tập 4A của "Nghệ thuật lập trình máy tính", D. Knuth cho thấy những cách đảo ngược bit thông minh, phần nào đáng ngạc nhiên đòi hỏi ít thao tác hơn các thuật toán phân vùng nhị phân cổ điển. Một thuật toán như vậy cho toán hạng 32 bit, mà tôi không thể tìm thấy trong TAOCP, được hiển thị trong tài liệu này trên trang web của Hacker Delight.

/* Knuth's algorithm from http://www.hackersdelight.org/revisions.pdf. Retrieved 8/19/2015 */
inline uint32_t brev_knuth (uint32_t a)
{
    uint32_t t;
    a = (a << 15) | (a >> 17);
    t = (a ^ (a >> 10)) & 0x003f801f; 
    a = (t + (t << 10)) ^ a;
    t = (a ^ (a >>  4)) & 0x0e038421; 
    a = (t + (t <<  4)) ^ a;
    t = (a ^ (a >>  2)) & 0x22488842; 
    a = (t + (t <<  2)) ^ a;
    return a;
}

Sử dụng trình biên dịch C / C ++ của trình biên dịch Intel 13.1.3.198, cả hai chức năng trên đều tự động vector hóa các XMMthanh ghi nhắm mục tiêu độc đáo . Họ cũng có thể được vector hóa thủ công mà không cần nhiều nỗ lực.

Trên IvyBridge Xeon E3 1270v2 của tôi, sử dụng mã được tự động hóa, 100 triệu uint32_ttừ được đảo ngược bit trong 0,070 giây khi sử dụng brev_classic()và 0,068 giây khi sử dụng brev_knuth(). Tôi đã cẩn thận để đảm bảo rằng điểm chuẩn của tôi không bị giới hạn bởi băng thông bộ nhớ hệ thống.


2
@JoelSnyder Tôi giả sử bằng "rất nhiều số ma thuật" mà bạn chủ yếu đề cập đến brev_knuth()? Sự quy kết trong bản PDF từ Hacker Delight dường như chỉ ra rằng những con số này là trực tiếp từ chính Knuth. Tôi không thể tuyên bố đã hiểu mô tả của Knuth về các nguyên tắc thiết kế cơ bản trong TAOCP để giải thích cách các hằng số được tạo ra, hoặc cách người ta đi về các hằng số và các hệ số dịch chuyển cho các kích thước từ tùy ý.
njuffa

8

Giả sử rằng bạn có một mảng các bit, làm thế nào về điều này: 1. Bắt đầu từ MSB, đẩy từng bit một chồng lên nhau. 2. Các bit pop từ ngăn xếp này vào một mảng khác (hoặc cùng một mảng nếu bạn muốn tiết kiệm không gian), đặt bit popped đầu tiên vào MSB và chuyển sang các bit ít quan trọng hơn từ đó.

Stack stack = new Stack();
Bit[] bits = new Bit[] { 0, 0, 1, 0, 0, 0, 0, 0 };

for (int i = 0; i < bits.Length; i++) 
{
    stack.push(bits[i]);
}

for (int i = 0; i < bits.Length; i++)
{
    bits[i] = stack.pop();
}

3
Điều này làm tôi mỉm cười :) Tôi rất muốn thấy một điểm chuẩn của giải pháp C # này so với một trong những giải pháp tôi đã nêu ở trên trong tối ưu hóa C.
Matt J

LOL ... Nhưng này! tính từ 'tốt nhất' trong 'thuật toán tốt nhất' là một điều khá chủ quan: D
Frederick The Fool

7

Hướng dẫn ARM "rbit" có thể làm điều đó với 1 chu kỳ cpu và 1 thanh ghi cpu bổ sung, không thể đánh bại.


6

Đây không phải là việc làm cho một con người! ... nhưng hoàn hảo cho một chiếc máy

Đây là năm 2015, 6 năm kể từ khi câu hỏi này lần đầu tiên được hỏi. Trình biên dịch đã trở thành bậc thầy của chúng tôi và công việc của chúng tôi là con người chỉ để giúp đỡ họ. Vì vậy, cách tốt nhất để đưa ra ý định của chúng tôi cho máy là gì?

Sự đảo ngược bit rất phổ biến đến nỗi bạn phải tự hỏi tại sao ISA ngày càng phát triển của x86 không bao gồm một hướng dẫn để thực hiện điều đó một lần.

Lý do: nếu bạn đưa ra ý định ngắn gọn thực sự của mình cho trình biên dịch, việc đảo ngược bit chỉ nên thực hiện ~ 20 chu kỳ CPU . Hãy để tôi chỉ cho bạn cách tạo đảo ngược () và sử dụng nó:

#include <inttypes.h>
#include <stdio.h>

uint64_t reverse(const uint64_t n,
                 const uint64_t k)
{
        uint64_t r, i;
        for (r = 0, i = 0; i < k; ++i)
                r |= ((n >> i) & 1) << (k - i - 1);
        return r;
}

int main()
{
        const uint64_t size = 64;
        uint64_t sum = 0;
        uint64_t a;
        for (a = 0; a < (uint64_t)1 << 30; ++a)
                sum += reverse(a, size);
        printf("%" PRIu64 "\n", sum);
        return 0;
}

Biên dịch chương trình mẫu này với phiên bản Clang> = 3.6, -O3, -march = bản địa (đã thử nghiệm với Haswell), cung cấp mã chất lượng tác phẩm nghệ thuật bằng cách sử dụng các hướng dẫn AVX2 mới, với thời gian chạy 11 giây xử lý ~ 1 tỷ đảo ngược (). Đó là ~ 10 ns mỗi lần đảo ngược (), với chu kỳ CPU 0,5 ns giả sử 2 GHz đưa chúng ta vào 20 chu kỳ CPU ngọt ngào.

  • Bạn có thể phù hợp với 10 đảo ngược () trong thời gian cần thiết để truy cập RAM một lần cho một mảng lớn!
  • Bạn có thể điều chỉnh 1 đảo ngược () trong thời gian cần thiết để truy cập bộ đệm L2 LUT hai lần.

Hãy cẩn thận: mã mẫu này sẽ giữ mức chuẩn trong một vài năm, nhưng cuối cùng nó sẽ bắt đầu hiển thị tuổi của nó khi trình biên dịch đủ thông minh để tối ưu hóa main () để chỉ in kết quả cuối cùng thay vì thực sự tính toán bất cứ điều gì. Nhưng bây giờ nó hoạt động trong hiển thị ngược ().


Bit-reversal is so common...Tôi không biết về điều đó. Tôi làm việc với mã liên quan đến dữ liệu ở cấp độ bit hầu như mỗi ngày và tôi không thể nhớ mình đã từng có nhu cầu cụ thể này. Trong những kịch bản nào bạn cần nó? - Không phải là vấn đề thú vị để tự giải quyết.
500 - Lỗi máy chủ nội bộ

@ 500-InternalServerError Tôi cuối cùng cần chức năng này nhiều lần trong suy luận ngữ pháp với các cấu trúc dữ liệu nhanh, ngắn gọn. Một cây nhị phân bình thường được mã hóa dưới dạng bitarray kết thúc bằng cách suy luận ngữ pháp theo thứ tự "endian lớn". Nhưng để khái quát hóa tốt hơn nếu bạn xây dựng một cây (bitarray) với các nút được hoán đổi xung quanh bởi hoán vị đảo ngược bit, các chuỗi ngữ pháp đã học nằm trong "endian nhỏ". Việc chuyển đổi đó cho phép bạn suy ra các chuỗi có độ dài thay vì các kích thước nguyên cố định. Tình huống này cũng xuất hiện rất nhiều trong FFT hiệu quả: xem en.wikipedia.org/wiki/Bit-reversal_permuting

1
Cảm ơn, bằng cách nào đó tôi đã xoay sở để xác định rằng FFT có thể liên quan đến câu trả lời của bạn :)
500 - Lỗi máy chủ nội bộ

Tại sao chỉ 20 chu kỳ? Kiến trúc nào? Điều này có đúng với tất cả các kiến ​​trúc VLIW siêu rộng của tương lai cho đến khi loài người và con cháu chúng ta chết không? Chỉ cần câu hỏi, không có câu trả lời ... downvote xuống địa ngục một lần nữa
Quonux


5

Tôi biết đó không phải là C mà là:

var1 dw 0f0f0
clc
     push ax
     push cx
     mov cx 16
loop1:
     shl var1
     shr ax
loop loop1
     pop ax
     pop cx

Điều này hoạt động với bit carry, vì vậy bạn cũng có thể lưu cờ


1
Tôi đoán bạn có thể sử dụng từ khóa asm , nó sẽ khá nhanh.
tom

Điều này thậm chí không hoạt động. Tôi nghĩ rằng bạn muốn rclchuyển CF vào var1, thay vì chỉ shlkhông đọc cờ. (Hoặc adc dx,dx). Ngay cả với bản sửa lỗi đó, điều này rất chậm một cách lố bịch, sử dụng loophướng dẫn chậm và lưu giữ var1trong bộ nhớ! Trên thực tế tôi nghĩ rằng điều này được cho là tạo ra đầu ra trong AX, nhưng nó tiết kiệm / khôi phục giá trị cũ của AX so với kết quả.
Peter Cordes

4

Thực hiện với bộ nhớ thấp và nhanh nhất.

private Byte  BitReverse(Byte bData)
    {
        Byte[] lookup = { 0, 8,  4, 12, 
                          2, 10, 6, 14 , 
                          1, 9,  5, 13,
                          3, 11, 7, 15 };
        Byte ret_val = (Byte)(((lookup[(bData & 0x0F)]) << 4) + lookup[((bData & 0xF0) >> 4)]);
        return ret_val;
    }

4

Chà, về cơ bản nó giống như "Reverse ()" đầu tiên nhưng nó là 64 bit và chỉ cần một mặt nạ ngay lập tức được tải từ luồng lệnh. GCC tạo mã mà không cần nhảy, vì vậy việc này sẽ khá nhanh.

#include <stdio.h>

static unsigned long long swap64(unsigned long long val)
{
#define ZZZZ(x,s,m) (((x) >>(s)) & (m)) | (((x) & (m))<<(s));
/* val = (((val) >>16) & 0xFFFF0000FFFF) | (((val) & 0xFFFF0000FFFF)<<16); */

val = ZZZZ(val,32,  0x00000000FFFFFFFFull );
val = ZZZZ(val,16,  0x0000FFFF0000FFFFull );
val = ZZZZ(val,8,   0x00FF00FF00FF00FFull );
val = ZZZZ(val,4,   0x0F0F0F0F0F0F0F0Full );
val = ZZZZ(val,2,   0x3333333333333333ull );
val = ZZZZ(val,1,   0x5555555555555555ull );

return val;
#undef ZZZZ
}

int main(void)
{
unsigned long long val, aaaa[16] =
 { 0xfedcba9876543210,0xedcba9876543210f,0xdcba9876543210fe,0xcba9876543210fed
 , 0xba9876543210fedc,0xa9876543210fedcb,0x9876543210fedcba,0x876543210fedcba9
 , 0x76543210fedcba98,0x6543210fedcba987,0x543210fedcba9876,0x43210fedcba98765
 , 0x3210fedcba987654,0x210fedcba9876543,0x10fedcba98765432,0x0fedcba987654321
 };
unsigned iii;

for (iii=0; iii < 16; iii++) {
    val = swap64 (aaaa[iii]);
    printf("A[]=%016llX Sw=%016llx\n", aaaa[iii], val);
    }
return 0;
}

4

Tôi tò mò sẽ nhanh như thế nào khi quay thô. Trên máy của tôi (i7 @ 2600), trung bình cho 1.500.150.000 lần lặp là 27.28 ns(trên một bộ ngẫu nhiên gồm 131.071 số nguyên 64 bit).

Ưu điểm: dung lượng bộ nhớ cần thiết rất ít và mã đơn giản. Tôi cũng sẽ nói nó không lớn lắm. Thời gian cần thiết là có thể dự đoán và không đổi cho bất kỳ đầu vào nào (128 phép toán SHift số học + 64 phép toán AND logic + 64 phép toán OR logic).

Tôi đã so sánh với thời gian tốt nhất mà @Matt J có được - người có câu trả lời được chấp nhận. Nếu tôi đọc chính xác câu trả lời của anh ta, điều tốt nhất anh ta có được là 0.631739vài giây cho các 1,000,000lần lặp lại, dẫn đến trung bình 631 nsmỗi vòng quay.

Đoạn mã tôi đã sử dụng là đoạn mã dưới đây:

unsigned long long reverse_long(unsigned long long x)
{
    return (((x >> 0) & 1) << 63) |
           (((x >> 1) & 1) << 62) |
           (((x >> 2) & 1) << 61) |
           (((x >> 3) & 1) << 60) |
           (((x >> 4) & 1) << 59) |
           (((x >> 5) & 1) << 58) |
           (((x >> 6) & 1) << 57) |
           (((x >> 7) & 1) << 56) |
           (((x >> 8) & 1) << 55) |
           (((x >> 9) & 1) << 54) |
           (((x >> 10) & 1) << 53) |
           (((x >> 11) & 1) << 52) |
           (((x >> 12) & 1) << 51) |
           (((x >> 13) & 1) << 50) |
           (((x >> 14) & 1) << 49) |
           (((x >> 15) & 1) << 48) |
           (((x >> 16) & 1) << 47) |
           (((x >> 17) & 1) << 46) |
           (((x >> 18) & 1) << 45) |
           (((x >> 19) & 1) << 44) |
           (((x >> 20) & 1) << 43) |
           (((x >> 21) & 1) << 42) |
           (((x >> 22) & 1) << 41) |
           (((x >> 23) & 1) << 40) |
           (((x >> 24) & 1) << 39) |
           (((x >> 25) & 1) << 38) |
           (((x >> 26) & 1) << 37) |
           (((x >> 27) & 1) << 36) |
           (((x >> 28) & 1) << 35) |
           (((x >> 29) & 1) << 34) |
           (((x >> 30) & 1) << 33) |
           (((x >> 31) & 1) << 32) |
           (((x >> 32) & 1) << 31) |
           (((x >> 33) & 1) << 30) |
           (((x >> 34) & 1) << 29) |
           (((x >> 35) & 1) << 28) |
           (((x >> 36) & 1) << 27) |
           (((x >> 37) & 1) << 26) |
           (((x >> 38) & 1) << 25) |
           (((x >> 39) & 1) << 24) |
           (((x >> 40) & 1) << 23) |
           (((x >> 41) & 1) << 22) |
           (((x >> 42) & 1) << 21) |
           (((x >> 43) & 1) << 20) |
           (((x >> 44) & 1) << 19) |
           (((x >> 45) & 1) << 18) |
           (((x >> 46) & 1) << 17) |
           (((x >> 47) & 1) << 16) |
           (((x >> 48) & 1) << 15) |
           (((x >> 49) & 1) << 14) |
           (((x >> 50) & 1) << 13) |
           (((x >> 51) & 1) << 12) |
           (((x >> 52) & 1) << 11) |
           (((x >> 53) & 1) << 10) |
           (((x >> 54) & 1) << 9) |
           (((x >> 55) & 1) << 8) |
           (((x >> 56) & 1) << 7) |
           (((x >> 57) & 1) << 6) |
           (((x >> 58) & 1) << 5) |
           (((x >> 59) & 1) << 4) |
           (((x >> 60) & 1) << 3) |
           (((x >> 61) & 1) << 2) |
           (((x >> 62) & 1) << 1) |
           (((x >> 63) & 1) << 0);
}

@greybeard Tôi không chắc là tôi hiểu câu hỏi của bạn.
marian adam

cảm ơn vì đã nhận thấy lỗi, tôi đã sửa mẫu mã được cung cấp.
marian adam

3

Bạn có thể muốn sử dụng thư viện mẫu tiêu chuẩn. Nó có thể chậm hơn mã đã đề cập ở trên. Tuy nhiên, dường như tôi rõ ràng và dễ hiểu hơn.

 #include<bitset>
 #include<iostream>


 template<size_t N>
 const std::bitset<N> reverse(const std::bitset<N>& ordered)
 {
      std::bitset<N> reversed;
      for(size_t i = 0, j = N - 1; i < N; ++i, --j)
           reversed[j] = ordered[i];
      return reversed;
 };


 // test the function
 int main()
 {
      unsigned long num; 
      const size_t N = sizeof(num)*8;

      std::cin >> num;
      std::cout << std::showbase << std::hex;
      std::cout << "ordered  = " << num << std::endl;
      std::cout << "reversed = " << reverse<N>(num).to_ulong()  << std::endl;
      std::cout << "double_reversed = " << reverse<N>(reverse<N>(num)).to_ulong() << std::endl;  
 }

2

Chung

Mã C. Sử dụng num dữ liệu đầu vào 1 byte làm ví dụ.

    unsigned char num = 0xaa;   // 1010 1010 (aa) -> 0101 0101 (55)
    int s = sizeof(num) * 8;    // get number of bits
    int i, x, y, p;
    int var = 0;                // make var data type to be equal or larger than num

    for (i = 0; i < (s / 2); i++) {
        // extract bit on the left, from MSB
        p = s - i - 1;
        x = num & (1 << p);
        x = x >> p;
        printf("x: %d\n", x);

        // extract bit on the right, from LSB
        y = num & (1 << i);
        y = y >> i;
        printf("y: %d\n", y);

        var = var | (x << i);       // apply x
        var = var | (y << p);       // apply y
    }

    printf("new: 0x%x\n", new);

Câu hỏi yêu cầu "hiệu quả nhất", không phải "đơn giản / đơn giản".
Peter Cordes

1

Làm thế nào về những điều sau đây:

    uint reverseMSBToLSB32ui(uint input)
    {
        uint output = 0x00000000;
        uint toANDVar = 0;
        int places = 0;

        for (int i = 1; i < 32; i++)
        {
            places = (32 - i);
            toANDVar = (uint)(1 << places);
            output |= (uint)(input & (toANDVar)) >> places;

        }


        return output;
    }

Nhỏ và dễ dàng (mặc dù, chỉ 32 bit).


Câu hỏi yêu cầu "hiệu quả nhất"; chúng ta có thể loại trừ vòng lặp 32 lần. (Và đặc biệt là không thay đổi mặt nạ cũng như phải chuyển kết quả xuống LSB)
Peter Cordes

1

Tôi nghĩ rằng đây là một trong những cách đơn giản nhất để đảo ngược bit. xin vui lòng cho tôi biết nếu có bất kỳ lỗ hổng trong logic này. về cơ bản trong logic này, chúng tôi kiểm tra giá trị của bit ở vị trí. đặt bit nếu giá trị là 1 trên vị trí đảo ngược.

void bit_reverse(ui32 *data)
{
  ui32 temp = 0;    
  ui32 i, bit_len;    
  {    
   for(i = 0, bit_len = 31; i <= bit_len; i++)   
   {    
    temp |= (*data & 1 << i)? (1 << bit_len-i) : 0;    
   }    
   *data = temp;    
  }    
  return;    
}    

Câu hỏi yêu cầu "hiệu quả nhất", không phải "đơn giản / đơn giản".
Peter Cordes

0
unsigned char ReverseBits(unsigned char data)
{
    unsigned char k = 0, rev = 0;

    unsigned char n = data;

    while(n)

    {
        k = n & (~(n - 1));
        n &= (n - 1);
        rev |= (128 / k);
    }
    return rev;
}

Thú vị, nhưng phân chia theo một biến thời gian chạy là chậm. kluôn là sức mạnh của 2, nhưng trình biên dịch có thể sẽ không chứng minh điều đó và biến nó thành quét / dịch chuyển bit.
Peter Cordes

0

Tôi nghĩ rằng phương pháp đơn giản nhất mà tôi biết sau đây. MSBlà đầu vào và LSBlà đầu ra 'đảo ngược':

unsigned char rev(char MSB) {
    unsigned char LSB=0;  // for output
    _FOR(i,0,8) {
        LSB= LSB << 1;
        if(MSB&1) LSB = LSB | 1;
        MSB= MSB >> 1;
    }
    return LSB;
}

//    It works by rotating bytes in opposite directions. 
//    Just repeat for each byte.

0
// Purpose: to reverse bits in an unsigned short integer 
// Input: an unsigned short integer whose bits are to be reversed
// Output: an unsigned short integer with the reversed bits of the input one
unsigned short ReverseBits( unsigned short a )
{
     // declare and initialize number of bits in the unsigned short integer
     const char num_bits = sizeof(a) * CHAR_BIT;

     // declare and initialize bitset representation of integer a
     bitset<num_bits> bitset_a(a);          

     // declare and initialize bitset representation of integer b (0000000000000000)
     bitset<num_bits> bitset_b(0);                  

     // declare and initialize bitset representation of mask (0000000000000001)
     bitset<num_bits> mask(1);          

     for ( char i = 0; i < num_bits; ++i )
     {
          bitset_b = (bitset_b << 1) | bitset_a & mask;
          bitset_a >>= 1;
     }

     return (unsigned short) bitset_b.to_ulong();
}

void PrintBits( unsigned short a )
{
     // declare and initialize bitset representation of a
     bitset<sizeof(a) * CHAR_BIT> bitset(a);

     // print out bits
     cout << bitset << endl;
}


// Testing the functionality of the code

int main ()
{
     unsigned short a = 17, b;

     cout << "Original: "; 
     PrintBits(a);

     b = ReverseBits( a );

     cout << "Reversed: ";
     PrintBits(b);
}

// Output:
Original: 0000000000010001
Reversed: 1000100000000000

0

Một giải pháp dựa trên vòng lặp khác thoát ra nhanh chóng khi số lượng thấp (trong C ++ cho nhiều loại)

template<class T>
T reverse_bits(T in) {
    T bit = static_cast<T>(1) << (sizeof(T) * 8 - 1);
    T out;

    for (out = 0; bit && in; bit >>= 1, in >>= 1) {
        if (in & 1) {
            out |= bit;
        }
    }
    return out;
}

hoặc trong C cho một số nguyên không dấu

unsigned int reverse_bits(unsigned int in) {
    unsigned int bit = 1u << (sizeof(T) * 8 - 1);
    unsigned int out;

    for (out = 0; bit && in; bit >>= 1, in >>= 1) {
        if (in & 1)
            out |= bit;
    }
    return out;
}

0

Có vẻ như nhiều bài viết khác quan tâm đến tốc độ (tức là tốt nhất = nhanh nhất). Còn sự đơn giản thì sao? Xem xét:

char ReverseBits(char character) {
    char reversed_character = 0;
    for (int i = 0; i < 8; i++) {
        char ith_bit = (c >> i) & 1;
        reversed_character |= (ith_bit << (sizeof(char) - 1 - i));
    }
    return reversed_character;
}

và hy vọng rằng trình biên dịch thông minh sẽ tối ưu hóa cho bạn.

Nếu bạn muốn đảo ngược danh sách bit dài hơn (chứa sizeof(char) * nbit), bạn có thể sử dụng chức năng này để nhận:

void ReverseNumber(char* number, int bit_count_in_number) {
    int bytes_occupied = bit_count_in_number / sizeof(char);      

    // first reverse bytes
    for (int i = 0; i <= (bytes_occupied / 2); i++) {
        swap(long_number[i], long_number[n - i]);
    }

    // then reverse bits of each individual byte
    for (int i = 0; i < bytes_occupied; i++) {
         long_number[i] = ReverseBits(long_number[i]);
    }
}

Điều này sẽ đảo ngược [10000000, 10101010] thành [01010101, 00000001].


Bạn có 3 ca trong vòng lặp bên trong. Lưu một với ith_bit = (c >> i) & 1. Đồng thời lưu SUB bằng cách dịch chuyển reversed_charthay vì dịch chuyển bit, trừ khi bạn hy vọng nó sẽ biên dịch trên x86 thành sub something/ bts reg,regđể đặt bit thứ n trong thanh ghi đích.
Peter Cordes

-1

Đảo ngược bit trong mã giả

nguồn -> byte được đảo ngược đích b00101100 -> đảo ngược, cũng cần phải là loại không dấu để bit dấu không bị đẩy xuống

sao chép vào temp để bản gốc không bị ảnh hưởng, cũng cần phải là loại không dấu để bit dấu không bị dịch chuyển trong tự động

bytecopy = b0010110

LOOP8: // thực hiện kiểm tra 8 lần này nếu nội dung bằng <0 (âm)

    set bit8 (msb) of reversed = reversed | b10000000 

else do not set bit8

shift bytecopy left 1 place
bytecopy = bytecopy << 1 = b0101100 result

shift result right 1 place
reversed = reversed >> 1 = b00000000
8 times no then up^ LOOP8
8 times yes then done.

-1

Giải pháp đơn giản của tôi

BitReverse(IN)
    OUT = 0x00;
    R = 1;      // Right mask   ...0000.0001
    L = 0;      // Left mask    1000.0000...
    L = ~0; 
    L = ~(i >> 1);
    int size = sizeof(IN) * 4;  // bit size

    while(size--){
        if(IN & L) OUT = OUT | R; // start from MSB  1000.xxxx
        if(IN & R) OUT = OUT | L; // start from LSB  xxxx.0001
        L = L >> 1;
        R = R << 1; 
    }
    return OUT;

1
Có gì i? Ngoài ra, ma thuật đó là * 4gì? Phải CHAR_BIT / 2không
Peter Cordes

-1

Đây là 32 bit, chúng ta cần thay đổi kích thước nếu xem xét 8 bit.

    void bitReverse(int num)
    {
        int num_reverse = 0;
        int size = (sizeof(int)*8) -1;
        int i=0,j=0;
        for(i=0,j=size;i<=size,j>=0;i++,j--)
        {
            if((num >> i)&1)
            {
                num_reverse = (num_reverse | (1<<j));
            }
        }
        printf("\n rev num = %d\n",num_reverse);
    }

Đọc số nguyên đầu vào "num" theo thứ tự LSB-> MSB và lưu trữ bằng num numverse theo thứ tự MSB-> LSB.


1
Bạn nên thêm một lời giải thích cho mã để nó được hiểu dễ dàng hơn.
Tunaki

-3
int bit_reverse(int w, int bits)
{
    int r = 0;
    for (int i = 0; i < bits; i++)
    {
        int bit = (w & (1 << i)) >> i;
        r |= bit << (bits - i - 1);
    }
    return r;
}

3
Nói chung, câu trả lời sẽ hữu ích hơn nhiều nếu chúng bao gồm một lời giải thích về những gì mã được dự định làm và tại sao điều đó giải quyết vấn đề.
IKavanagh
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.