我正在编写基本的线性代数子程序(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,反之亦然)