如何在位图中的位之间插入零?

时间:2011-01-04 20:11:10

标签: c optimization assembly bit-manipulation

我有一些性能很重的代码执行位操作。它可以简化为以下明确定义的问题:

给定一个13位位图,构造一个26位位图,其中包含在偶数位置间隔的原始位

举例说明:

0000000000000000000abcdefghijklm (input, 32 bits)
0000000a0b0c0d0e0f0g0h0i0j0k0l0m (output, 32 bits)

我目前在C中以下列方式实现它:

if (input & (1 << 12))
    output |= 1 << 24;
if (input & (1 << 11))
    output |= 1 << 22;
if (input & (1 << 10))
    output |= 1 << 20;
...

我的编译器(MS Visual Studio)将其转换为以下内容:

test        eax,1000h
jne         0064F5EC
or          edx,1000000h
... (repeated 13 times with minor differences in constants)

我想知道我是否可以更快地完成任务。我希望我的代码用C语言编写,但是可以切换到汇编语言。

  • 我可以使用一些MMX / SSE指令一次处理所有位吗?
  • 也许我可以使用乘法? (乘以0x11111111或其他一些神奇的常数)
  • 使用条件设置指令(SETcc)而不是条件跳转指令会更好吗?如果是,我如何让编译器为我生成这样的代码?
  • 任何其他想法如何让它更快?
  • 任何想法如何进行逆位图转换(我必须实现它,对它不那么重要)?

11 个答案:

答案 0 :(得分:9)

有一种聪明的方法可以在这里做到这一点。实际上 解决了一个稍微更普遍的位改组问题。你的问题有一个 输入:

+---------------+---------------+---------------+---------------+
|0 0 0 0 0 0 0 0|0 0 0 0 0 0 0 0|0 0 0 a b c d e|f g h i j k l m|
+---------------+---------------+---------------+---------------+

....但是让我们考虑所有的比特:

+---------------+---------------+---------------+---------------+
|A B C D E F G H|I J K L M N O P|Q R S a b c d e|f g h i j k l m|
+---------------+---------------+---------------+---------------+

并尝试将它们全部交错:

+---------------+---------------+---------------+---------------+
|A Q B R C S D a|E b F c G d H e|I f J g K h L i|M j N k O l P m|
+---------------+---------------+---------------+---------------+

第一步,考虑输入的中间部分:

bit 31        24              16               8               0
 v             v               v               v               v
+---------------+---------------+---------------+---------------+
|               |I J K L M N O P|Q R S a b c d e|               |
+---------------+---------------+---------------+---------------+

