为什么这段代码这么慢?

时间:2009-09-18 13:24:53

标签: c++ performance

所以我有这个函数用来计算统计数据(min / max / std / mean)。现在的问题是它通常在10,000乘15,000矩阵上运行。矩阵在类中存储为vector<vector<int> >。现在创建和填充所述矩阵变得非常快,但是当它归结为统计部分时,它变得非常缓慢。

E.g。一次读取一个像素的地理像素的所有像素值大约需要30秒。 (它涉及大量复杂的数学运算,以便将像素值正确地映射到相应的点),以计算整个矩阵的统计数据,大约需要6分钟。

void CalculateStats()
{
    //OHGOD
    double new_mean = 0;
    double new_standard_dev = 0;

    int new_min = 256;
    int new_max = 0;

    size_t cnt = 0;
    for(size_t row = 0; row < vals.size(); row++)
    {
        for(size_t col = 0; col < vals.at(row).size(); col++)
        {
            double mean_prev = new_mean;
            T value = get(row, col);
            new_mean += (value - new_mean) / (cnt + 1);
            new_standard_dev += (value - new_mean) * (value - mean_prev);

            // find new max/min's
            new_min = value < new_min ? value : new_min;
            new_max = value > new_max ? value : new_max;
            cnt++;
        }
    }

    stats_standard_dev = sqrt(new_standard_dev / (vals.size() * vals.at(0).size()) + 1);
    std::cout << stats_standard_dev << std::endl;
}

我在做一些可怕的事吗?

修改

要回复评论,T将是一个int。

编辑2

我修复了我的std算法,这是最终产品:

void CalculateStats(const std::vector<double>& ignore_values)
{
    //OHGOD
    double new_mean = 0;
    double new_standard_dev = 0;

    int new_min = 256;
    int new_max = 0;

    size_t cnt = 0;

    int n = 0;
    double delta = 0.0;
    double mean2 = 0.0;

    std::vector<double>::const_iterator ignore_begin = ignore_values.begin();
    std::vector<double>::const_iterator ignore_end = ignore_values.end();

    for(std::vector<std::vector<T> >::const_iterator row = vals.begin(), row_end = vals.end();  row != row_end; ++row)
    {
        for(std::vector<T>::const_iterator col = row->begin(), col_end = row->end(); col != col_end; ++col)
        {
            // This method of calculation is based on Knuth's algorithm.
            T value = *col;
            if(std::find(ignore_begin, ignore_end, value) != ignore_end)
                continue;
            n++;
            delta = value - new_mean;
            new_mean = new_mean + (delta / n);
            mean2 = mean2 + (delta * (value - new_mean));

            // Find new max/min's.
            new_min = value < new_min ? value : new_min;
            new_max = value > new_max ? value : new_max;
        }
    }
    stats_standard_dev = mean2 / (n - 1);
    stats_min = new_min;
    stats_max = new_max;
    stats_mean = new_mean;

这仍然需要大约120-130秒,但这是一个巨大的进步:)!

16 个答案:

答案 0 :(得分:29)

您是否尝试过编写代码?

你甚至不需要花哨的探查器。只需在那里粘贴一些调试时序语句。

我告诉你的任何事情都只是一种有根据的猜测(可能是错误的)

由于您访问矢量内容的方式,您可能会遇到大量缓存未命中。您可能希望将某些结果缓存到size()但我不知道这是否是问题。

答案 1 :(得分:8)

我只是简介它。 90%的执行时间都在这一行:

new_mean + =(value - new_mean)/(cnt + 1);

答案 2 :(得分:7)

您应该计算第一个循环中的值,最小值,最大值和计数的总和, 然后通过除以总和/计数来计算一次操作中的平均值, 然后在第二个循环中计算std_dev的总和

那可能会快一点。

答案 3 :(得分:5)

我发现的第一件事是你在循环中评估vals.at(row).size(),这显然不应该提高性能。它也适用于vals.size(),但当然内循环更糟。如果vals是向量的向量,最好使用迭代器或者至少保留外向量的引用(因为带有索引参数的get()肯定会耗费相当长的时间)。

此代码示例应用于说明我的意图; - )

for(TVO::const_iterator i=vals.begin(),ie=vals.end();i!=ie;++i) {
    for(TVI::const_iterator ii=i->begin(),iie=i->end();ii!=iie;++ii) {
        T value = *ii;
        // the rest
    }
}

