我需要迅速否定大量的双打。如果 bit_generator 生成0,则必须更改符号。如果 bit_generator 生成1,则没有任何反应。循环运行多次, bit_generator 非常快。在我的平台上,案例2明显快于案例1.看起来我的CPU不喜欢分支。有没有更快,更便携的方式来做到这一点?您如何看待案例3?
// generates 0 and 1
int bit_generator();
// big vector (C++)
vector<double> v;
// case 1
for (size_t i=0; i<v.size(); ++i)
if (bit_generator()==0)
v[i] = -v[i];
// case 2
const int sign[] = {-1, 1};
for (size_t i=0; i<v.size(); ++i)
v[i] *= sign[bit_generator()];
// case 3
const double sign[] = {-1, 1};
for (size_t i=0; i<v.size(); ++i)
v[i] *= sign[bit_generator()];
// case 4 uses C-array
double a[N];
double number_generator(); // generates doubles
double z[2]; // used as buffer
for (size_t i=0; i<N; ++i) {
z[0] = number_generator();
z[1] = -z[0];
a[i] = z[bit_generator()];
}
编辑:添加了案例4和C-tag,因为矢量可以是普通数组。由于我可以控制如何生成双精度,我重新设计了代码,如案例4所示。它避免了额外的乘法和分支。我认为它应该在所有平台上都很快。
答案 0 :(得分:8)
除非你想在循环中调整向量的大小,否则将v.size()从for表达式中取出,即
const unsigned SZ=v.size();
for (size_t i=0; i<SZ; ++i)
if (bit_generator()==0)
v[i] = -v[i];
如果编译器无法看到bit_generator()中发生的情况,那么编译器可能很难证明v.size()没有改变,这使得循环展开或矢量化成为不可能。
更新:我做了一些测试,在我的机器上方法2似乎是最快的。但是,使用我称之为“群组行动”的模式似乎更快:-)。基本上,您将多个决策分组为一个值并切换它:
const size_t SZ=v.size();
for (size_t i=0; i<SZ; i+=2) // manual loop unrolling
{
int val=2*bit_generator()+bit_generator();
switch(val) // only one conditional
{
case 0:
break; // nothing happes
case 1:
v[i+1]=-v[i+1];
break;
case 2:
v[i]=-v[i];
break;
case 3:
v[i]=-v[i];
v[i+1]=-v[i+1];
}
}
// not shown: wrap up the loop if SZ%2==1
答案 1 :(得分:5)
如果您可以假设该符号由一个特定位表示,就像在x86实现中一样,您可以这样做:
v[i] ^= !bit_generator() << SIGN_BIT_POSITION; // negate the output of
// bit_generator because 0 means
// negate and one means leave
// unchanged.
在x86中,符号位是MSB,因此对于位63的双精度数:
#define SIGN_BIT_POSITION 63
会做到这一点。
修改强>
根据评论,我应该补充一点,你可能需要做一些额外的工作来编译它,因为v
是double
的数组,而bit_generator()
返回{{ 1}}。你可以这样做:
int
(C语法可能略有不同,因为您可能需要一个typedef。)
然后将union int_double {
double d; // assumption: double is 64 bits wide
long long int i; // assumption: long long is 64 bits wide
};
定义为v
的向量并使用:
int_double
答案 2 :(得分:3)
通常,如果循环中有if()
,则无法对该循环进行向量化或展开,并且每次传递代码必须执行一次,从而最大限度地增加循环开销。情况3应该表现得非常好,特别是如果编译器可以使用SSE指令。
为了好玩,如果您正在使用GCC,请使用-S -o foo.S -c foo.c
标志而不是通常的-o foo.o -c foo.c
标志。这将为您提供汇编代码,您可以看到为这三种情况编译的内容。
答案 3 :(得分:2)
您不需要查找表,一个简单的公式就足够了:
const size_t size = v.size();
for (size_t i=0; i<size; ++i)
v[i] *= 2*bit_generator() - 1;
答案 4 :(得分:1)
假设实际的否定很快(对现代编译器和CPU的一个很好的假设),你可以使用条件赋值,它在现代CPU上也很快,可以在两种可能性之间进行选择:
v[i] = bit_generator() ? v[i] : -v[i];
这避免了分支,并允许编译器对循环进行矢量化并使其更快。
答案 5 :(得分:0)
你能否重写bit_generator
所以它返回1和-1而不是?这可以从方程式中消除间接性,但可能会有一定的清晰度。
答案 6 :(得分:-1)
在我的机器上,以5333.24 BogoMIPS运行,在1'000'000双精度阵列上进行1 000次迭代的时间,每个表达式产生以下时间:
p->d = -p->d 7.33 ns
p->MSB(d) ^= 0x80 6.94 ns
其中MSB(d)是用于获取d
的最高有效字节的伪代码。这意味着天真d = -d
执行的时间比混淆方法长5.32%。对于十亿次这样的否定,这意味着7.3和6.9秒之间的差异。
有人必须拥有一大堆双打来关心这种优化。
顺便说一句,我必须在完成时打印出数组的内容,或者我的编译器将整个测试优化为零操作码。