3

我正在编写基本的线性代数子程序(BLAS)库。fma 代码的性能存在一个问题。

using System;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;

namespace LinearAlgebra
{
    public static class Level1
    {
        const int Vector256Length = 8;
        const int Vector256Block0 = Vector256Length * 0;
        const int Vector256Block1 = Vector256Length * 1;
        const int Vector256Block2 = Vector256Length * 2;

        public static unsafe void ApplyAffineTransformFma(int N, float* X, float* Y, float m11, float m12, float m21, float m22, float dx, float dy)
        {
            var vm11 = Avx.BroadcastScalarToVector256(&m11);
            var vm12 = Avx.BroadcastScalarToVector256(&m12);
            var vm21 = Avx.BroadcastScalarToVector256(&m21);
            var vm22 = Avx.BroadcastScalarToVector256(&m22);
            var vdx = Avx.BroadcastScalarToVector256(&dx);
            var vdy = Avx.BroadcastScalarToVector256(&dy);

            int n = 0;
            for (; n <= N - Vector256Block2; n += Vector256Block2)
            {
                float* xn = X + n;
                float* yn = Y + n;

                var vx0 = Avx.LoadVector256(xn + Vector256Block0);
                var vx1 = Avx.LoadVector256(xn + Vector256Block1);
                var vy0 = Avx.LoadVector256(yn + Vector256Block0);
                var vy1 = Avx.LoadVector256(yn + Vector256Block1);

                var sx0 = Fma.MultiplyAdd(vm21, vy0, vdx);
                var sx1 = Fma.MultiplyAdd(vm21, vy1, vdx);
                var sy0 = Fma.MultiplyAdd(vm12, vx0, vdy);
                var sy1 = Fma.MultiplyAdd(vm12, vx1, vdy);

                vx0 = Fma.MultiplyAdd(vm11, vx0, sx0);
                vx1 = Fma.MultiplyAdd(vm11, vx1, sx1);
                vy0 = Fma.MultiplyAdd(vm22, vy0, sy0);
                vy1 = Fma.MultiplyAdd(vm22, vy1, sy1);

                Avx.Store(xn + Vector256Block0, vx0);
                Avx.Store(xn + Vector256Block1, vx1);
                Avx.Store(yn + Vector256Block0, vy0);
                Avx.Store(yn + Vector256Block1, vy1);
            }

            for (; n <= N - Vector256Length; n += Vector256Length)
            {
                float* xn = X + n;
                float* yn = Y + n;

                var vx = Avx.LoadVector256(xn);
                var vy = Avx.LoadVector256(yn);

                var sx = Fma.MultiplyAdd(vm21, vy, vdx);
                var sy = Fma.MultiplyAdd(vm12, vx, vdy);

                vx = Fma.MultiplyAdd(vm11, vx, sx);
                vy = Fma.MultiplyAdd(vm22, vy, sy);

                Avx.Store(xn, vx);
                Avx.Store(yn, vy);
            }

            ApplyAffineTransformOdd(N - n, X + n, Y + n, m11, m12, m21, m22, dx, dy);
        }

        public static unsafe void ApplyAffineTransformAvx(int N, float* X, float* Y, float m11, float m12, float m21, float m22, float dx, float dy)
        {
            var vm11 = Avx.BroadcastScalarToVector256(&m11);
            var vm12 = Avx.BroadcastScalarToVector256(&m12);
            var vm21 = Avx.BroadcastScalarToVector256(&m21);
            var vm22 = Avx.BroadcastScalarToVector256(&m22);
            var vdx = Avx.BroadcastScalarToVector256(&dx);
            var vdy = Avx.BroadcastScalarToVector256(&dy);

            int n = 0;
            for (; n <= N - Vector256Length; n += Vector256Length)
            {
                float* xn = X + n;
                float* yn = Y + n;

                var vx = Avx.LoadVector256(xn);
                var vy = Avx.LoadVector256(yn);

                var x1 = Avx.Multiply(vm11, vx);
                var x2 = Avx.Multiply(vm21, vy);
                var y1 = Avx.Multiply(vm12, vx);
                var y2 = Avx.Multiply(vm22, vy);

                x1 = Avx.Add(x1, x2);
                y1 = Avx.Add(y1, y2);

                vx = Avx.Add(x1, vdx);
                vy = Avx.Add(y1, vdy);

                Avx.Store(xn, vx);
                Avx.Store(yn, vy);
            }

            ApplyAffineTransformOdd(N - n, X + n, Y + n, m11, m12, m21, m22, dx, dy);
        }

        public static unsafe void ApplyAffineTransformOdd(int N, float* X, float* Y, float m11, float m12, float m21, float m22, float dx, float dy)
        {
            for (int n = 0; n < N; n++)
            {
                float xn = X[n];
                float yn = Y[n];

                X[n] = m11 * xn + m21 * yn + dx;
                Y[n] = m12 * xn + m22 * yn + dy;
            }
        }
    }
}

以下是基准测试结果。测试机为Intel Core i5-7200U(Skylake架构)。该代码是使用 .NET 5 在 x64 模式下编译的。