答案 4 :(得分:4)

  • 首先,将行++更改为++行。一个小问题,但你想要速度,这将有助于
  • 第二,让你的行&lt;相反,vals.size进入一些const比较。编译器不知道val不会改变,所以它必须打好并始终调用大小。
  • 那里的'get'方法是什么?那是做什么的?这可能是你真正的问题。
  • 我对你的std dev计算不太确定。看一下wikipedia page on calculating variance in a single pass(他们快速解释了Knuth的算法,这是递归关系的扩展)。

答案 5 :(得分:3)

这很慢,因为您正在调试代码。

使用VS2008在Windows XP上构建和运行代码:

  • 具有默认优化级别的发布版本,OP中的代码运行时间为2734毫秒。
  • 一个默认为no optimization的Debug版本,OP中的代码运行时间很长,为398,531毫秒。

在下面的评论中,你说你没有使用优化,这似乎在这种情况下产生很大的不同 - 通常它不到十分之一,但在这种情况下它会慢一百倍。

我使用的是VS2008而不是2005,但它可能类似:

在Debug构建中,每次访问都有两个范围检查,每个范围检查使用非内联函数调用调用std::vector::size()并需要分支预测。函数调用和分支都有开销。

在Release版本中,编译器优化了范围检查(我不知道它是否只是丢弃它们,或者根据循环的限制进行流量分析),并且向量访问变为少量内联指针算术没有分支。

没有人关心调试版本的速度有多快。无论如何,您应该对发布版本进行单元测试,因为这是必须正常工作的构建。如果您尝试单步执行代码时没有所需的所有信息,请仅使用Debug构建。


发布的代码在&lt;在我的电脑上1.5秒,测试数据为15000 x 10000个整数,均等于42.您报告它的运行速度比它慢230倍。您使用的是10 MHz处理器吗?

虽然还有其他建议可以让它更快(例如将其移动到使用SSE,如果所有值都可以使用8位类型表示),但显然还有其他一些因素会让它变慢。

在我的机器上,既没有提升对行的向量的引用并且提升行的大小的版本,也没有使用迭代器的版本具有任何可测量的好处(使用迭代器的g ++ -O3可重复使用1511ms;悬挂和原始版本都需要1485毫秒)。未优化意味着它以7487毫秒(原始),3496毫秒(悬挂)或5331毫秒(迭代器)运行。

但是,除非你是在一个功耗非常低的设备上运行,或者正在进行分页,或者运行的非优化代码附带调试器,否则它应该不会这么慢,并且无论是什么使它变慢都不太可能是您发布的代码。

(作为旁注,如果您使用偏差为零的值对其进行测试,则SD会显示为1)

答案 6 :(得分:3)

内循环中有太多计算:

  1. 用于描述性统计(均值,标准 偏差)唯一需要的是计算总和 和平方和值之和。从这些 两个总和可以计算平均值和标准差 在外循环之后(与第三个值一起, 样本数量 - n是您的新/更新代码)。该 方程可以从定义中导出或找到 在网上,例如维基百科。例如,平均值是 只是之和除以n。对于n版本(在 与n-1版本相反 - 但是n很大 这种情况所以使用哪一个并不重要) 标准差为:
    sqrt(n * sumOfSquaredValue - sumOfValue * sumOfValue)。

    因此只有两个浮点 需要添加一个乘法 内循环。溢出不是这些总和的问题 双打的范围是10 ^ 318。特别是你会的 摆脱expensive floating point division那个 另一个答案中报告的分析显示。

  2. 一个较小的问题是最小值和最大值是 每次都重写(编译器可能会也可能不会 防止这种情况)。随着最小化迅速变小 最大值很快变大,只有两个比较 应该在大多数循环迭代中发生:使用 if语句而不是确定。可以说,但是 另一方面,它是微不足道的。

答案 7 :(得分:2)

我会改变访问数据的方式。假设您使用std::vector作为容器,可以执行以下操作:

vector<vector<T> >::const_iterator row;
vector<vector<T> >::const_iterator row_end = vals.end();
for(row = vals.begin(); row < row_end; ++row)
{
    vector<T>::const_iterator value;
    vector<T>::const_iterator value_end = row->end();
    for(value = row->begin(); value < value_end; ++value)
    {
        double mean_prev = new_mean;
        new_mean += (*value - new_mean) / (cnt + 1);
        new_standard_dev += (*value - new_mean) * (*value - mean_prev);

        // find new max/min's
        new_min = min(*value, new_min);
        new_max = max(*value, new_max);
        cnt++;
    }
}

