我怎样才能有效地洗牌?

Posted

技术标签:

【中文标题】我怎样才能有效地洗牌?【英文标题】:How can I shuffle bits efficiently? 【发布时间】:2014-11-03 05:48:32 【问题描述】:

我需要对一个 16 位无符号整数进行洗牌,使偶数索引位于低字节,奇数索引位于高字节。

input:
fedcba98 76543210 (contiguously numbered)

output:
fdb97531 eca86420 (even and odd separated)

我的代码现在是这样的:

typedef unsigned short u16;

u16 segregate(u16 x)

    u16 g = (x & 0x0001);
    u16 h = (x & 0x0004) >> 1;
    u16 i = (x & 0x0010) >> 2;
    u16 j = (x & 0x0040) >> 3;
    u16 k = (x & 0x0100) >> 4;
    u16 l = (x & 0x0400) >> 5;
    u16 m = (x & 0x1000) >> 6;
    u16 n = (x & 0x4000) >> 7;

    u16 o = (x & 0x0002) << 7;
    u16 p = (x & 0x0008) << 6;
    u16 q = (x & 0x0020) << 5;
    u16 r = (x & 0x0080) << 4;
    u16 s = (x & 0x0200) << 3;
    u16 t = (x & 0x0800) << 2;
    u16 u = (x & 0x2000) << 1;
    u16 v = (x & 0x8000);

    return g | h | i | j | k | l | m | n | o | p | q | r | s | t | u | v;

我想知道是否有比简单地提取和移动每个单独的位更优雅的解决方案?

【问题讨论】:

"看起来很慢" 放一个profiler就可以了。这会告诉你它是否真的很慢。 它看起来很慢,但它实际上对于您的特定应用程序来说太慢了吗?测量两次,切割一次。 Related,我想。 只需将“0 2 4 6 8 10 12 14 1 3 5 7 9 11 13 15”输入此页面:" Code generator for bit permutations"。 似乎按预期工作:ideone.com/05oXgr 【参考方案1】:

其他人展示的表格方法是最便携的版本,可能相当快。

如果您想利用特殊指令集,还有其他一些选项。例如,对于 Intel Haswell 及更高版本,可以使用以下方法(需要 BMI2 指令集扩展):

unsigned segregate_bmi (unsigned arg)

  unsigned oddBits  = _pext_u32(arg,0x5555);
  unsigned evenBits = _pext_u32(arg,0xaaaa);
  return (oddBits | (evenBits << 8));

【讨论】:

酷指令! “对于掩码中设置的每个位,内在函数从第一个源操作数中提取相应的位,并将它们写入目标的连续低位。目标的剩余高位设置为 0。” (说Intel)。我敢打赌这是为了一些图形处理。 @Jongware 是的。它进行各种位域提取。与它的兄弟指令 pdep 一起,您可以非常快速地进行任何类型的排列和位洗牌。 IsProcessorFeaturePresent 检查吗? (cpuid 在多处理器上不可靠)【参考方案2】:

有一个非常方便的网络资源可以帮助解决许多位排列问题:Code generator for bit permutations。在这种特殊情况下,向此页面提供“0 2 4 6 8 10 12 14 1 3 5 7 9 11 13 15”会产生非常快的代码。

很遗憾,此代码生成器无法生成 64 位代码(尽管任何人都可以下载源代码并添加此选项)。因此,如果我们需要使用 64 位指令并行执行 4 个排列,我们必须手动将所有涉及的位掩码扩展到 64 位:

uint64_t bit_permute_step(uint64_t x, uint64_t m, unsigned shift) 
  uint64_t t;
  t = ((x >> shift) ^ x) & m;
  x = (x ^ t) ^ (t << shift);
  return x;


uint64_t segregate4(uint64_t x)
 // generated by http://programming.sirrida.de/calcperm.php, extended to 64-bit
  x = bit_permute_step(x, 0x2222222222222222ull, 1);
  x = bit_permute_step(x, 0x0c0c0c0c0c0c0c0cull, 2);
  x = bit_permute_step(x, 0x00f000f000f000f0ull, 4);
  return x;

使用 SSE 指令可以进一步提高并行度(一次 8 或 16 个排列)。 (并且最新版本的 gcc 可以自动矢量化这段代码)。

如果不需要并行性并且程序的其他部分没有广泛使用数据缓存,则更好的选择是使用查找表。其他答案中已经讨论了各种LUT方法,这里还可以说更多:

    16 位字的第一个和最后一个位永远不会被置换,我们只需要对 1..14 位进行混洗。因此(如果我们想通过单个 LUT 访问来执行任务)拥有 16K 条目的 LUT 就足够了,这意味着 32K 内存。 我们可以将查表和计算方法结合起来。单个 256 字节表中的两次查找可以分别打乱每个源字节。之后我们只需要交换两个中间的 4 位半字节。这样可以使查找表保持较小,仅使用 2 次内存访问,并且不需要太多计算(即平衡计算和内存访问)。

这是第二种方法的实现:

