更快地计算(近似)方差

时间:2014-05-29 13:58:28

标签: c++ algorithm optimization variance

我可以通过CPU分析器看到compute_variances()是我项目的瓶颈。

  %   cumulative   self              self     total           
 time   seconds   seconds    calls  ms/call  ms/call  name    
 75.63      5.43     5.43       40   135.75   135.75  compute_variances(unsigned int, std::vector<Point, std::allocator<Point> > const&, float*, float*, unsigned int*)
 19.08      6.80     1.37                             readDivisionSpace(Division_Euclidean_space&, char*)
 ...

这是函数的主体:

void compute_variances(size_t t, const std::vector<Point>& points, float* avg,
                       float* var, size_t* split_dims) {
  for (size_t d = 0; d < points[0].dim(); d++) {
    avg[d] = 0.0;
    var[d] = 0.0;
  }
  float delta, n;
  for (size_t i = 0; i < points.size(); ++i) {
    n = 1.0 + i;
    for (size_t d = 0; d < points[0].dim(); ++d) {
      delta = (points[i][d]) - avg[d];
      avg[d] += delta / n;
      var[d] += delta * ((points[i][d]) - avg[d]);
    }
  }

  /* Find t dimensions with largest scaled variance. */
  kthLargest(var, points[0].dim(), t, split_dims);
}

其中kthLargest()似乎不是问题,因为我看到了:

0.00 7.18 0.00 40 0.00 0.00 kthLargest(float*, int, int, unsigned int*)

compute_variances()采用浮点数向量的向量(即Points的向量,其中Points是我已实现的类)并计算每个维度中它们的方差(关于Knuth的算法)。

以下是我如何调用该函数:

float avg[(*points)[0].dim()];
float var[(*points)[0].dim()];
size_t split_dims[t];

compute_variances(t, *points, avg, var, split_dims);

问题是,我能做得更好吗?我真的很乐意在速度和差异的近似计算之间进行权衡。或者也许我可以使代码更加缓存友好或什么?

我这样编译:

g++ main_noTime.cpp -std=c++0x -p -pg -O3 -o eg

请注意,在编辑之前,我使用了-o3,而没有使用大写&#39; o&#39;。感谢 ypnos ,我现在使用优化标记-O3编译。我确信它们之间存在差异,因为我在伪网站中使用these methods之一进行了时间测量。

请注意,现在compute_variances占据了整个项目的时间!

[编辑]

copute_variances()被召唤40次。

每10次通话,以下情况属实:

points.size() = 1000   and points[0].dim = 10000
points.size() = 10000  and points[0].dim = 100
points.size() = 10000  and points[0].dim = 10000
points.size() = 100000 and points[0].dim = 100

每个调用都处理不同的数据。

问:访问points[i][d]的速度有多快?

答:point[i]只是std :: vector的第i个元素,其中第二个[]Point类中实现。

const FT& operator [](const int i) const {
  if (i < (int) coords.size() && i >= 0)
     return coords.at(i);
  else {
     std::cout << "Error at Point::[]" << std::endl;
     exit(1);
  }
  return coords[0];  // Clear -Wall warning 
}

其中coordsstd::vector float个值。这似乎有点沉重,但编译器是否应该足够聪明以正确预测分支始终为真? (我的意思是冷启动后)。此外,std::vector.at()应该是恒定时间(如ref中所述)。我将此更改为在函数体中只有.at(),并且时间测量保持不变,几乎相同。

compute_variances()中的分区肯定是重的!然而,Knuth的算法是一个数值稳定的算法,我无法找到另一种算法,即数值稳定且没有除法。

请注意,我现在并行

[EDIT.2]

Point班的最小例子(我想我没有忘记展示一些东西):

class Point {
 public:

  typedef float FT;

  ...

  /**
   * Get dimension of point.
   */
  size_t dim() const {
    return coords.size();
  }

