我正在使用x86或x86_64计算机。我有一个数组unsigned int a[32]
,其所有元素的值都为0或1.我想设置单个变量unsigned int b
,以便(b >> i) & 1 == a[i]
能够保存a
的所有32个元素}。我在Linux上与GCC合作(我猜不应该这么做)。
在C中执行此操作的最快方法是什么?
答案 0 :(得分:10)
最近的x86处理器上最快的方法可能是利用MOVMSKB系列指令,它们提取SIMD字的MSB并将它们打包成普通的整数寄存器。
我担心SIMD内在函数不是我真正的东西,但如果你有一台配备AVX2的处理器,那么这些内容应该有效:
uint32_t bitpack(const bool array[32]) {
__mm256i tmp = _mm256_loadu_si256((const __mm256i *) array);
tmp = _mm256_cmpgt_epi8(tmp, _mm256_setzero_si256());
return _mm256_movemask_epi8(tmp);
}
假设sizeof(bool) = 1
。对于较旧的SSE2系统,您必须将一对128位操作串联起来。将数组对齐在32字节边界上,并应保存另一个周期左右。
答案 1 :(得分:6)
其他答案包含一个明显的循环实现。
这是第一个变体:
unsigned int result=0;
for(unsigned i = 0; i < 32; ++i)
result = (result<<1) + a[i];
在现代x86 CPU上,我认为寄存器中任何距离的移位都是不变的,这种解决方案也不会更好。你的CPU可能不那么好;这段代码最大限度地降低了长途班次的成本;它执行32个1位移位,每个CPU都可以执行(您可以始终将结果添加到自身以获得相同的效果)。其他人所示的明显的循环实现通过移动等于循环索引的距离来进行大约900(总和32)1位移位。 (参见@Jongware对评论差异的测量结果; x86上的长时间偏移不是单位时间)。
让我们尝试更激进的事情。
假设您可以以某种方式将 m 布尔值打包到一个int中(通常可以为 m == 1执行此操作),并且您有两个实例变量 i1 和 i2 包含此类 m 打包位。
然后,下面的代码将m * 2个布尔值打包成一个int:
(i1<<m+i2)
使用这个我们可以打包2 ^ n位,如下所示:
unsigned int a2[16],a4[8],a8[4],a16[2], a32[1]; // each "aN" will hold N bits of the answer
a2[0]=(a1[0]<<1)+a2[1]; // the original bits are a1[k]; can be scalar variables or ints
a2[1]=(a1[2]<<1)+a1[3]; // yes, you can use "|" instead of "+"
...
a2[15]=(a1[30]<<1)+a1[31];
a4[0]=(a2[0]<<2)+a2[1];
a4[1]=(a2[2]<<2)+a2[3];
...
a4[7]=(a2[14]<<2)+a2[15];
a8[0]=(a4[0]<<4)+a4[1];
a8[1]=(a4[2]<<4)+a4[3];
a8[1]=(a4[4]<<4)+a4[5];
a8[1]=(a4[6]<<4)+a4[7];
a16[0]=(a8[0]<<8)+a8[1]);
a16[1]=(a8[2]<<8)+a8[3]);
a32[0]=(a16[0]<<16)+a16[1];
假设我们友好的编译器将[k]解析为(标量)直接内存访问(如果没有,你可以简单地用an_k替换变量an [k]),上面的代码(抽象地)执行63次提取,31次写入31班,31加。 (64位有明显的扩展)。
在现代x86 CPU上,我认为寄存器中任何距离的移位都是不变的。如果没有,这段代码可以最大限度地降低长途班次的成本;它实际上有64个1位移位。
在x64机器上,除了原始布尔值a1 [k]的提取之外,我希望编译器可以调度所有其余的标量以适应寄存器,因此32个内存提取,31个移位和31添加。很难避免提取(如果原始的布尔分散在周围)并且移位/添加匹配明显的简单循环。但是 没有循环,所以我们避免了32次递增/比较/索引操作。
如果起始布尔值确实在数组中,则每个位占据底部位,否则为零字节:
bool a1[32];
然后我们可以滥用我们的内存布局知识来一次获取几个:
a4[0]=((unsigned int)a1)[0]; // picks up 4 bools in one fetch
a4[1]=((unsigned int)a1)[1];
...
a4[7]=((unsigned int)a1)[7];
a8[0]=(a4[0]<<1)+a4[1];
a8[1]=(a4[2]<<1)+a4[3];
a8[2]=(a4[4]<<1)+a4[5];
a8[3]=(a8[6]<<1)+a4[7];
a16[0]=(a8[0]<<2)+a8[1];
a16[0]=(a8[2]<<2)+a8[3];
a32[0]=(a16[0]<<4)+a16[1];
这里我们的成本是8次(4组)布尔,7班和7加。同样,没有循环开销。 (同样有一个明显的64位泛化)。
为了比这更快,你可能不得不放入汇编程序并使用那里可用的许多精彩和奇怪的指令(向量寄存器可能有分散/收集可能很好用的操作)。
与往常一样,这些解决方案需要进行性能测试。
答案 2 :(得分:6)
如果sizeof(bool) == 1
,那么您可以使用所讨论的技术here将 8 bool
一次打包成8位(更多内容使用128位乘法)在具有快速乘法的计算机中
假设bools a[0]
到a[7]
的最低有效位分别命名为a-h。将这8个连续的bool
作为一个64位字处理并加载它们,我们将在小端机器中以相反的顺序获取这些位。现在我们将进行乘法运算(此处点为零位)
| a7 || a6 || a4 || a4 || a3 || a2 || a1 || a0 |
.......h.......g.......f.......e.......d.......c.......b.......a
x 1000000001000000001000000001000000001000000001000000001000000001
▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬
↑......h.↑.....g..↑....f...↑...e....↑..d.....↑.c......↑b.......a
↑.....g..↑....f...↑...e....↑..d.....↑.c......↑b.......a
↑....f...↑...e....↑..d.....↑.c......↑b.......a
+ ↑...e....↑..d.....↑.c......↑b.......a
↑..d.....↑.c......↑b.......a
↑.c......↑b.......a
↑b.......a
a
▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬
= abcdefghxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
添加箭头,以便更容易看到幻数中设置位的位置。此时,在最高字节中放置了8个最低有效位,我们只需要屏蔽其余的位
因此,通过使用幻数0b1000000001000000001000000001000000001000000001000000001000000001
或0x8040201008040201
,我们有以下代码
inline int pack8b(bool* a)
{
uint64_t t = *((uint64_t*)a);
return (0x8040201008040201*t >> 56) & 0xFF;
}
int pack32b(bool* a)
{
return (pack8b(a) << 24) | (pack8b(a + 8) << 16) | (pack8b(a + 16) << 8) | (pack8b(a + 24));
}
当然,您需要确保bool数组正确对齐8字节。您也可以展开代码并对其进行优化,例如仅移位一次而不是向左移动56位
抱歉,我忽略了这个问题,看到了doynax的bool数组以及误读“32 0/1 values”并认为它们是32 bool
s。当然,同样的技术也可以用于使用128位乘法同时打包4 uint32_t
,或者使用正常的64位乘法同时打包2 {,但它的效率远低于打包字节} p>
在具有BMI2的较新x86 CPU上,可以使用PEXT指令。上面的pack8b
函数可以替换为
_pext_u64(*((uint64_t*)a), 0x0101010101010101ULL);
要打包2 uint32_t
,问题需要使用
_pext_u64(*((uint64_t*)a), (1ULL << 32) | 1ULL);
答案 3 :(得分:4)
我可能会这样做:
unsigned a[32] =
{
1, 0, 0, 1, 1, 1, 0 ,0, 1, 0, 0, 0, 1, 1, 0, 0
, 1, 1, 1, 0, 0, 1, 1, 0, 1, 0, 1, 0, 0, 1, 1, 1
};
int main()
{
unsigned b = 0;
for(unsigned i = 0; i < sizeof(a) / sizeof(*a); ++i)
b |= a[i] << i;
printf("b: %u\n", b);
}
编译器优化可以很好地展开,但万一你可以随时尝试:
int main()
{
unsigned b = 0;
b |= a[0];
b |= a[1] << 1;
b |= a[2] << 2;
b |= a[3] << 3;
// ... etc
b |= a[31] << 31;
printf("b: %u\n", b);
}
答案 4 :(得分:3)
要确定最快的方式,请记录所有各种建议。这是一个很好的结果可能最终会成为&#34;&#34;最快(使用标准C,没有依赖处理器的SSE等):
unsigned int bits[32][2] = {
{0,0x80000000},{0,0x40000000},{0,0x20000000},{0,0x10000000},
{0,0x8000000},{0,0x4000000},{0,0x2000000},{0,0x1000000},
{0,0x800000},{0,0x400000},{0,0x200000},{0,0x100000},
{0,0x80000},{0,0x40000},{0,0x20000},{0,0x10000},
{0,0x8000},{0,0x4000},{0,0x2000},{0,0x1000},
{0,0x800},{0,0x400},{0,0x200},{0,0x100},
{0,0x80},{0,0x40},{0,0x20},{0,0x10},
{0,8},{0,4},{0,2},{0,1}
};
unsigned int b = 0;
for (i=0; i< 32; i++)
b |= bits[i][a[i]];
数组中的第一个值是最左边的位:可能的最高值。
使用一些粗略的时间测试概念验证表明这确实不比使用b |= (a[i]<<(31-i))
的简单循环更好:
Ira 3618 ticks
naive, unrolled 5620 ticks
Ira, 1-shifted 10044 ticks
Galik 10265 ticks
Jongware, using adds 12536 ticks
Jongware 12682 ticks
naive 13373 ticks
(相对时序,使用相同的编译器选项。)
(&#39;添加&#39;例程是我的,索引替换为两个索引数组的指针和显式添加。它慢了10%,这意味着我的编译器有效地优化了索引访问。知道。)
答案 5 :(得分:2)
unsigned b=0;
for(int i=31; i>=0; --i){
b<<=1;
b|=a[i];
}
答案 6 :(得分:-1)
您的问题是使用-->
(也称为 downto 运算符)的好机会:
unsigned int a[32];
unsigned int b = 0;
for (unsigned int i = 32; i --> 0;) {
b += b + a[i];
}
使用-->
的优点是它可以与有符号和无符号循环索引类型一起使用。
这种方法具有可移植性和可读性,它可能不会产生最快的代码,但是clang
确实会展开循环并产生不错的性能,请参阅https://godbolt.org/g/6xgwLJ