转换位数组以更快地设置
Convert array of bits to set faster
输入是存储在连续存储器中的位数组,每 1 位内存有 1 位位数组。
输出是位数组的设置位的索引数组。
例:
bitarray: 0000 1111 0101 1010
setA: {4,5,6,7,9,11,12,14}
setB: {2,4,5,7,9,10,11,12}
获取集合 A 或集合 B 都可以。集合存储为uint32_t数组,因此集合的每个元素都是数组中的无符号 32 位整数。
如何在单个 CPU 内核上以大约 5 倍的速度做到这一点?
当前代码:
#include <iostream>
#include <vector>
#include <time.h>
using namespace std;
template <typename T>
uint32_t bitarray2set(T& v, uint32_t * ptr_set){
uint32_t i;
uint32_t base = 0;
uint32_t * ptr_set_new = ptr_set;
uint32_t size = v.capacity();
for(i = 0; i < size; i++){
find_set_bit(v[i], ptr_set_new, base);
base += 8*sizeof(uint32_t);
}
return (ptr_set_new - ptr_set);
}
inline void find_set_bit(uint32_t n, uint32_t*& ptr_set, uint32_t base){
// Find the set bits in a uint32_t
int k = base;
while(n){
if (n & 1){
*(ptr_set) = k;
ptr_set++;
}
n = n >> 1;
k++;
}
}
template <typename T>
void rand_vector(T& v){
srand(time(NULL));
int i;
int size = v.capacity();
for (i=0;i<size;i++){
v[i] = rand();
}
}
template <typename T>
void print_vector(T& v, int size_in = 0){
int i;
int size;
if (size_in == 0){
size = v.capacity();
} else {
size = size_in;
}
for (i=0;i<size;i++){
cout << v[i] << ' ';
}
cout << endl;
}
int main(void){
const int test_size = 6000;
vector<uint32_t> vec(test_size);
vector<uint32_t> set(test_size*sizeof(uint32_t)*8);
rand_vector(vec);
//for (int i; i < 64; i++) vec[i] = -1;
//cout << "input" << endl;
print_vector(vec);
//cout << "calculate result" << endl;
int i;
int rep = 10000;
uint32_t res_size;
struct timespec tp_start, tp_end;
clock_gettime(CLOCK_MONOTONIC, &tp_start);
for (i=0;i<rep;i++){
res_size = bitarray2set(vec, set.data());
}
clock_gettime(CLOCK_MONOTONIC, &tp_end);
double timing;
const double nano = 0.000000001;
timing = ((double)(tp_end.tv_sec - tp_start.tv_sec )
+ (tp_end.tv_nsec - tp_start.tv_nsec) * nano) /(rep);
cout << "timing per cycle: " << timing << endl;
cout << "print result" << endl;
//print_vector(set, res_size);
}
结果(使用 ICC -O3 代码编译.cpp -LRT 编译)
...
timing per cycle: 0.000739613 (7.4E-4).
print result
0.0008 秒转换 768000 位以设置。但是每个周期中至少有 10,000 个 768,000 位的数组。即每个周期 8 秒。这很慢。
CPU 具有 popcnt 指令和 sse4.2 指令集。
谢谢。
更新
template <typename T>
uint32_t bitarray2set(T& v, uint32_t * ptr_set){
uint32_t i;
uint32_t base = 0;
uint32_t * ptr_set_new = ptr_set;
uint32_t size = v.capacity();
uint32_t * ptr_v;
uint32_t * ptr_v_end = &(v[size]);
for(ptr_v = v.data(); ptr_v < ptr_v_end; ++ptr_v){
while(*ptr_v) {
*ptr_set_new++ = base + __builtin_ctz(*ptr_v);
(*ptr_v) &= (*ptr_v) - 1; // zeros the lowest 1-bit in n
}
base += 8*sizeof(uint32_t);
}
return (ptr_set_new - ptr_set);
}
此更新版本使用 rhashimoto 提供的内部循环。我不知道内联是否真的使函数变慢(我从没想过会发生这种情况!新的时序是1.14E-5(由icc -O3 code.cpp -lrt
编译,并以随机向量为基准)。
警告:
我刚刚发现保留而不是调整 std::vector 的大小,然后通过原始指向直接写入矢量的数据是一个坏主意。不过,先调整大小然后使用原始指针是可以的。请参阅 Robφ 在调整 C++ std::vector
我注意到当你可能想使用.size()
时,你使用.capacity()
。这可能会让你做额外的不必要的工作,并给你错误的答案。
find_set_bit()
中的循环遍历单词中的所有 32 位。相反,您可以只遍历每个设置位,并使用 BSF 指令来确定最低位的索引。GCC 有一个内在函数__builtin_ctz()
生成 BSF 或等效函数 - 我认为英特尔编译器也支持它(如果没有,您可以内联组装)。修改后的函数如下所示:
inline void find_set_bit(uint32_t n, uint32_t*& ptr_set, uint32_t base){
// Find the set bits in a uint32_t
while(n) {
*ptr_set++ = base + __builtin_ctz(n);
n &= n - 1; // zeros the lowest 1-bit in n
}
}
在我的 Linux 机器上,使用 g++ -O3
进行编译,替换该函数会将报告的时间从 0.000531434 降至 0.000101352。
有很多方法可以在这个问题的答案中找到一点索引。不过,我确实认为__builtin_ctz()
将是您的最佳选择。我不认为有合理的 SIMD 方法来解决您的问题,因为每个输入字都会产生可变量的输出。
如@davidbak所建议的,您可以使用表查找一次处理位图的 4 个元素。
每次查找都会生成一个可变大小的集合成员块,我们可以使用 popcnt 来处理它。
@rhashimoto 的基于标量 CTZ 的建议可能会更好地处理具有大量零的稀疏位集,但当有很多设置位时,这应该更好。
我在想类似的事情
// a vector of 4 elements for every pattern of 4 bits.
// values range from 0 to 3, and will have a multiple of 4 added to them.
alignas(16) static const int LUT[16*4] = { 0,0,0,0, ... };
// mostly C, some pseudocode.
unsigned int bitmap2set(int *set, int input) {
int *set_start = set;
__m128i offset = _mm_setzero_si128();
for (nibble in input[]) { // pseudocode for the actual shifting / masking
__m128i v = _mm_load_si128(&LUT[nibble]);
__m128i vpos = _mm_add_epi32(v, offset);
_mm_store((__m128i*)set, vpos);
set += _mm_popcount_u32(nibble); // variable-length store
offset = _mm_add_epi32(offset, _mm_set1_epi32(4)); // increment the offset by 4
}
return set - set_start; // set size
}
当一个啃食没有1111
时,下一家商店会重叠,但这没关系。
通常,使用 popcnt
来确定指针的增量量是一种有用的技术,可以将可变长度数据左打包到目标数组中。
- 为不同配置设置MSVC_RUNTIME_LIBRARY的正确方法是什么
- 在C++/Linux中设置单调时钟的一些技巧
- 如何在选项卡视图Qt中设置一个新项目,并保存以前的项目
- 嵌套在类中时无法设置成员数据
- 需要帮助设置在C++中使用的Potrace
- 如何在自删除后将对象设置为nullptr
- 将指针设置为"nullptr"并不能防止双重删除?
- 如何在Ubuntu中使用cmake设置qt4
- ld:bind_at_load和-bitcode_bundle(Xcode设置ENABLE_bitcode=YES)不能
- 如何在boost beast http请求中设置http头
- 如何解决gcc编译器优化导致的centos双编译器设置中的分段错误
- 如何将这个C++哈希表转换为动态扩展和收缩,而不是使用硬设置的最大值
- 为什么文件名被设置为一个点,而不是在读取矢量中的文件名时
- 如何在24位SDL_Surface上设置像素的颜色
- std::设置自定义比较器
- 如何设置一个范围来提取我想要获得的信息
- 如何在C/C++中用FD_set Unix设置套接字文件描述符
- 换位表导致测试失败(但在游戏中运行良好)
- 通过选项卡的文本设置QTabWidget顺序
- 将特征矩阵的向量设置为0