#define B10(x)          x+0x00,      x+0x10,      x+0x01,      x+0x11
#define B32(x)      B10(x+0x00), B10(x+0x20), B10(x+0x02), B10(x+0x22)
#define B54(x)      B32(x+0x00), B32(x+0x40), B32(x+0x04), B32(x+0x44)
uint8_t lut[256] = B54(  0x00), B54(  0x80), B54(  0x08), B54(  0x88);
#undef B54
#undef B32
#undef B10

uint_fast16_t segregateLUT(uint_fast16_t x)

  uint_fast16_t low = lut[x & 0x00ff];
  low |= low << 4;
  uint_fast16_t high = lut[x >> 8] << 4;
  high |= high << 4;
  return (low & 0x0f0f) | (high & 0xf0f0);

但最快的方法(如果可移植性不是问题)是使用来自 BMI2 指令集 as noted by Nils Pipenbrinck 的 pext 指令。使用一对 64 位 pext,我们可以并行执行 4 个 16 位随机播放。由于pext 指令正是针对这种位排列而设计的,因此这种方法很容易胜过所有其他方法。

【讨论】:

【参考方案3】:

您可以为 16 位数字的每个字节使用一个 256 字节的表格,以便满足您的偶数/奇数条件。手动编码表条目(或使用您已有的算法)来创建表,然后将在编译时完成改组。这本质上是一个转换表的概念。

【讨论】:

我同意。这是最快的洗牌方式。您可以使用数组或地图,这将是一个 O(1) 操作。 (旁注:应该始终运行基准测试,尤其是在如此低的级别:使用查找表而不是少数 OR/SHIFT 指令可能对性能产生负面影响由于缓存...)【参考方案4】:

您可以为 16 位数字的每个字节使用一个 256 字节的表,精心设计以满足您的偶数/奇数条件。

啊,是的,查找表来救援 :) 你甚至可以用一张表和一个额外的班次来做:

u16 every_other[256] = 
0x00, 0x01, 0x00, 0x01, 0x02, 0x03, 0x02, 0x03, 
0x00, 0x01, 0x00, 0x01, 0x02, 0x03, 0x02, 0x03, 
0x04, 0x05, 0x04, 0x05, 0x06, 0x07, 0x06, 0x07, 
0x04, 0x05, 0x04, 0x05, 0x06, 0x07, 0x06, 0x07, 
0x00, 0x01, 0x00, 0x01, 0x02, 0x03, 0x02, 0x03, 
0x00, 0x01, 0x00, 0x01, 0x02, 0x03, 0x02, 0x03, 
0x04, 0x05, 0x04, 0x05, 0x06, 0x07, 0x06, 0x07, 
0x04, 0x05, 0x04, 0x05, 0x06, 0x07, 0x06, 0x07, 
0x08, 0x09, 0x08, 0x09, 0x0a, 0x0b, 0x0a, 0x0b, 
0x08, 0x09, 0x08, 0x09, 0x0a, 0x0b, 0x0a, 0x0b, 
0x0c, 0x0d, 0x0c, 0x0d, 0x0e, 0x0f, 0x0e, 0x0f, 
0x0c, 0x0d, 0x0c, 0x0d, 0x0e, 0x0f, 0x0e, 0x0f, 
0x08, 0x09, 0x08, 0x09, 0x0a, 0x0b, 0x0a, 0x0b, 
0x08, 0x09, 0x08, 0x09, 0x0a, 0x0b, 0x0a, 0x0b, 
0x0c, 0x0d, 0x0c, 0x0d, 0x0e, 0x0f, 0x0e, 0x0f, 
0x0c, 0x0d, 0x0c, 0x0d, 0x0e, 0x0f, 0x0e, 0x0f, 
0x00, 0x01, 0x00, 0x01, 0x02, 0x03, 0x02, 0x03, 
0x00, 0x01, 0x00, 0x01, 0x02, 0x03, 0x02, 0x03, 
0x04, 0x05, 0x04, 0x05, 0x06, 0x07, 0x06, 0x07, 
0x04, 0x05, 0x04, 0x05, 0x06, 0x07, 0x06, 0x07, 
0x00, 0x01, 0x00, 0x01, 0x02, 0x03, 0x02, 0x03, 
0x00, 0x01, 0x00, 0x01, 0x02, 0x03, 0x02, 0x03, 
0x04, 0x05, 0x04, 0x05, 0x06, 0x07, 0x06, 0x07, 
0x04, 0x05, 0x04, 0x05, 0x06, 0x07, 0x06, 0x07, 
0x08, 0x09, 0x08, 0x09, 0x0a, 0x0b, 0x0a, 0x0b, 
0x08, 0x09, 0x08, 0x09, 0x0a, 0x0b, 0x0a, 0x0b, 
0x0c, 0x0d, 0x0c, 0x0d, 0x0e, 0x0f, 0x0e, 0x0f, 
0x0c, 0x0d, 0x0c, 0x0d, 0x0e, 0x0f, 0x0e, 0x0f, 
0x08, 0x09, 0x08, 0x09, 0x0a, 0x0b, 0x0a, 0x0b, 
0x08, 0x09, 0x08, 0x09, 0x0a, 0x0b, 0x0a, 0x0b, 
0x0c, 0x0d, 0x0c, 0x0d, 0x0e, 0x0f, 0x0e, 0x0f, 
0x0c, 0x0d, 0x0c, 0x0d, 0x0e, 0x0f, 0x0e, 0x0f;

