【问题标题】:What is the reason for inner loop performance degradation after upgrade?升级后内循环性能下降的原因是什么?
【发布时间】:2019-06-22 16:21:24
【问题描述】:

我有一个手卷矩阵算法,它可以找到方阵右下平方的最大数(因此在迭代时,某些部分被“跳过”) - 存储为密集矩阵。在从 更新到 之后,它似乎要慢得多——总体下降了大约 50%。经过一番调查,这位于找到绝对最大值的函数的内部循环中。查看 输出,这似乎是由于在紧密循环中插入了一些额外的 指令。以不同的方式重新设计循环似乎可以解决或部分解决问题。 相比之下, 似乎没有这个“问题”。

简化示例(fabs 并不总是需要重现):

#include <cmath>
#include <iostream>

int f_slow(double *A, size_t from, size_t w)
{
    double biga_absval = *A;
    size_t ir = 0,ic=0;
    for ( size_t j = 0; j < w; j++ ) {
      size_t n = j*w;
      for ( ; n < j*w+w; n++ ) {
        if ( fabs(A[n]) <= biga_absval ) {
          biga_absval = fabs( A[n] );
          ir   = j;
          ic   = n;
        }
        n++;
      }
    }

    std::cout << ir <<ic;
    return 0;
}

int f_fast(double *A, size_t from, size_t w)
{
    double* biga = A;
    double biga_absval = *biga;

    double* n_begin = A + from;
    double* n_end = A + w;
    for (double* A_n = n_begin; A_n < n_end; ++A_n) {
      if (fabs(*A_n) > biga_absval) {
        biga_absval = fabs(*A_n);
        biga = A_n;
      }
    }

    std::cout << biga;
    return 0;
}

int f_faster(double *A, size_t from, size_t w)
{
    double biga_absval = *A;
    size_t ir = 0,ic=0;
    for ( size_t j = 0; j < w; j++ ) {
      size_t n = j;
      for ( ; n < j*w+w; n++ ) {
        if ( fabs(A[n]) > biga_absval ) {
          biga_absval = fabs( A[n] );
          ir   = j;
          ic   = n - j*w;
        }
        n++;
      }
    }

    std::cout << ir <<ic;
    return 0;
}

请注意:创建示例仅用于查看输出(索引等不一定有意义):

https://godbolt.org/z/q9rWwi

所以我的问题是:这只是一个(已知的?)优化器错误(?)还是在这种情况下似乎是明显的优化失误背后有一些逻辑?

使用最新稳定版15.9.5

更新:我看到的额外s 在跳转代码之前 - 在编译器资源管理器中找到的最简单方法是右键单击if,然后“滚动到”。

【问题讨论】:

  • f_fast 正在检查 A 的每个元素(1x ++A_n),f_faster 仅每秒检查一次(2x n++ 在内部循环中)......这是故意的吗?
  • 这个问题可以从一些清理中受益。共有三个功能,但尚不清楚这三个功能中哪一个受到 VS2017 减速的影响。这些功能根本不一样。 3 个中的 2 个甚至忽略了 from 参数。
  • @MatthieuBrucher:MSVC 没有/O3 选项。它会忽略它,您会得到默认的调试模式未优化代码。 cl : Command line warning D9002 : ignoring unknown option '/O3'。这与 gcc/clang 不同,其中 -O3 启用完全优化,包括使用 gcc 进行自动矢量化。 (clang 在 -O2 处启用 auto-vec,但 gcc 仅在 -O3 处启用)。
  • 我在stackoverflow.com/questions/32511862/… 中处理了类似的问题,原因是 Visual Studio ~2013 中 C++ 编译器的默认安全设置发生了变化。缓冲区溢出处理已更改,但我想这与您正在处理的问题不完全相同。
  • 可能是 Spectre 缓解措施减缓了速度

标签: vs2010 vs2017 assembler mov gcc vs2017 mov c++ assembly visual-c++ visual-studio-2017 compiler-optimization


【解决方案1】:

好吧,我不知道为什么 VC 在你的情况下变得更糟,但我想提供一些提示如何保护一些操作。

void f_faster( const double* A, const std::size_t w ) {
    double      biga_absval = A[ 0 ];
    std::size_t ir, ic_n;
    for ( std::size_t j = 0; j < w; ++j ) {
        const auto N = j * w + w;
        for ( std::size_t n = j; n < N; n += 2 ) {
            if ( const auto new_big_a = std::fabs( A[ n ] ); new_big_a > biga_absval ) {
                biga_absval = new_big_a;
                ir          = j;
                ic_n        = n;
            }
        }
    }

    std::cout << ir << ( ic_n - ir * w );
}
  • 不要在内循环中计算ic,只存储n以备后用
  • 使用 const 帮助优化器
  • 不要两次评估 std::fabs
  • 后增量创建一个您不需要的副本(可能已优化掉)
  • 将循环的上限存储在外部,否则可能会被重新评估(可能会优化掉)
  • 只需将 n 增加 2,而不是 2 增加 1
  • 不要使用未使用的值进行初始化

也许这已经足以摆脱多余的 mov 了吗?

【讨论】:

  • 这可以通过基于向量 AND (abs) / compare 递增和混合整数 n 值向量来为 x86 自动向量化。仅对double 有利可图,使用 AVX 对每个向量执行 4 个双打,其中只有 2 个有用,因为它的步长为 2。(在循环结束时水平检查时忽略奇数元素。)总体上可能不值得,尽管使用 float 使用 AVX 会很好。
  • 是否愿意在编译器资源管理器中提供一个链接以进行比较?
  • 我不相信您的建议会有所帮助 - 即。最终得到更快的汇编代码 - 但请随意修改我在编译器资源管理器上的示例以证明我错了。
  • 你是对的,差异很小,你可以在这里实验:quick-bench.com/Y7LD3P2QRwmrp8U0G6a_vyoQouY 这也取决于编译器。我的猜测是,代码中的某些内容会使您的编译器咳嗽并稍微更改它可能会解决您的编译器问题。您只能通过对您的设置进行编译和基准测试来了解。比较程序集是不可靠的,因为几乎无法预测您的 CPU 实际在做什么。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-09-08
  • 1970-01-01
  • 1970-01-01
  • 2021-06-14
  • 2020-10-09
相关资源
最近更新 更多