  /**
   * Operator that returns the coordinate at the given index.
   * @param i - index of the coordinate
   * @return the coordinate at index i
   */
  FT& operator [](const int i) {
    return coords.at(i);
    //it's the same if I have the commented code below
    /*if (i < (int) coords.size() && i >= 0)
      return coords.at(i);
    else {
      std::cout << "Error at Point::[]" << std::endl;
      exit(1);
    }
    return coords[0];  // Clear -Wall warning*/
  }

  /**
   * Operator that returns the coordinate at the given index. (constant)
   * @param i - index of the coordinate
   * @return the coordinate at index i
   */
  const FT& operator [](const int i) const {
        return coords.at(i);
    /*if (i < (int) coords.size() && i >= 0)
      return coords.at(i);
    else {
      std::cout << "Error at Point::[]" << std::endl;
      exit(1);
    }
    return coords[0];  // Clear -Wall warning*/
  }

 private:
  std::vector<FT> coords;
};

7 个答案:

答案 0 :(得分:3)

<强> 1。 SIMD

一个简单的加速就是使用向量指令(SIMD)进行计算。在x86上,这意味着SSE,AVX指令。根据您的字长和处理器,您可以获得大约x4甚至更多的加速。这段代码在这里:

for (size_t d = 0; d < points[0].dim(); ++d) {
    delta = (points[i][d]) - avg[d];
    avg[d] += delta / n;
    var[d] += delta * ((points[i][d]) - avg[d]);
}
通过使用SSE一次计算四个元素,可以加速

。由于您的代码实际上只在每个循环迭代中处理一个元素,因此没有瓶颈。如果你下降到16位短而不是32位浮点数(那么近似值),你可以在一条指令中输入8个元素。使用AVX它会更多,但你需要一个最新的处理器。

它不是解决您的性能问题的 解决方案,而只是其中一个也可以与其他人结合使用。

<强> 2。微parallelizm

当你拥有那么多循环时,第二个简单的加速就是使用并行处理。我通常使用英特尔TBB,其他人可能会建议使用OpenMP。为此,您可能需要更改循环顺序。因此,在外循环中并行化d,而不是在i。

你可以结合使用这两种技术,如果你做得对,在带有HT的四核中你可以获得25-30的加速度,而不会有任何精度损失。

第3。编译器优化

首先,也许这只是SO上的一个错字,但它必须是-O3,而不是-o3! 总的来说,如果在实际使用它们的范围内声明变量delta,n,编译器可能更容易优化代码。您还应该尝试-funroll-loops编译器选项以及-march。后者的选项取决于你的CPU,但现在通常-march core2很好(也适用于最近的AMD),并且包括SSE优化(但我不相信编译器只是为你的循环做这件事)。 / p>

答案 1 :(得分:3)

您的数据结构存在的一个主要问题是它本质上是vector<vector<float> >。这是一个指向float数组的指针数组的指针,附有一些铃声和口哨声。特别是,访问Point中的连续vector s对应于访问连​​续的存储器位置。我敢打赌,当您分析此代码时,您会看到大量的缓存未命中。

在用其他任何东西徘徊之前解决这个问题。

低阶关注点包括内循环中的浮点除法(在外循环中计算1/n)和作为内循环的大型加载存储链。例如,您可以使用SIMD计算数组切片的均值和方差,并将它们组合在一起。

每次访问一次的边界检查可能也没有帮助。摆脱它,或者至少将它从内环中提升出来;不要假设编译器知道如何自行修复它。

答案 2 :(得分:1)