方法 ñ 意思是 错误 标准差 最大限度 中位数 比率
仿射变换Fma 68 1.805 我们 0.0018 我们 0.0017 我们 1.802 我们 1.808 我们 1.805 我们 1.00
仿射变换Avx 68 1.152 我们 0.0158 我们 0.0140 我们 1.137 我们 1.184 我们 1.150 我们 0.64
仿射变换Fma 1159 25.966 我们 0.1048 我们 0.0929 我们 25.843 我们 26.114 我们 25.999 我们 1.00
仿射变换Avx 1159 14.070 我们 0.0174 我们 0.0145 我们 14.051 我们 14.104 我们 14.066 我们 0.54
仿射变换Fma 4101 90.094 我们 0.1041 我们 0.0974 我们 89.865 我们 90.214 我们 90.140 我们 1.00
仿射变换Avx 4101 48.180 我们 0.0933 我们 0.0779 我们 48.089 我们 48.320 我们 48.149 我们 0.53
仿射变换Fma 16389 360.215 我们 0.2143 我们 0.1789 我们 359.840 我们 360.456 我们 360.266 我们 1.00
仿射变换Avx 16389 191.222 我们 0.3403 我们 0.3183 我们 190.660 我们 191.765 我们 191.170 我们 0.53
仿射变换Fma 32773 725.299 我们 0.8294 我们 0.7758 我们 723.925 我们 726.415 我们 725.374 我们 1.00
仿射变换Avx 32773 379.920 我们 0.8381 我们 0.6999 我们 378.887 我们 381.257 我们 379.776 我们 0.52

基准代码:

using BenchmarkDotNet.Attributes;
using LinearAlgebra;
using LinearAlgebra.Tests;

namespace Benchmarks.LinearAlgebra
{
    [MinColumn, MaxColumn, MedianColumn]
    public unsafe class TransformBenchmark
    {
        [Params(68, 1159, 4101, 16389, 32773)]
        public int N { get; set; }

        public float[] X;
        public float[] Y;

        const float m11 = 1f;
        const float m12 = 0f;
        const float m21 = 1f;
        const float m22 = 1f;
        const float dx = 0f;
        const float dy = 0f;

        [GlobalSetup]
        public void GlobalSetup()
        {
            X = LinAlgGenerator.GetRandomVector(N);
            Y = LinAlgGenerator.GetRandomVector(N);
        }

        [Benchmark(Baseline = true)]
        public void AffineTransformFma()
        {
            fixed (float* ptrX = X, ptrY = Y)
            {
                Level1.ApplyAffineTransformFma(N, ptrX, ptrY, m11, m12, m21, m22, dx, dy);
            }
        }
        
        [Benchmark]
        public void AffineTransformAvx()
        {
            fixed (float* ptrX = X, ptrY = Y)
            {
                Level1.ApplyAffineTransformAvx(N, ptrX, ptrY, m11, m12, m21, m22, dx, dy);
            }
        }
    }
}

这有点奇怪。Fma 代码几乎比 avx 慢两倍!基于https://software.intel.com/sites/landingpage/IntrinsicsGuide/ Fma.MultiplyAdd方法有latency=4和throughput=0.5这意味着可以在4x4x0.5=8个周期内执行4个独立操作(假设处理器只有一个fma 端口,这当然不是真的)。

该方法的 Fma 版本使用两个这样的块,因此加载(延迟 = 7 和吞吐量 = 0.5)和保存(延迟 = 5 和吞吐量 = 1)的一次迭代的总体性能是 4x7x0.5 + 2x4x4x0.5 + 4x5x1 = 50 个周期每 16 个浮点数。

Avx 版本的代码使用 4 个独立的乘法运算(延迟=4 和吞吐量=0.5)和两个独立的加法运算(延迟=4 和吞吐量=0.5)的两个块。因此,加载和保存的整体性能应该是:2x7x0.5 + 4x4x0.5 + 2x4x0.5 + 2x4x0.5 +2x5x1 = 每 8 个浮点数 33 个周期(每 16 个浮点数 66 个周期)。

理论上avx代码应该比fma代码慢。我究竟做错了什么?该代码使用的寄存器不超过 16 个 256 位。我还尝试在没有流水线的情况下测试 Fma 版本的代码 - 结果是相同的(avx 性能更好)。

更新 看起来这只是 BenchmarkDotNet 中的一个错误。在没有 BenchmarkDotNet 框架的情况下编写了几次基准代码后,我得到了以下结果:

方法 ñ 时间 比率
仿射变换Avx 68 27.24 纳秒 1.00
仿射变换Fma 68 29.19 纳秒 1.07
仿射变换Avx 1159 258.33 纳秒 1.00
仿射变换Fma 1159 188.40 纳秒 0.73
仿射变换Avx 4101 818.48 纳秒 1.00
仿射变换Fma 4101 582.80 纳秒 0.71
仿射变换Avx 16389 4,263.31 纳秒 1.00
仿射变换Fma 16389 2,959.02 纳秒 0.69
仿射变换Avx 32773 9,782.48 纳秒 1.00
仿射变换Fma 32773 6,943.25 纳秒 0.71

是的——在大多数情况下,fma 版本的速度提高了 30%!基准测试的结果不取决于它们的顺序(先是 avx,然后是 fma,反之亦然)

4

0 回答 0