C / C ++优化:快速否定双打

时间:2010-07-29 12:07:50

标签: c++ c optimization floating-point

我需要迅速否定大量的双打。如果 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所示。它避免了额外的乘法和分支。我认为它应该在所有平台上都很快。

7 个答案:

答案 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 

会做到这一点。

修改

根据评论,我应该补充一点,你可能需要做一些额外的工作来编译它,因为vdouble的数组,而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)

过早优化是平淡无奇的SO问题的根源

在我的机器上,以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秒之间的差异。

有人必须拥有一大堆双打来关心这种优化。

顺便说一句,我必须在完成时打印出数组的内容,或者我的编译器将整个测试优化为零操作码。