这就是我要做的事情,按照估计的重要性顺序:

  1. Point::operator[]按值返回浮点,而不是按引用返回。
  2. 使用coords[i]代替coords.at(i),因为已经断言它在界限范围内。 at成员检查边界。你只需要检查一次。
  3. 用断言替换Point::operator[]中的自制错误指示/检查。这就是断言的意义所在。它们在发布模式下名义上是无操作的 - 我怀疑你需要在发布代码中检查它。
  4. 用一个分区替换重复的分区并重复乘法。
  5. 通过展开外部循环的前两次迭代来消除浪费初始化的需要。
  6. 为了减少缓存未命中的影响,可以选择向前或向后运行内部循环。这至少让您有机会使用一些缓存的avgvar。事实上,如果prefetch按照相反的迭代顺序工作,它可能会删除avgvar上的所有缓存未命中。
  7. 在现代C ++编译器上,std::fillstd::copy可以利用类型对齐,并且有可能比C库memsetmemcpy更快。
  8. Point::operator[]将有可能在发布版本中加入内联,并且可以减少到两个机器指令(有效地址计算和浮点加载)。那正是你想要的。当然,它必须在头文件中定义,否则只有在启用链接时代码生成(a.k.a. LTO)时才会执行内联。

    请注意,Point::operator[]的正文仅相当于单行 调试版本中的return coords.at(i)。在发布版本中,整个正文等同于return coords[i] return coords.at(i)

    FT Point::operator[](int i) const {
      assert(i >= 0 && i < (int)coords.size());
      return coords[i];
    }
    
    const FT * Point::constData() const {
      return &coords[0];
    }
    
    void compute_variances(size_t t, const std::vector<Point>& points, float* avg,
                           float* var, size_t* split_dims)
    {
      assert(points.size() > 0);
      const int D = points[0].dim();
    
      // i = 0, i_n = 1
      assert(D > 0);
    #if __cplusplus >= 201103L
      std::copy_n(points[0].constData(), D, avg);
    #else
      std::copy(points[0].constData(), points[0].constData() + D, avg);
    #endif
    
      // i = 1, i_n = 0.5
      if (points.size() >= 2) {
        assert(points[1].dim() == D);
        for (int d = D - 1; d >= 0; --d) {
          float const delta = points[1][d] - avg[d];
          avg[d] += delta * 0.5f;
          var[d] = delta * (points[1][d] - avg[d]);
        }
      } else {
        std::fill_n(var, D, 0.0f);
      }
    
      // i = 2, ...
      for (size_t i = 2; i < points.size(); ) {
        {
          const float i_n = 1.0f / (1.0f + i);
          assert(points[i].dim() == D);
          for (int d = 0; d < D; ++d) {
            float const delta = points[i][d] - avg[d];
            avg[d] += delta * i_n;
            var[d] += delta * (points[i][d] - avg[d]);
          }
        }
        ++ i;
        if (i >= points.size()) break;
        {
          const float i_n = 1.0f / (1.0f + i);
          assert(points[i].dim() == D);      
          for (int d = D - 1; d >= 0; --d) {
            float const delta = points[i][d] - avg[d];
            avg[d] += delta * i_n;
            var[d] += delta * (points[i][d] - avg[d]);
          }
        }
        ++ i;
      }
    
      /* Find t dimensions with largest scaled variance. */
      kthLargest(var, D, t, split_dims);
    }
    

答案 3 :(得分:0)

for (size_t d = 0; d < points[0].dim(); d++) {
  avg[d] = 0.0;
  var[d] = 0.0;
}

只需使用memset即可优化此代码。在32位中的0.0的IEEE754表示是0x00000000。如果尺寸很大,那就值得了。 类似的东西:

memset((void*)avg, 0, points[0].dim() * sizeof(float));

在你的代码中,你有很多对点[0] .dim()的调用。最好在函数开头调用一次并存储在变量中。可能,编译器已经这样做了(因为你使用的是-O3)。

除了其他操作(加法,减法)之外,除法运算(从时钟周期POV开始)要贵得多。

avg[d] += delta / n;

尝试减少分割数量是有意义的:使用部分非累积平均值计算,这将导致N个元素的 Dim 除法运算(而不是 N x暗淡); N&lt; points.size()

使用Cuda或OpenCL可以实现巨大的加速,因为avg和var的计算可以针对每个维度同时进行(考虑使用GPU)。

答案 4 :(得分:0)

另一项优化是缓存优化,包括数据缓存指令缓存

High level optimization techniques
Data Cache optimizations

数据缓存优化示例&amp;展开