构造8位值:{I^QJ^RK^SL^aM^bN^c,{{1 },O^d}。

如果我们将这个8位值与位[15:8]异或,并且还进行异或 与位[23:16]相同的8位值,我们将交换中间的两个字节:for 例如,第23位(原P^e)将变为I和第15位 (原I ^ (I^Q) = Q)将成为Q

要做到这一点:Q ^ (I^Q) = I

tmp = (input ^ (input >> 8)) & 0x0000ff00;

现在我们需要的8位值是位[15:8],其他所有位都是0。 现在我们可以用

进行交换
+---------------+---------------+---------------+---------------+
|A B C D E F G H|I J K L M N O P|Q R S a b c d e|f g h i j k l m| input
+---------------+---------------+---------------+---------------+
                            exclusive-OR with:
+---------------+---------------+---------------+---------------+
|0 0 0 0 0 0 0 0|A B C D E F G H|I J K L M N O P|Q R S a b c d e| input >> 8
+---------------+---------------+---------------+---------------+

                             -->|want these bits|<--

 mask (bitwise AND) with 0x0000ff00:
+---------------+---------------+---------------+---------------+
|0 0 0 0 0 0 0 0|0 0 0 0 0 0 0 0|1 1 1 1 1 1 1 1|0 0 0 0 0 0 0 0| 0x0000ff00
+---------------+---------------+---------------+---------------+

导致:

input ^= (tmp ^ (tmp << 8));

下一步,分而治之......执行类似的中间交换 左半边的位:

+---------------+---------------+---------------+---------------+
|A B C D E F G H|Q R S a b c d e|I J K L M N O P|f g h i j k l m| input
+---------------+---------------+---------------+---------------+

......和右半边:

+---------------+---------------+---------------+---------------+
|A B C D E F G H|Q R S a b c d e|               |               |
+---------------+---------------+---------------+---------------+
             becomes
+---------------+---------------+---------------+---------------+
|A B C D Q R S a|E F G H b c d e|               |               |
+---------------+---------------+---------------+---------------+

我们可以使用与第一步完全相同的技巧,因为我们想要 在32位字的两个16位半部分执行完全相同的操作, 我们可以并行完成:

+---------------+---------------+---------------+---------------+
|               |               |I J K L M N O P|f g h i j k l m|
+---------------+---------------+---------------+---------------+
                                             becomes
+---------------+---------------+---------------+---------------+
|               |               |I J K L f g h i|M N O P j k l m|
+---------------+---------------+---------------+---------------+

构造我们将用于交换的两对4位,然后

tmp = (input ^ (input >> 4)) & 0x00f000f0;

实际上是交换。

我们可以继续应用相同的原则,直到交换完成。 在每个点参与交换的位标有input ^= (tmp ^ (tmp << 4));

#

代码:

+---------------+---------------+---------------+---------------+
|A B C D E F G H|I J K L M N O P|Q R S a b c d e|f g h i j k l m|
+---------------+---------------+---------------+---------------+
                 ###############/###############
+---------------+---------------+---------------+---------------+
|A B C D E F G H|Q R S a b c d e|I J K L M N O P|f g h i j k l m|
+---------------+---------------+---------------+---------------+
         #######/#######                 #######/#######
+---------------+---------------+---------------+---------------+
|A B C D Q R S a|E F G H b c d e|I J K L f g h i|M N O P j k l m|
+---------------+---------------+---------------+---------------+
     ###/###         ###/###         ###/###         ###/###
+---------------+---------------+---------------+---------------+
|A B Q R C D S a|E F b c G H d e|I J f g K L h i|M N j k O P l m|
+---------------+---------------+---------------+---------------+
   #/#     #/#     #/#     #/#       #/#   #/#     #/#     #/#
+---------------+---------------+---------------+---------------+
|A Q B R C S D a|E b F c G d G e|I f J g K h L i|M j N k O l P m|
+---------------+---------------+---------------+---------------+

可以通过向后运行4个步骤来执行反向操作:

tmp = (input ^ (input >> 8)) & 0x0000ff00;
input ^= (tmp ^ (tmp << 8));
tmp = (input ^ (input >> 4)) & 0x00f000f0;
input ^= (tmp ^ (tmp << 4));
tmp = (input ^ (input >> 2)) & 0x0c0c0c0c;
input ^= (tmp ^ (tmp << 2));
tmp = (input ^ (input >> 1)) & 0x22222222;
input ^= (tmp ^ (tmp << 1));                    /* = output */

尽管您可以针对特定应用对此进行改进, 如果已知其他所有位为零:请参阅我对另一位的回答 问题here


作为最后一点,不要相信任何人对相对表现的看法 这里建议的任何方法都没有在中对它们进行基准测试 应用。 (特别是,大型查找表似乎要好得多 在简单的微基准测试中,它们实际上是在给定的实际中 应用程序,由于从缓存中驱逐大量其他数据, 这可能会对外循环产生负面影响。)

答案 1 :(得分:5)

使用查找表执行此操作。 2 ^ 13听起来像很多条目,但它们很容易适应CPU缓存。

哦,如果其他19位中有垃圾,你需要先将它们屏蔽掉。

答案 2 :(得分:4)

不要使用分支:

output =
   (input & 1)
   | ((input & 2) << 1)
   | ((input & 4) << 2)
   | ((input & 8) << 3)
   | ((input & 16) << 4)
   /* etc. */

这是一个可能更容易阅读/理解同一件事的版本:

output =
     ((input & (1 <<  0)) <<  0)
   | ((input & (1 <<  1)) <<  1)
   | ((input & (1 <<  2)) <<  2)
   | ((input & (1 <<  3)) <<  3)
   | ((input & (1 <<  4)) <<  4)
   | ((input & (1 <<  5)) <<  5)
   | ((input & (1 <<  6)) <<  6)
   | ((input & (1 <<  7)) <<  7)
   | ((input & (1 <<  8)) <<  8)
   | ((input & (1 <<  9)) <<  9)
   | ((input & (1 << 10)) << 10)
   | ((input & (1 << 11)) << 11)
   | ((input & (1 << 12)) << 12);

答案 3 :(得分:4)

你可以这样做:

; eax = input bits
shrd edx,eax,2
shr eax,1
shrd edx,eax,2
shr eax,1
shrd edx,eax,2
shr eax,1
shrd edx,eax,2
shr eax,1
shrd edx,eax,2
shr eax,1
shrd edx,eax,2
shr eax,1
shrd edx,eax,2
shr eax,1
shrd edx,eax,2
shr eax,1
shrd edx,eax,2
shr eax,1
shrd edx,eax,2
shr eax,1
shrd edx,eax,2
shr eax,1
shrd edx,eax,2
shr eax,1
shrd edx,eax,8
and edx,0x01555555
; edx = output

答案 4 :(得分:4)

我会给出一个没有条件的算法(只有加法和按位运算),我相信这会比你当前的解决方案更快。

这是13位的C代码。下面是一个说明该方法如何适用于3位的说明,我希望这种概括是明确的。

(注意:代码是循环展开的。一个好的编译器会为你做这个,所以你可以将它压缩成一个循环。)

unsigned mask, output;
unsigned x = input;

mask = ((1<<13)-1) << 13;
x = (x + mask) & ~mask;

mask = ((1<<12)-1) << 12;
x = (x + mask) & ~mask;

...

mask = ((1<<3)-1) << 3;
x = (x + mask) & ~mask;

mask = ((1<<2)-1) << 2;
x = (x + mask) & ~mask;

mask = ((1<<1)-1) << 1;
x = (x + mask) & ~mask;

output = x;

unsigned mask, output; unsigned x = input; mask = ((1<<13)-1) << 13; x = (x + mask) & ~mask; mask = ((1<<12)-1) << 12; x = (x + mask) & ~mask; ... mask = ((1<<3)-1) << 3; x = (x + mask) & ~mask; mask = ((1<<2)-1) << 2; x = (x + mask) & ~mask; mask = ((1<<1)-1) << 1; x = (x + mask) & ~mask; output = x;

现在,这是3位方法的解释。初始状态为'00abc'。首先将'a'两个位置向左移动,添加01100然后与10011进行AND运算(这恰好是前一个数字的按位NOT)。这是a = 0,1的工作原理(第一个箭头是加法,第二个箭头是AND):

a = 0:00abc = 000bc - &gt; 011bc - &gt; 000bc = a00bc
a = 1:00abc = 001bc - &gt; 100bc - &gt; 100bc = a00bc

接下来,通过添加00010然后与10101进行AND运动将'b'向左移动一个位置:

b = 0:a00bc = a000c - &gt; a001c - &gt; a000c = a0b0c
b = 1:a00bc = a001c - &gt; a010c - &gt; a010c = a0b0c

就是这样。

答案 5 :(得分:2)

首先,对于“26位”值,最高位应始终清零,因此它实际上是25位值。

1)MMX(和/或SSE)无济于事,因为主要的问题是没有简单的算术或布尔运算系列可以得到你想要的结果,并且所有东西都支持相同的算术和布尔运算。

