【问题标题】:Faster computation of (approximate) variance needed需要更快地计算(近似)方差
【发布时间】:2014-07-19 01:35:54
【问题描述】:

我可以通过 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 是我实现的一个类)并计算它们在每个维度上的方差(关于算法克努斯的)。

这是我调用函数的方式:

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,而不是大写的“o”。感谢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::vectorfloat 值。这似乎有点重,但是编译器不应该足够聪明以正确预测分支总是正确的吗? (我的意思是在冷启动之后)。此外,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;
};

【问题讨论】:

  • 什么是值 - points.size 和 points[0].dim - 运行 32 秒?访问点数[i][d]的速度有多快?
  • 几个超级快速的观察结果:如果每个 compute_variances() 批次的点的维数是一致的,那么在该值上模板化函数可能会让编译器更有效地展开内部循环。此外,正如其他人所说,每次迭代的划分可能会主导您的时间成本。由于存在多个维度,您不能只对数组进行一次数值稳定性预排序,但如果这是您的瓶颈,您可能需要考虑其他方差算法。
  • @MBo 已更新。 Jeff 我不确定你所说的统一维度是什么意思,但我认为更新也会涵盖你。
  • 我们确实需要查看Point 类的整个 声明。例如,如果dim() 不是 const 方法,则内部循环将非常低效。
  • @KubaOber,我在考虑是否应该发布Point 课程。 dim() 是常量。我将发布最小的Point 类。

标签: c++ algorithm optimization variance


【解决方案1】:

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 位浮点(当时的近似值),您可以在一条指令中容纳八个元素。使用 AVX 会更多,但您需要最新的处理器。

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

2。微平行化

当您有这么多循环时,第二个简单的加速方法是使用并行处理。我通常使用 Intel TBB,其他人可能会建议使用 OpenMP。为此,您可能必须更改循环顺序。所以在外循环中并行化 d,而不是 i。

您可以将这两种技术结合起来,如果操作正确,在具有 HT 的四核上,您可能会在不损失任何准确性的情况下获得 25-30 的加速。

3.编译器优化

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

【讨论】:

  • 嗨,ypnos (==ύπνος?),抱歉,我忘了说我现在对并行性不感兴趣(我认为它是用 's',而不是用 'z')。一年前,我在 C 中使用了第一个,在执行点积时。但是,我清楚地记得我未能在 C++ 中传输我的代码。现在,我什至不记得如何在 C 中做到这一点。我看到了你的第 3 部分:stackoverflow.com/questions/10021536/… 但是,我没有任何错误,这意味着编译器忽略了“-o3”,或者它像其他东西一样解释它。我会编辑
  • "作为一般说明,如果您在实际使用它们的范围内声明变量 delta, n,编译器可能更容易优化您的代码。"这是编写代码时困扰我的事情。你说的是事实吗?因为,我只是认为变量的破坏和创建需要一些时间。当然,我可以写一些代码和测试,但现在我必须测试这个页面中所有好的想法!
  • 这不是错字!必须更新整个问题。 +1
  • 创建原始变量不会导致额外的 CPU 指令。范围有限的变量更容易优化,例如使其成为 CPU 寄存器,而不是将其放入堆栈。在这个简单的示例中,我希望编译器能够意识到该变量仅在本地使用,但只是作为一般规则。程序员也更容易阅读代码。还是看看SIMD指令吧,真的很值得。是的,我的昵称是 ύπνος,但我不是希腊人(我可以告诉你来自你的姓氏;)。
【解决方案2】:

你的数据结构最大的问题是它本质上是一个vector&lt;vector&lt;float&gt; &gt;。这是一个指向 float 数组的指针数组的指针,并附有一些花里胡哨的东西。特别是,访问vector 中的连续Points 并不对应于访问连​​续的内存位置。我敢打赌,当您分析此代码时,您会看到大量的缓存未命中。

先解决这个问题,然后再玩其他任何东西。

低阶问题包括内循环中的浮点除法(而不是在外循环中计算 1/n)和作为内循环的大负载存储链。例如,您可以使用 SIMD 计算数组切片的均值和方差,并在最后将它们组合起来。

每次访问一次边界检查可能也无济于事。也摆脱它,或者至少将其提升出内循环;不要假设编译器知道如何自行修复。

【讨论】:

  • 如何解决您提到的我的数据结构问题?如何在外循环中移动除法(看不到这是如何实现的,因为它delta 是在内循环中计算的)?我将测试没有边界检查的速度有多快。查看我的编辑。
  • @G.Samaras “看不出这是如何实现的” 乘法而不是除法。毕竟是内循环中的一个常数。
  • 库巴我看到了! +1 tmyklebu,因为他是第一个提出如何不使用除法的人。
  • @G.Samaras: vector&lt;T&gt;::at() 自己做边界检查,我相信。要修复您的数据结构,丢弃Point 结构并使用单个平面vector&lt;float&gt; 来存储所有内容,其中第一个Point 位于0 位置,第二个位于dim() 位置,依此类推.
  • 我猜你的意思是扔掉vector&lt;Point&gt;。但是这个类提供了很多操作符、构造函数等。
【解决方案3】:

按照估计的重要性顺序,我会这样做:

  1. Point::operator[] 返回浮点值,而不是引用。
  2. 使用coords[i] 而不是coords.at(i),因为您已经断言它在界限内。 at 成员检查边界。您只需检查一次。
  3. 用断言替换Point::operator[] 中的自制错误指示/检查。这就是断言的用途。它们在发布模式下名义上是无操作的 - 我怀疑您需要在发布代码中检查它。
  4. 用单除法和重复乘法代替重复的除法。
  5. 通过展开外循环的前两次迭代,无需进行浪费的初始化。
  6. 为了减少缓存未命中的影响,交替运行内部循环向前然后向后。这至少让您有机会使用一些缓存的avgvar。如果预取以相反的迭代顺序工作,它实际上可能会删除 avgvar 上的所有缓存未命中,因为它应该这样做。
  7. 在现代 C++ 编译器上,std::fillstd::copy 可以利用类型对齐并有机会比 C 库 memsetmemcpy 更快。