for (size_t d = 0; d < points[0].dim(); d += 4)
{
  // Perform loading all at once.
  register const float p1 = points[i][d + 0];
  register const float p2 = points[i][d + 1];
  register const float p3 = points[i][d + 2];
  register const float p4 = points[i][d + 3];

  register const float delta1 = p1 - avg[d+0];
  register const float delta2 = p2 - avg[d+1];
  register const float delta3 = p3 - avg[d+2];
  register const float delta4 = p4 - avg[d+3];

  // Perform calculations
  avg[d + 0] += delta1 / n;
  var[d + 0] += delta1 * ((p1) - avg[d + 0]);

  avg[d + 1] += delta2 / n;
  var[d + 1] += delta2 * ((p2) - avg[d + 1]);

  avg[d + 2] += delta3 / n;
  var[d + 2] += delta3 * ((p3) - avg[d + 2]);

  avg[d + 3] += delta4 / n;
  var[d + 3] += delta4 * ((p4) - avg[d + 3]);
}

这与传统的循环展开不同之处在于,矩阵的加载是作为循环顶部的一组执行的。

编辑1:
微妙的数据优化是将avgvar放入结构中。这将确保两个数组在内存中彼此相邻,无填充。处理器中的数据提取机制,例如彼此非常接近的基准。数据缓存未命中的可能性更小,并且更有可能将所有数据加载到缓存中。

答案 5 :(得分:0)

您可以使用固定点数学而不是浮点数学作为优化。

通过固定点优化
处理器喜欢操纵整数(有符号或无符号)。由于零件的提取,执行数学运算,然后重新组装零件,浮点可能需要额外的计算能力。一种缓解方法是使用 Fixed Point 数学。

简单示例:米
给定米的单位,可以通过使用浮点(例如3.14159米)来表示小于一米的长度。然而,相同的长度可以以更精细的细节为单位表示,例如毫米,例如, 3141.59毫米。为了获得更精细的分辨率,选择较小的单位并将该值乘以,例如, 3,141,590微米(微米)。关键是选择足够小的单位来将浮点精度表示为整数。

浮点值在输入时转换为固定点。所有数据处理都在固定点进行。固定点值在输出之前转换为浮点。

2个固定点基地的力量
与从浮点计转换为固定点毫米一样,使用1000,可以使用2而不是1000的幂。选择2的幂允许处理器使用位移而不是乘法或除法。以2的幂移位通常比乘法或除法更快。

保持毫米的主题和准确度,我们可以使用1024作为基础而不是1000.同样,为了更高的准确度,使用65536或131072.

<强>摘要
将设计或实现更改为使用的定点数学运算,允许处理器使用比浮点更多的整数数据处理指令。除了专用处理器之外,浮点运算比集成运算消耗更多的处理能力。使用2的幂作为基(或分母)允许代码使用位移而不是乘法或除法。除法和乘法比移位需要更多的操作,因此移位更快。因此,不是优化执行代码(例如循环展开),而是可以尝试使用固定点表示法而不是浮点数。

答案 6 :(得分:0)

第1点。 您可以同时计算平均值和方差。 是对的吗? 你不必首先计算平均值,然后一旦你知道它,计算平均值的平方和的平方和? 除了正确之外,它更有可能帮助表现而不是伤害它。 尝试在一个循环中做两件事并不一定比两个连续的简单循环快。

第2点。 您是否意识到有一种方法可以同时计算平均值和方差,如下所示:

double sumsq = 0, sum = 0;
for (i = 0; i < n; i++){
  double xi = x[i];
  sum += xi;
  sumsq += xi * xi;
}
double avg = sum / n;
double avgsq = sumsq / n
double variance = avgsq - avg*avg;

第3点。 内循环正在进行重复索引。 编译器可能能够将其优化为最小化,但我不会打赌它。

第4点。 您正在使用 gprof 或类似的东西。 唯一合理可靠的数字是按功能自行计时。 它不会告诉你如何在函数内部花费时间。 我和其他许多人都依赖this method,这会让你直接找到需要时间的核心。