2)我无法想到或找到乘法的魔法常数。

3)我看不到使用任何条件设置指令(例如SETcc)的方法,它比移位/添加指令有任何优势。

4)jdv和paul(上图)是对的。如果你需要经常进行这种转换以至于性能很重要,那么查找表将是现代CPU上最好/最快的选择。 “13位到26位”的查找表将是2 ** 13个双字或32 KiB。在旧的CPU(具有小的L1缓存)上,CPU速度和RAM速度之间的相对差异并不像现在那么糟糕。

如果你不能为“13位到25位”查找表节省32 KiB,你可以将13位值分成一对值(一个6位值和一个7位值) )然后在组合结果之前对每个值使用查找表,如下所示:

mov ebx,eax                    ;ebx = 13-bit value
shr eax,6                      ;eax = highest 7 bits of value
and ebx,0x003F                 ;ebx = lowest 6 bits of value
mov eax,[lookup_table + eax*2] ;eax = highest 14-bits of result
mov ebx,[lookup_table + ebx*2] ;eax = lowest 12-bits of result
shl eax,12
or eax,ebx                     ;eax = 25-bit result

在这种情况下,查找表有128个条目(每个条目有2个字节),所以它只有256个字节。

5)对于反向操作,一个简单的查找表将花费你64 MiB(2 ** 25 * 2),所以这不是一个好主意。但是,您可以将25位值拆分为13位值和11位值(12位值,其中最高位始终清除),并使用8192条目表,每个条目一个字节(总计成本是8 KiB)。没有理由你不能将25位值拆分成更多/更小的块(并使用更小的表)。