u16 segregate(u16 x)

    return every_other[x & 0xff]
         | every_other[(x >> 8)] << 4
         | every_other[(x >> 1) & 0xff] << 8
         | every_other[(x >> 9)] << 12;

【讨论】:

或者您可以将其设为 256 uint16_t 和 return every_other[x&amp;0xff]|every_other[x&gt;&gt;8]&lt;&lt;4 的表。 每行重复 8 次。我们能做得更好吗? @NickyC 由于该表将字节映射到半字节,因此值必然会重复。 @FredOverflow 好的,有充分的理由重复,已经足够了。【参考方案5】:

表。但在编译时生成它们!

namespace details 
  constexpr uint8_t bit( unsigned byte, unsigned n ) 
    return (byte>>n)&1;
  
  constexpr uint8_t even_bits(uint8_t byte) 
    return bit(byte, 0) | (bit(byte, 2)<<1) | (bit(byte, 4)<<2) | (bit(byte, 6)<<3);
  
  constexpr uint8_t odd_bits(uint8_t byte) 
    return even_bits(byte/2);
  
  template<unsigned...>struct indexesusing type=indexes;;
  template<unsigned Max,unsigned...Is>struct make_indexes:make_indexes<Max-1,Max-1,Is...>;
  template<unsigned...Is>struct make_indexes<0,Is...>:indexes<Is...>;
  template<unsigned Max>using make_indexes_t=typename make_indexes<Max>::type;

  template<unsigned...Is>
  constexpr std::array< uint8_t, 256 > even_bit_table( indexes<Is...> ) 
    return  even_bits(Is)... ;
  
  template<unsigned...Is>
  constexpr std::array< uint8_t, 256 > odd_bit_table( indexes<Is...> ) 
    return  odd_bits(Is)... ;
  
  constexpr std::array< uint8_t, 256 > even_bit_table() 
    return even_bit_table( make_indexes_t<256> );
  
  constexpr std::array< uint8_t, 256 > odd_bit_table() 
    return odd_bit_table( make_indexes_t<256> );
  

  static constexpr auto etable = even_bit_table();
  static constexpr auto otable = odd_bit_table();


uint8_t constexpr even_bits( uint16_t in ) 
  return details::etable[(uint8_t)in] | ((details::etable[(uint8_t)(in>>8)])<<4);

uint8_t constexpr odd_bits( uint16_t in ) 
  return details::otable[(uint8_t)in] | ((details::otable[(uint8_t)(in>>8)])<<4);

live example

【讨论】:

@dyp 没有理由。好吧,unsigned byte 有点有趣,但它可能和 ... 函数一样有趣?运行?范围。 (什么叫非模板参数?) @dyp 好吧,我重写了实时示例,并找到了一个原因:正如所写,odd_bits 将始终在O(1) 中运行,无论是uint16_t 还是&lt;unsigned byte&gt; 版本。当然,&lt;unsigned byte&gt; 版本也不好用。所以我把所有东西都塞进了details O(1)? IIRC,我可怜的 8 位 AVR 不能在 O(1) 中移动;) @dyp 它可以在 O(1) 中精确地移动 4 和 8 步!现在,如果索引较大时执行 8 位数组查找需要不同的时间......(如果您的输入数据限制为 16 位,则一切都是 O(1))【参考方案6】:

您对 64 位的偶数位和奇数位随机播放的答案不准确。要将 16 位解决方案扩展到 64 位解决方案,我们不仅需要扩展掩码,还要覆盖从 1 一直到 16 的交换间隔:

x = bit_permute_step(x, 0x2222222222222222, 1);
x = bit_permute_step(x, 0x0c0c0c0c0c0c0c0c, 2);
x = bit_permute_step(x, 0x00f000f000f000f0, 4);
**x = bit_permute_step(x, 0x0000ff000000ff00, 8);
x = bit_permute_step(x, 0x00000000ffff0000, 16);**

【讨论】:

【参考方案7】:

赞成做空:

unsigned short segregate(unsigned short x)

    x = (x & 0x9999) | (x >> 1 & 0x2222) | (x << 1 & 0x4444);
    x = (x & 0xC3C3) | (x >> 2 & 0x0C0C) | (x << 2 & 0x3030);
    x = (x & 0xF00F) | (x >> 4 & 0x00F0) | (x << 4 & 0x0F00);
    return x;

【讨论】:

以上是关于我怎样才能有效地洗牌?的主要内容,如果未能解决你的问题,请参考以下文章

我怎样才能有效地做到这一点? [关闭]

我怎样才能有效地计时一个只有几个周期长的函数的执行时间?

我怎样才能有效地找到在预算范围内并最大化效用的活动子集?

我怎样才能更好地有条件地渲染我的组件?

我怎样才能干净地规范化数据,然后在以后“取消规范化”它?

我怎样才能将双 splat 论点折叠成虚无?