Point::operator[] 将有机会在发布版本中内联,并且可以减少到两条机器指令(有效地址计算和浮点加载)。那就是你想要的。当然它必须在头文件中定义,否则只有在启用链接时代码生成(又名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);
}

【讨论】:

  • 拜托,下次你在行动结束后编辑你的帖子时,以某种方式通知 OP,因为我不会看到std::fill vs memcpy,例如如果我没有来回到这里。 :)
  • @G.Samaras “行动结束后” 嗯?什么动作?怎么通过的?
  • 我的意思是,在我的问题的活动通过之后,在没有人评论/回答问题之后,在我接受你的问题之后,我不会看到你的编辑,除非我来这个问题再次检查其他用户告诉的内容。
  • 我相信您可以将问题的答案作为一项功能请求编辑通知。
【解决方案4】:
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));

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

除法运算(从时钟周期 POV 计算)比其他运算(加法、减法)要昂贵得多。

avg[d] += delta / n;

尝试减少除法次数是有意义的:使用部分非累积平均计算,这将导致对 N 个元素进行 Dim 除法运算(而不是 N x暗淡); N

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

【讨论】:

  • memset 的好主意,因为这些是传统的一维数组。我不确定您所说的部分非累积平均值是什么意思。你能举个例子吗?我提醒您,我对速度和准确性之间的权衡感兴趣。 +1 memset,但我渴望我提到的例子。 :)
  • 事实证明memset 并没有那么有用,因为我在这里使用了时间方法gsamaras.wordpress.com/code/1651-2,但没有看到任何区别。我不会删除我的 +1,因为您从其他用户那里得到了 -1。
  • 无论如何,在 C++ 项目中推荐 memset 似乎很奇怪,当你有 std::fillstd::fill_n - 事实上,它们的性能可能比 memset 更好,特别是对于少量元素。 C 总是比 C++ 快的神话就是:)
  • 库巴奥伯;该方法的输入是指针,而不是向量;我们这里有很多元素。
  • G.萨马拉斯,部分非累积平均我指的是发烧 div 的解决方案。 Ex N=3, 1 dim:对于前三个迭代,算法获得这些元素的总和,然后计算平均值(N 个元素为 1 div),存储这个部分平均值;然后处理接下来的 N 个元素,依此类推。最后,再使用 1 个 div,就可以从部分 avgs 中得到所有值的 avg。如果 N=points.size() 您获得经典的平均计算。仅当值的上限远小于所用数据类型的最大值时,这才有效;否则可能会溢出。
【解决方案5】:

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

High level optimization techniques
Data Cache optimizations

数据缓存优化和展开示例

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 放入一个结构中。这将确保两个数组在内存中彼此相邻,没有填充。处理器中的数据获取机制,例如彼此非常接近的数据。数据缓存未命中的机会更少,将所有数据加载到缓存中的机会更大。

【讨论】:

  • 我正在寻找利用缓存(数据和指令)的方法。但是,您不相信编译器已经这样做了吗?另外,您认为需要register 关键字吗?我从来没有使用过它。 stackoverflow.com/questions/3207018/register-keyword-in-c/…
  • 我喜欢使用register 关键字来提醒编译器和可读性。除非设置为高,否则我正在使用的编译器之一不会应用优化;这意味着要么是整个源文件,要么什么都没有。
  • 我不相信编译器会自动优化缓存加载和卸载。缓存通常是特定于平台的问题,并非所有平台都具有相同数量的缓存。 事实在汇编语言中,所以打印带有和不带有优化的函数的汇编语言。比较看看。您的里程可能会有所不同。
  • 我学过汇编,但是很久没接触过,所以我想,我要比较一下时间(与我在我的问题中的伪站点中提到的方法)。
  • 我喜欢结构的想法,但是我如何设置静态分配数组的大小,因为我知道运行时的大小?我已经习惯了这些结构 gsamaras.wordpress.com/code/structs-c,但是我学习新东西不会有问题!
【解决方案6】:

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

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

简单示例:米
给定米的单位,可以使用浮点表示小于一米的长度,例如 3.14159 m。但是,相同的长度可以用更精细的单位(例如毫米)来表示,例如3141.59 毫米。对于更精细的分辨率,选择更小的单位并将值相乘,例如3,141,590 微米(微米)。重点是选择一个足够小的单位来将浮点精度表示为整数。

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

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

为了保持毫米的主题和精度,我们可以使用 1024 作为基础而不是 1000。同样,如果要获得更高的精度,请使用 65536 或 131072。

总结
将设计或实现更改为使用定点数学允许处理器使用比浮点更多的积分数据处理指令。在除专用处理器之外的所有处理器中,浮点运算比积分运算消耗更多的处理能力。使用 2 的幂作为基数(或分母)允许代码使用位移而不是乘法或除法。除法和乘法比移位需要更多的操作,因此移位更快。因此,与其优化执行代码(例如循环展开),不如尝试使用定点表示法而不是浮点数。

【讨论】:

  • 是否使用floats、ints等由用户决定。所以,假设他给了我们floats。我们可以将它们转换为ints,但这会取消处理器的好处。
【解决方案7】:

第 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,它可以让您直接了解需要时间的核心。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-11-13
    • 2013-06-24
    • 2018-10-09
    • 1970-01-01
    • 1970-01-01
    • 2015-05-13
    • 2018-09-13
    • 1970-01-01
    相关资源
    最近更新 更多