【问题标题】:C/C++ optimization: negate doubles fastC/C++ 优化:快速取反
【发布时间】:2010-07-30 07:38:53
【问题描述】:

我需要快速否定大量的双打。如果 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()];
}

编辑:添加了 case 4 和 C-tag,因为向量可以是一个普通数组。因为我可以控制如何生成双精度数,所以我重新设计了代码,如案例 4 所示。它同时避免了额外的乘法和分支。我认为它在所有平台上都应该很快。

【问题讨论】:

  • 可能使用迭代器而不是索引获得更好的性能(但可能不会,分析它)
  • 非常依赖于平台。你能指定你的吗?
  • C中没有vector&lt;double&gt;这样的东西。为什么这个问题有C标签?
  • @Fred:因为他可以很容易地使用双精度数组,而且答案不会改变。
  • 对不起 C 标签。 v 可以是一个普通数组。

标签: c++ c optimization floating-point


【解决方案1】:

除非您想在循环中调整向量的大小,否则将 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 

【讨论】:

  • 我还在准备更多的材料。
  • 不错的答案,但将 int 用于 SZ 是不可移植的。 v.size() 是无符号的,大小足以容纳一个内存地址。我不知道任何与 int 相同的平台。
  • 我做了那个size_t。由于 'x-1 > x' 问题,我通常避免使用无符号循环索引。
  • 2*f()+g(); 中,f()g() 的评估顺序未指定。所以,当v[i+1] 需要被否定时,你不能确定2*bit_generator()+bit_generator(); 为1,而v[i] 需要被否定时为2。更好的方法是val = 2*bit_generator(); val += bit_generator();
  • 当然。我认为这是某种随机生成器,在这种情况下,评估顺序无关紧要。
【解决方案2】:

如果您可以假设符号由一个特定位表示,就像在 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 

会成功的。

编辑:

基于 cmets,我应该补充一点,您可能需要做一些额外的工作才能编译它,因为 vdouble 的数组,而 bit_generator() 返回 int。你可以这样做:

union int_double {
    double d;        // assumption: double is 64 bits wide
    long long int i; // assumption: long long is 64 bits wide
};

(C 的语法可能有点不同,因为您可能需要 typedef。)

然后将v定义为int_double的向量并使用:

v[i].i ^= bit_generator() << SIGN_BIT_POSITION;

【讨论】:

  • 我需要 100% 便携的解决方案。
  • +1 正确,但为了放大这是一个处理器架构和编译器相关的优化,OP 应该在代码中清楚地标记它,可能适当参考 IEEE 754 - en.wikipedia.org/wiki/IEEE_754-2008
  • 这适用于floatdouble,但不适用于int。对于int,您需要反转所有位,然后为二进制补码加 1。 @m141:如果平台使用 IEEE 754 浮点,它仅适用于 floatdouble(我不知道现在有哪一种)。
  • @m141 - 快速或便携;选择其中一个
  • 没有必要讨论这个,因为任何现代编译器都已经如果可能的话会进行 XOR 否定。在代码中显式执行此操作不再是一个好主意(21 世纪)。
【解决方案3】:

一般来说,如果循环中有if(),则该循环无法矢量化或展开,并且代码必须每次执行一次,从而最大限度地提高循环开销。案例 3 的性能应该很好,尤其是在编译器可以使用 SSE 指令的情况下。

为了好玩,如果您使用的是 GCC,请使用 -S -o foo.S -c foo.c 标志而不是通常的 -o foo.o -c foo.c 标志。这将为您提供汇编代码,您可以查看针对您的三种情况编译的内容。

【讨论】:

    【解决方案4】:

    你不需要查找表,一个简单的公式就足够了:

    const size_t size = v.size();
    for (size_t i=0; i<size; ++i)
        v[i] *= 2*bit_generator() - 1;
    

    【讨论】:

    • 这个问题是乘法通常很慢,尤其是双打。从好的方面来说,这个便携的。
    • @Mark B:我不确定双精度数,但整数乘以 2 是简单的移位(bit_generator() 返回 int
    • @Mark:如果有问题的双精度是非规范化的,则不是。
    • @Mark:我不知道你所说的“双倍乘法”是什么意思。 2*x-1 只是将 0 映射到 -1 和 1 到 1。
    • 是的,它和案例 2 一样快。
    【解决方案5】:

    假设实际的否定很快(在现代编译器和 CPU 上是一个很好的假设),您可以使用条件赋值(在现代 CPU 上也很快)在两种可能性之间进行选择:

    v[i] = bit_generator() ? v[i] : -v[i];
    

    这避免了分支并允许编译器向量化循环并使其更快。

    【讨论】:

    • 如何避免生成分支?
    • 因为 SSE 允许您使用掩码在一条指令中选择两个源之一,而无需使用分支。
    • 我试过了。与我平台上的案例1相同。也许我需要启用 SSE?
    【解决方案6】:

    你能重写bit_generator 让它返回 1 和 -1 吗?这可能会以某种清晰度为代价从等式中删除间接性。

    【讨论】:

      【解决方案7】:

      过早的优化是乏味 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 秒之间的差异。

      有人必须有一大堆双打才能关心优化。

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

      【讨论】:

      • 我上次检查时,1'000'000'000 比 100'000'000 大约高出 10 倍。您的一天如此忙碌,需要节省 39 毫秒吗?
      • 在我的平台上每 100M 双打更像是 500ms。似乎使用分支的版本比案例 4 更依赖于平台(我的意思是速度)。
      猜你喜欢
      • 1970-01-01
      • 2021-08-30
      • 2011-08-25
      • 1970-01-01
      • 1970-01-01
      • 2010-12-13
      • 1970-01-01
      • 2012-05-06
      • 1970-01-01
      相关资源
      最近更新 更多