【发布时间】:2022-10-04 16:27:25
【问题描述】:
我需要以最快的方式在0 和N-1 之间置换N 数字(在CPU 上,没有多线程,但可能使用SIMD)。 N 不大,我认为在大多数情况下,N<=12,所以 N! 适合带符号的 32 位整数。
到目前为止我尝试过的大致如下(省略了一些优化,我的原始代码是用Java编写的,但如果不是伪代码,我们会用C++来谈论性能):
#include <random>
#include <cstdint>
#include <iostream>
static inline uint64_t rotl(const uint64_t x, int k) {
return (x << k) | (x >> (64 - k));
}
static uint64_t s[2];
uint64_t Next(void) {
const uint64_t s0 = s[0];
uint64_t s1 = s[1];
const uint64_t result = rotl(s0 + s1, 17) + s0;
s1 ^= s0;
s[0] = rotl(s0, 49) ^ s1 ^ (s1 << 21); // a, b
s[1] = rotl(s1, 28); // c
return result;
}
// Assume the array |dest| must have enough space for N items
void GenPerm(int* dest, const int N) {
for(int i=0; i<N; i++) {
dest[i] = i;
}
uint64_t random = Next();
for(int i=0; i+1<N; i++) {
const int ring = (N-i);
// I hope the compiler optimizes acquisition
// of the quotient and modulo for the same
// dividend and divisor pair into a single
// CPU instruction, at least in Java it does
const int pos = random % ring + i;
random /= ring;
const int t = dest[pos];
dest[pos] = dest[i];
dest[i] = t;
}
}
int main() {
std::random_device rd;
uint32_t* seed = reinterpret_cast<uint32_t*>(s);
for(int i=0; i<4; i++) {
seed[i] = rd();
}
int dest[20];
for(int i=0; i<10; i++) {
GenPerm(dest, 12);
for(int j=0; j<12; j++) {
std::cout << dest[j] << ' ';
}
std::cout << std::endl;
}
return 0;
}
上面的速度很慢,因为 CPU 的模运算(%)很慢。我可以考虑在0和N!-1(含)之间生成一个随机数;这将减少模运算和Next() 调用的数量,但我不知道如何进行。另一种方法可能是以生成的模数中的小偏差为代价,用乘以反整数来代替除法运算,但我没有这些反整数,并且乘法可能不会快得多(按位运算和移位应该是快点)。
还有更具体的想法吗?
更新:有人问我为什么它是实际应用程序中的瓶颈。所以我只是发布了一个其他人可能感兴趣的任务。生产中的真正任务是:
struct Item {
uint8_t is_free_; // 0 or 1
// ... other members ...
};
Item* PickItem(const int time) {
// hash-map lookup, non-empty arrays
std::vector<std::vector<Item*>>> &arrays = GetArrays(time);
Item* busy = nullptr;
for(int i=0; i<arrays.size(); i++) {
uint64_t random = Next();
for(int j=0; j+1<arrays[i].size(); j++) {
const int ring = (arrays[i].size()-j);
const int pos = random % ring + j;
random /= ring;
Item *cur = arrays[i][pos];
if(cur.is_free_) {
// Return a random free item from the first array
// where there is at least one free item
return cur;
}
arrays[i][pos] = arrays[i][j];
arrays[i][j] = cur;
}
Item* cur = arrays[i][arrays[i].size()-1];
if(cur.is_free_) {
return cur;
} else {
// Return the busy item in the last array if no free
// items are found
busy = cur;
}
}
return busy;
}
【问题讨论】:
-
是否多次调用
GenPerm应该将dest设置为不同的值?在我的情况下不是这样。请提供MCVE。 -
@Nelfeal,那是因为您没有初始化种子。我已经扩展了示例并在在线 C++ 编译器中对其进行了检查。它打印 12 个数字的 10 个随机排列。
-
我很好奇你使用这些排列是为了什么,实际生成其中是性能瓶颈,而不是它们的用途。
-
你看过
std::shuffle吗? -
使用
%不仅速度慢,而且还引入了modulo bias 的潜力。为了尽可能快地获得无偏的均匀分布结果,请查看“Daniel Lemire. 2019. Fast Random Integer Generation in an Interval. ACM Trans. Model. Comput. Simul. 29, 1, Article 3 附录中的代码(2019 年 2 月),12 页。DOI:doi.org/10.1145/3230636"。
标签: c++ algorithm performance random permutation