我试图找出快速和简单的方法来找到一个字节数组中的每个位的模式(“平均”)。
以下是我正在寻找的一个例子:
Byte 1 1010 1010
Byte 2 0101 0101
Byte n 1010 1000
Result 1010 1000
因此,如果位位置主要包含1,则答案中的位位置为1。如果位位置主要包含0,则答案为0。如果1和0的出现次数相等,则我不关心答案中的该位置的值。
对于我的用例来说,输入数量的数量级很小(大约10到20个输入),但是欢迎讨论您的方法的性能,因为它随输入数量而扩展。
我可以手动计算每个1和每个0,并以这种方式计算出来,但我希望有一种更优雅,可能更快的方法来完成它。
7条答案
按热度按时间z3yyvxxp1#
假设
bytes
是vector<uint8_t>
或array<uint8_t>
。保持一个名为
counts
的表,其中包含8个整数,所有整数都初始化为0。伪代码:然后使用
counts
构建最终结果:bz4sfanl2#
对于标准C++,已经有很多答案了。
有人在评论中说:“* 但我不认为有一个比特旋转的黑客可以让你在不检查单个比特的情况下做到这一点。*”,这给了我写这个的动力,它基于英特尔内部函数,而不是标准的C++。
您可以使用PDEP将位提取到U64中的字节中(如果您的输入中有超过256个元素,请相应地调整PDEP,以使用多个U64,并为每个位累加器“通道”增加位宽)。
Wikipedia link for Intel BMI,Intel intrinsic reference,related Stack Overflow question .
您可以将
std::transform_reduce
与std::execution::par_unseq
一起使用,其中转换是PDEP展开,reduce是求和操作(假设您基于不可能溢出的输入数量将每个通道的位宽设置得足够宽),然后在结束时,如果通道的值超过输入字节数的一半,然后将相应的输出位设置为1
。可能有一种奇特的SIMD方法来完成这一部分,但这一步的性能影响是一个常数(我现在懒得找到这种方法)。krugob8w3#
如果你有一个for循环,迭代0x 80,0x 40,0x 20,0x 10,0x 08,0x 04,0x 02,0x 01,我们可以把它作为一个掩码来检查数据中的每一位:
我们可以通过使用条件来重构if语句,即
当计算位数时,我们只需要计算足够多的位数,直到我们可以在移动到下一位之前确定特定位的大多数。如果数据的倾向变得明显,我们可以考虑提前退出实现。如果它是一个令人紧张的平均值,那么我们别无选择,只能迭代所有记录。
在下列情况下达到多数:
在进行整数数学运算时,我们可以使用以下语句来涵盖这两种情况:
当我们确定了一个1-case时,我们可以在结果中设置一个位:
下面是一个C++的例子:
其输出:
wfveoks04#
C和C的解决方案(因为我后来才注意到C标签):
这种方法相当简单,但我认为不可能做出一些聪明的位操作动作。部分参数如下:
考虑两个比特,因此可能的组合是00,01,10,11。这给出了3种可能性:多数0、相等计数和多数1。因此,不可能将这些信息压缩到单个比特中。因此,如果我们逐个处理输入字节,我们就不能有大小仅为一个字节的中间状态。
6ojccjat5#
从数学上讲,您可以通过乘法和一些位操作将8位字节的位“扩展”为64位无符号值。
然后,您可以通过添加64位数字来“并行”添加多达255个。
user提到的方法在phuclv的详细回答中显示(沿着使用intrinsic实现的一些示例):
https://stackoverflow.com/a/51750902/4944425
使用他们的公式,我们可以写如下:
地址:https://godbolt.org/z/K1MrYcnvK
9o685dep6#
有一种有点繁琐的方法可以在
std::uint8_t
和std::uint64_t
上工作。(几乎)没有分支并且是原位的。然而,有一个警告,我在下面讨论。创意
如果你有一系列的位,你要对它们进行排序,你可以查看中间值并得到平均值。事实上,你可以得到任何你想要的分位数。这是可行的,因为位是二进制的。
例如:01001010001 -〉00000001111 -〉中间位为0 -〉平均值为0。
分类
如果你有两个数字
a
和b
,你可以通过实现将所有1“向下”移动和将所有0“向上”移动的结果来按列排序位:这保证了保留1和0的总数,并且它以位并行方式工作。* 并且是无分支的。*
问题
将其应用于n个值而不分支并不像人们想象的那么简单,简单的解决方案是将每个值彼此伪交换,这在纸面上具有二次复杂度,但也是无分支的(除了可以很容易地进行分支预测或展开的循环),并且除了现有的数组之外不需要额外的内存。我很确定有一种更好的方法来做到这一点,但我没想到。也许有人能进一步发展这个想法。
算法
该算法将其变为:
变成这样:
示例实现
看一下working demo on compiler explorer,它展示了对主要实现的一些很好的优化:
对于小的数组大小,
sieve
循环似乎是展开的,在热路径中没有任何跳跃。讨论
这听起来很糟糕,这是二次的。但是你说你的输入数组非常小,在10和20之间。这意味着复杂度bn和nn(b =一个字中的位数)是无法区分的,因为b ~ n。这样做的好处是:
如果你达到了拥有数百万或数十亿字节的地步,那么就切换到一个计数
1
s的解决方案,因为这样的话,二次复杂度就会成为一个问题。*如有疑问 * 基准 !
nzkunb0c7#
这是一个部分的答案,因为我的方法不能处理任意数量的输入字节。它可以处理任何数量的输入,有效的排序网络是已知的。我在下面的代码中展示了长度为2到16字节的数组的例子。
Knuth,TAOCP,第4A卷,第7.1.1节详细解释了三元多数运算的实用性(x ∧ y)∨(y ∧ z)∨(x ∧ z)。此函数按位应用于三个输入时,将实现asker请求的结果。Knuth更喜欢将该函数称为“中位数”而不是“多数”因为如果让∧对应于
min
,∨对应于max
,那么当x ≤ y ≤ z时,恰好〈xyz〉= y。这里有趣的观察是,构建排序网络的一种方法是从
min
和max
原语。三个median3 (x,y,z) = min (max (min (y, z), x), max (y,z))
的中位数,而min3 (x,y,z) = min (x, min (y, z))
和max3 (x,y,z) = max (x, max (y, z))
。克努特指出,任何单调布尔函数都可以只用中值运算和常数0和1来表示,因此,五的中值可以用这种方式表示,根据克努特(第64页),最有效的排列是:∠ vwxyz ∠ = ∠ v ∠ xyz ∠ ∠ wx ∠ wyz ∠。
为了测试排序网络的位中值计算的可导出性,我使用了文献中的一个9输入网络,并将其转换为9位中值计算,它提供了asker所要求的结果。我还将以前工作中的一些搜索网络图转换为相应的mix / max操作序列。以及从TAOCP vol.3翻译的其他网络图。对于其他排序网络,我参考了Bert Dobbelaere's list,如代码注解中所述。Wikipedia article on sorting networks建议(接近)最佳排序网络已知多达20个输入,因此覆盖了提问者感兴趣的数组尺子范围。
至于效率,compiled with Clang 16下面代码中的
byte_mode_16()
编译为大约170条x86指令,具有大量指令级并行性,因此我 * 猜测 * 在现代x86-64 CPU上执行大约需要50个周期。在NVIDIA GPU上,LOP3
指令支持任意三输入逻辑运算,相同的函数编译为大约80条指令。