答案 6 :(得分:2)

在从Haswell开始的Intel x86处理器上,您可以使用pdep指令集中的单BMI2条指令来执行此操作:

uint32_t interleave_zero_bits(uint32_t x) {
    return _pdep_u32(x, 0x55555555U);
}

答案 7 :(得分:1)

我认为this可能是相关的,但我不完全确定。我知道MMX指令用于交错32/64位值的字节,但不是单个位。

答案 8 :(得分:1)

你还没有指定要运行的平台,我想尝试一种与已发布的平台不同的方法(我喜欢查找表一,它可以正常工作,直到位数增加)

大多数平台都有单独的移位和旋转指令。几乎总是有一条包含进位/溢出标志的指令,因此您可以“移入”您想要的位。假设我们有这些说明: * SHIFTLEFT:做一个leftshift并用零填充低位。 * ROTATELEFT:执行leftshift,设置进位标志中前一个值的最低位,并从左侧“向外”移位的位设置进位。

伪代码:

LOAD value into register A;
LOAD 0 into register B;
SHIFT register A (registerwidth-13) times; 
ROTATELEFT A
ROTATELEFT B
SHIFTLEFT  B

...重复13次。随便展开。

第一个班次应该在进位之前将最上面的位置到位。 ROTATELEFT A将MSB推入进位,ROTATELEFT B将该位推入B的LSB,SHIFTLEFT B将0输入。对所有位执行此操作。


修改/增加:

您可以使用相同的指令执行相反的操作(反向位图转换),如下所示:

LOAD值到寄存器A; 将0加载到寄存器B中;

ROTATELEFT A; ROTATELEFT A; ROTATELEFT B; ......重复13次 然后 SHIFTLEFT B; for(registerwidth-13)次。

LSB携带;忘了它,接下来LSB进入进位,把它放到目标寄存器中,重复所有位,然后对齐结果。

答案 9 :(得分:0)

您始终可以使用for循环:

for (int i = 0; i < 13; i++)
{
    output |= (input & (1 << i)) << i;
}

这个更短,但我认为它的速度要快得多。

答案 10 :(得分:-2)

检查你的CPU是否支持字节和字交换(对于字节序转换) - 如果是这样 - 只需对其进行交换 - 这将会缩短一些6(5)指令。