这样做的好处是,在你的内循环中,你不是在咨询外部vector,而只是在内部。{1}}。

如果容器类型是一个列表,这将明显加快。因为get / operator []的查找时间对于列表是线性的,对于向量是常量。

编辑,我将调用移到了end()

答案 8 :(得分:1)

在每个循环之前移动.size()调用,并确保在编译时启用了优化。

答案 9 :(得分:1)

如果矩阵存储为向量矢量,那么在外部for循环中,您应该直接检索第i个向量,然后在内部循环中对其进行操作。试一试,看看它是否能提高性能。

答案 10 :(得分:0)

我不确定vals是什么类型,但vals.at(row).size()如果自己遍历集合则需要很长时间。将该值存储在变量中。否则,它可能使算法更像O(n³)而不是O(n²)

答案 11 :(得分:0)

我认为我会重写它以使用const迭代器而不是row和col索引。我会为row_end和col_end设置一个const const_iterator来进行比较,以确保它不会在每个循环结束时进行函数调用。

答案 12 :(得分:0)

正如人们所提到的那样,它可能是get()。例如,如果它访问邻居,你将完全粉碎缓存,这将大大降低性能。您应该剖析或只考虑访问模式。

答案 13 :(得分:0)

我修改了算法以摆脱几乎所有的浮点除法。

警告:未经检验的代码!!!

void CalculateStats()
{
    //OHGOD

    double accum_f;
    double accum_sq_f;
    double new_mean = 0;
    double new_standard_dev = 0;

    int new_min = 256;
    int new_max = 0;

    const int oku = 100000000;
    int accum_ichi = 0;
    int accum_oku = 0;
    int accum_sq_ichi = 0;
    int accum_sq_oku = 0;

    size_t cnt = 0;
    int v1 = 0;
    int v2 = 0;

    v1 = vals.size();

    for(size_t row = 0; row < v1; row++)
    {

            v2 = vals.at(row).size();
            for(size_t col = 0; col < v2; col++)
            {
                    T value = get(row, col);
                    int accum_ichi += value;
                    int accum_sq_ichi += (value * value);

                    // perform carries
                    accum_oku += (accum_ichi / oku);
                    accum_ichi %= oku;
                    accum_sq_oku += (accum_sq_ichi / oku);
                    accum_sq_ichi %= oku;

                    // find new max/min's
                    new_min = value < new_min ? value : new_min;
                    new_max = value > new_max ? value : new_max;
                    cnt++;
            }
    }

    // now, and only now, do we use floating-point arithmetic
    accum_f = (double)(oku) * (double)(accum_oku) + (double)(accum_ichi);
    accum_sq_f = (double)(oku) * (double)(accum_sq_oku) + (double)(accum_sq_ichi);

    new_mean = accum_f / (double)(cnt);

    // standard deviation formula from Wikipedia
    stats_standard_dev = sqrt((double)(cnt)*accum_sq_f - accum_f*accum_f)/(double)(cnt);        

    std::cout << stats_standard_dev << std::endl;
}

答案 14 :(得分:0)

在这里参加派对有点晚了,但有几点:

  1. 你在这里有效地做数值工作。我对数值算法知之甚少,但我知道参考和专家支持通常很有用。这个discussion thread提供了一些参考;和数字食谱是一项标准(如果过时)的工作。

  2. 如果您有机会重新设计矩阵,您可以尝试使用valarray和切片而不是矢量矢量;立即想到的一个优点是,您可以保证平坦的线性布局,这使得缓存预取和SIMD指令(如果您的编译器可以使用它们)更有效。

答案 15 :(得分:0)

在内部循环中,您不应该测试大小,不应该进行任何划分,并且迭代器也可能是昂贵的。事实上,一些展开会很好。 当然,你应该注意缓存局部性。

如果你的循环开销足够低,那么在单独的传递中进行它可能是有意义的:一个得到总和(你除以得到均值),一个得到平方和(你与之结合)得到方差的总和),得到最小和/或最大的一个。原因是简化了内部展开循环中的内容,因此编译器可以将内容保存在寄存器中。

我无法编译代码,所以我无法确定问题。