.NET 4.6 RC x64比x86版本(发布版)慢两倍。

10

Net 4.6 RC x64版本比x86版本(发布版)慢了两倍:

考虑以下代码片段:

class SpectralNorm
{
    public static void Main(String[] args)
    {
        int n = 5500;
        if (args.Length > 0) n = Int32.Parse(args[0]);

        var spec = new SpectralNorm();
        var watch = Stopwatch.StartNew();
        var res = spec.Approximate(n);

        Console.WriteLine("{0:f9} -- {1}", res, watch.Elapsed.TotalMilliseconds);
    }

    double Approximate(int n)
    {
        // create unit vector
        double[] u = new double[n];
        for (int i = 0; i < n; i++) u[i] = 1;

        // 20 steps of the power method
        double[] v = new double[n];
        for (int i = 0; i < n; i++) v[i] = 0;

        for (int i = 0; i < 10; i++)
        {
            MultiplyAtAv(n, u, v);
            MultiplyAtAv(n, v, u);
        }

        // B=AtA         A multiplied by A transposed
        // v.Bv /(v.v)   eigenvalue of v 
        double vBv = 0, vv = 0;
        for (int i = 0; i < n; i++)
        {
            vBv += u[i] * v[i];
            vv += v[i] * v[i];
        }

        return Math.Sqrt(vBv / vv);
    }


    /* return element i,j of infinite matrix A */
    double A(int i, int j)
    {
        return 1.0 / ((i + j) * (i + j + 1) / 2 + i + 1);
    }

    /* multiply vector v by matrix A */
    void MultiplyAv(int n, double[] v, double[] Av)
    {
        for (int i = 0; i < n; i++)
        {
            Av[i] = 0;
            for (int j = 0; j < n; j++) Av[i] += A(i, j) * v[j];
        }
    }

    /* multiply vector v by matrix A transposed */
    void MultiplyAtv(int n, double[] v, double[] Atv)
    {
        for (int i = 0; i < n; i++)
        {
            Atv[i] = 0;
            for (int j = 0; j < n; j++) Atv[i] += A(j, i) * v[j];
        }
    }

    /* multiply vector v by matrix A and then by matrix A transposed */
    void MultiplyAtAv(int n, double[] v, double[] AtAv)
    {
        double[] u = new double[n];
        MultiplyAv(n, v, u);
        MultiplyAtv(n, u, AtAv);
    }
}

在我的电脑上,x86版本需要4.5秒完成,而x64版本需要9.5秒。是否需要特定的标志/设置来优化x64?

更新:

事实证明,RyuJIT在此问题中起了作用。 如果在app.config中启用了useLegacyJit,结果会有所不同,这次x64更快。

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6"/>
  </startup>
  <runtime>
    <useLegacyJit enabled="1" />
 </runtime>
</configuration>

更新

现在该问题已经向CLR团队报告 coreclr, issue 993


4
你是否正在运行发布版本,而不是在调试器中运行它? - Matthew Watson
值得在 for 循环中运行几次并折扣前几次迭代,因为 JIT 编译器需要第一次发挥其魔力。 - Wai Ha Lee
3
.NET 4.6有一个全新的x64即时编译器(项目RyuJIT),您无法在以前的.NET版本中获得可比较的结果。报告严重性能下降的最佳方法是使用connect.microsoft.com,赶在4.6仍处于beta版时报告。 - Hans Passant
实际上4.6是rc而不是beta。这是关于Microsoft Connect的报告:“https://connect.microsoft.com/VisualStudio/feedback/details/1294384”。 - Bijan
你能确认在两种架构上运行的 .net 4.5 运行时,以确认这确实是一个 4.6 的问题吗? - tolanj
显示剩余6条评论
1个回答

4

性能下降的原因在GitHub上有解答;简单来说,这似乎只在Intel机器上重现,而不是在Amd64机器上。内部循环操作。

Av[i] += v[j] * A(i, j);

导致结果为:
IN002a: 000093 lea      eax, [rax+r10+1]
IN002b: 000098 cvtsi2sd xmm1, rax
IN002c: 00009C movsd    xmm2, qword ptr [@RWD00]
IN002d: 0000A4 divsd    xmm2, xmm1
IN002e: 0000A8 movsxd   eax, edi
IN002f: 0000AB movaps   xmm1, xmm2
IN0030: 0000AE mulsd    xmm1, qword ptr [r8+8*rax+16]
IN0031: 0000B5 addsd    xmm0, xmm1
IN0032: 0000B9 movsd    qword ptr [rbx], xmm0

Cvtsi2sd会将xmm寄存器的高8字节保持不变,仅对低8字节进行部分写入。在重现案例中,xmm1被部分写入,但代码中又有其他使用xmm1的指令,这就产生了cvtsi2sd和其他使用xmm1的指令之间的虚假依赖关系,影响了指令并行性。实际上,在生成Int到Float转换的代码时,在cvtsi2sd之前发出“xorps xmm1,xmm1”的命令可以修复性能回归。
解决方法:如果我们在MultiplyAv / MultiplyAvt方法中颠倒乘法操作数的顺序,则也可以避免性能回归。
void MultiplyAv(int n, double[] v, double[] Av)
{
    for (int i = 0; i < n; i++)
    {
        Av[i] = 0;
        for (int j = 0; j < n; j++)  
              Av[i] += v[j] * A(i, j);  //  order of operands reversed
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接