实现 C# 硬件内在封装问题

如何解决实现 C# 硬件内在封装问题

我正在尝试利用硬件内在的强大功能,只是为了测试创建一个基于 Avx2 指令的函数,并将其与我当前完全没有内在的 Vector 实现进行比较。

当我对 2 个做同样事情的函数进行基准测试时,内在函数实际上慢了 2 倍给我留下了深刻的印象。我对此进行了调查,发现计算本身要快 3.8 倍,但是当我开始创建我的包装器结构并返回结果时,它实际上花费了最多的时间。

这是我对内在方法的实现:

public static Vector4FHW Subtract(Vector4FHW left,Vector4FHW right)
{
    if (Avx2.IsSupported)
    {
        var left1 = Vector128.Create(left.X,left.Y,left.Z,left.W);
        var right1 = Vector128.Create(right.X,right.Y,right.Z,right.W);
        var result = Avx2.Subtract(left1,right1);
 
        var x = result.GetElement(0);
        var y = result.GetElement(1);
        var z = result.GetElement(2);
        var w = result.GetElement(3);
 
        return new Vector4FHW(x,y,z,w);
    }

    return default;
}

这是我对旧 Vector 的幼稚实现:

public static void Subtract(ref Vector3F left,ref Vector3F right,out Vector3F result)
{
   result = new Vector3F(left.X - right.X,left.Y - right.Y,left.Z - right.Z);
}

我使用 BenchmarkDotNet 进行了基准测试,我调用了 Subtract 1 000 000 次,这是我的结果:

有了硬件支持,我有 ~3170 us,没有 - 970 us

我的主要问题是:我做错了什么,与我的旧实现相比,创建带有值的 C# 结构需要很长时间和/或我可以在这里做一些额外的优化?

更新

我的 Vector4FHW 和 Vector3F 实际上是相同的结构。它们看起来像这样:

[StructLayout(LayoutKind.Sequential)]
public struct Vector4FHW
{
    public float X;

    public float Y;

    public float Z;

    public float W;

    public Vector4FHW(float x,float y,float z,float w)
    {
        X = x;
        Y = y;
        Z = z;
            W = w;
    }
    //...
}

这是我的测试。它们也很简单:

[Benchmark]
public void SubtractBenchMarkAccelerated()
{
    for (int i = 0; i < 1000000; i++)
    {
        Vector4FHW.Subtract(new Vector4FHW(1,20,60,15),new Vector4FHW(20,48,79,19));
    }
}

[Benchmark]
public void SubtractBenchMark()
{
    for (int i = 0; i < 1000000; i++)
    {
        Vector4F.Subtract(new Vector4F(1,new Vector4F(20,19));
    }
}

解决方法

你可以用这种方式进行 3+3 个双打的单次运算

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector3D Substract(Vector3D left,Vector3D right)
{
    Vector256<double> v0 = Vector256.Create(left.X,left.Y,left.Z,0);
    Vector256<double> v1 = Vector256.Create(right.X,right.Y,right.Z,0);
    Vector256<double> result = Avx.Subtract(v0,v1);
    return new Vector3D(result.GetElement(0),result.GetElement(1),result.GetElement(2));
}

MethodImplOptions.AggressiveInlining 告诉编译器将方法的代码嵌入到调用者的主体中(如果可能)。输出汇编中没有方法调用,只有计算。

它可能更快,但您的测试有 2 个问题。

  1. 不要每次操作都检查 Avx2.IsSupported,每个应用程序生命周期检查一次。
  2. 不要在循环中创建数据,内存分配会使测试变得缓慢和肮脏。

干净的测试看起来像这样

[Benchmark]
public void SubtractBenchMarkAccelerated()
{
    Vector3D vector1 = new Vector3D(1.5,2.5,3.5);
    Vector3D vector2 = new Vector3D(0.1,0.2,0.3);
    for (int i = 0; i < 1000000; i++)
    {
        Subtract(vector1,vector2);
    }
}

但是如果只使用了 Vector256 容量的 75%,就会出现单次操作的问题。它可以快 25% 吗?是的,有更多数据。


这只是故事的开始。假设您要一次计算 4 组向量。 4 对 4。表演魔术开始的地方。

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector3D[] SubstractArray(Vector3D[] left,Vector3D[] right)
{
    var v0 = MemoryMarshal.Cast<Vector3D,Vector256<double>>(left);
    var v1 = MemoryMarshal.Cast<Vector3D,Vector256<double>>(right);
    Vector3D[] result = new Vector3D[left.Length];
    var r = MemoryMarshal.Cast<Vector3D,Vector256<double>>(result);

    for (int i = 0; i < v0.Length; i++) // v0.Length = 3 here,not 4
    {
        r[i] = Avx.Subtract(v0[i],v1[i]);
    }

    return result;
}

MemoryMarshal.Cast 不复制任何东西,它只是让 Span<T> 指向与源数组相同的内存,因此它是闪电般的。我测试过。

测试看起来像这样。

[Benchmark]
public void SubtractBenchMarkAccelerated4()
{
    Vector3D[] array1 = new Vector3D[4];
    array1[0] = new Vector3D(1.5,3.5);
    array1[1] = new Vector3D(1.5,3.5);
    array1[2] = new Vector3D(1.5,3.5);
    array1[3] = new Vector3D(1.5,3.5);
    Vector3D[] array2 = new Vector3D[4];
    array2[0] = new Vector3D(0.1,0.3);
    array2[1] = new Vector3D(0.1,0.3);
    array2[2] = new Vector3D(0.1,0.3);
    array2[3] = new Vector3D(0.1,0.3);
    for (int i = 0; i < 1000000; i++)
    {
        SubstractArray(array1,array2);
    }
}

在 1000000 的同时计算 4000000 个向量,为什么不呢?您可以通过这种方式计算任意数量的向量。请确保doubles count % 4 == 0

能比上面的例子更快吗?是的,但只有不安全的代码。

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static unsafe Vector3D[] SubstractArrayUnsafe(Vector3D[] left,Vector256<double>>(result);

    fixed (Vector256<double>* vPtr0 = v0,vPtr1 = v1,rPtr = r)
    {
        Vector256<double>* endPtr0 = vPtr0 + v0.Length;
        Vector256<double>* vPos0 = vPtr0;
        Vector256<double>* vPos1 = vPtr1;
        Vector256<double>* rPos = rPtr;
        while (vPos0 < endPtr0)
        {
            *rPos = Avx.Subtract(*vPos0,*vPos1);
            vPos0++;
            vPos1++;
            rPos++;
        }
    }
    return result;
}

您不仅可以通过这种方式减去 Vector3D[],还可以减去您的 Vector4D[] 或简单的 double[] 数组。

还可以访问这些有用的页面:x86/x64 SIMD Instruction List (SSE to AVX512)this one

更新

针对相同尺寸的包裹优化单次操作

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector4DHW Substract(ref Vector4DHW left,ref Vector4DHW right)
{
    var left1 = Unsafe.As<Vector4DHW,Vector256<double>>(ref left);
    var right1 = Unsafe.As<Vector4DHW,Vector256<double>>(ref right);
    var result = Avx.Subtract(left1,right1);
    return Unsafe.As<Vector256<double>,Vector4DHW>(ref result);
}

让我们进行基准测试

class Program
{
    static void Main()
    {
        var summary = BenchmarkRunner.Run<MyBenchmark>();
        Console.ReadKey();
    }
}

[StructLayout(LayoutKind.Sequential)]
public struct Vector4DHW
{
    public double X;

    public double Y;

    public double Z;

    public double W;

    public Vector4DHW(double x,double y,double z,double w)
    {
        X = x;
        Y = y;
        Z = z;
        W = w;
    }
}

public class MyBenchmark
{
    private Vector4DHW vector1 = new Vector4DHW(1.5,3.5,4.5);
    private Vector4DHW vector2 = new Vector4DHW(0.1,0.3,0.4);

    [Benchmark]
    public void Loop()
    {
        for (int i = 0; i < 1000000; i++)
        {
            var j = i;
        }
    }

    [Benchmark]
    public void Substract()
    {
        for (int i = 0; i < 1000000; i++)
        {
            var result = Substract(ref vector1,ref vector2);
        }
    }

    [Benchmark]
    public void SubstractAvx()
    {
        for (int i = 0; i < 1000000; i++)
        {
            var result = SubstractAvx(ref vector1,ref vector2);
        }
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static Vector4DHW Substract(ref Vector4DHW left,ref Vector4DHW right)
    {
        return new Vector4DHW(left.X - right.X,left.Y - right.Y,left.Z - right.Z,left.W - right.W);
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static Vector4DHW SubstractAvx(ref Vector4DHW left,ref Vector4DHW right)
    {
        var left1 = Unsafe.As<Vector4DHW,Vector256<double>>(ref left);
        var right1 = Unsafe.As<Vector4DHW,Vector256<double>>(ref right);
        var result = Avx.Subtract(left1,right1);
        return Unsafe.As<Vector256<double>,Vector4DHW>(ref result);
    }
}

去吧!

BenchmarkDotNet=v0.12.1,OS=Windows 10.0.19042
Intel Core i7-4700HQ CPU 2.40GHz (Haswell),1 CPU,8 logical and 4 physical cores
.NET Core SDK=5.0.102
  [Host]     : .NET Core 5.0.2 (CoreCLR 5.0.220.61120,CoreFX 5.0.220.61120),X64 RyuJIT
  DefaultJob : .NET Core 5.0.2 (CoreCLR 5.0.220.61120,X64 RyuJIT

|       Method |       Mean |   Error |  StdDev |
|------------- |-----------:|--------:|--------:|
|         Loop |   317.6 us | 1.36 us | 1.21 us |
|    Substract | 1,427.0 us | 4.14 us | 3.46 us |
| SubstractAvx |   478.0 us | 1.58 us | 1.40 us |

作为结论,我可以说当您试图在性能上节省更多微秒时,内存优化非常重要。甚至堆栈分配也很重要,无论其闪电般的速度如何。最后,for 循环开销消耗了 478 微秒起的大量时间。这就是我单独测量 Loop 开销的原因。

让我们计算一下 AVX 带来的性能提升。

1,427.0 - 317.6 = 1109.4
478.0 - 317.6 = 160.4
1109.4 / 160.4 = 6.92

AVX 几乎快了 7 倍。

更新2

也测试一下

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public unsafe static Vector4DHW Substract(Vector4DHW left,Vector4DHW right)
{
    var result = Avx.Subtract(*(Vector256<double>*)&left,*(Vector256<double>*)&right);
    return *(Vector4DHW*)&result;
}
,

@aepot 好的,我考虑到了你的评论并稍微玩了一下,这是我的结果:

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static Vector4DHW Subtract(Vector4DHW left,Vector4DHW right)
    {
        var left1 = Vector256.Create(left.X,0);
        var right1 = Vector256.Create(right.X,0);
        var result = Avx2.Subtract(left1,right1);

         return new Vector4DHW(result.GetElement(0),result.GetElement(2),result.GetElement(3));
    }

如果我像这样使用它,我会收到 ~2470 us

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static unsafe Vector4DHW Subtract(Vector4DHW left,left.W);
        var right1 = Vector256.Create(right.X,left.W);
        var result = Avx2.Subtract(left1,right1);

        double* value = stackalloc double[4];
        Avx2.Store(value,result);
        return new Vector4DHW(value[0],value[1],value[2],value[3]);
    }

这个变体给了我 ~2089 我们

即使我让我的方法无效并且根本不做这样的转换:

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static void Subtract(Vector4DHW left,right1);
    }

它会给我 ~990 我们。 这是内在函数更快的唯一情况,但我需要让用户有可能看到计算结果

纯 Vector4D 的计算接近 1990 - 2000 我们

因此,我认为使用内在函数进行此类计算没有任何好处。也许在某些情况下会更快,但我认为每个情况都应该分开考虑

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


使用本地python环境可以成功执行 import pandas as pd import matplotlib.pyplot as plt # 设置字体 plt.rcParams[&#39;font.sans-serif&#39;] = [&#39;SimHei&#39;] # 能正确显示负号 p
错误1:Request method ‘DELETE‘ not supported 错误还原:controller层有一个接口,访问该接口时报错:Request method ‘DELETE‘ not supported 错误原因:没有接收到前端传入的参数,修改为如下 参考 错误2:cannot r
错误1:启动docker镜像时报错:Error response from daemon: driver failed programming external connectivity on endpoint quirky_allen 解决方法:重启docker -&gt; systemctl r
错误1:private field ‘xxx‘ is never assigned 按Altʾnter快捷键,选择第2项 参考:https://blog.csdn.net/shi_hong_fei_hei/article/details/88814070 错误2:启动时报错,不能找到主启动类 #
报错如下,通过源不能下载,最后警告pip需升级版本 Requirement already satisfied: pip in c:\users\ychen\appdata\local\programs\python\python310\lib\site-packages (22.0.4) Coll
错误1:maven打包报错 错误还原:使用maven打包项目时报错如下 [ERROR] Failed to execute goal org.apache.maven.plugins:maven-resources-plugin:3.2.0:resources (default-resources)
错误1:服务调用时报错 服务消费者模块assess通过openFeign调用服务提供者模块hires 如下为服务提供者模块hires的控制层接口 @RestController @RequestMapping(&quot;/hires&quot;) public class FeignControl
错误1:运行项目后报如下错误 解决方案 报错2:Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.8.1:compile (default-compile) on project sb 解决方案:在pom.
参考 错误原因 过滤器或拦截器在生效时,redisTemplate还没有注入 解决方案:在注入容器时就生效 @Component //项目运行时就注入Spring容器 public class RedisBean { @Resource private RedisTemplate&lt;String
使用vite构建项目报错 C:\Users\ychen\work&gt;npm init @vitejs/app @vitejs/create-app is deprecated, use npm init vite instead C:\Users\ychen\AppData\Local\npm-
参考1 参考2 解决方案 # 点击安装源 协议选择 http:// 路径填写 mirrors.aliyun.com/centos/8.3.2011/BaseOS/x86_64/os URL类型 软件库URL 其他路径 # 版本 7 mirrors.aliyun.com/centos/7/os/x86
报错1 [root@slave1 data_mocker]# kafka-console-consumer.sh --bootstrap-server slave1:9092 --topic topic_db [2023-12-19 18:31:12,770] WARN [Consumer clie
错误1 # 重写数据 hive (edu)&gt; insert overwrite table dwd_trade_cart_add_inc &gt; select data.id, &gt; data.user_id, &gt; data.course_id, &gt; date_format(
错误1 hive (edu)&gt; insert into huanhuan values(1,&#39;haoge&#39;); Query ID = root_20240110071417_fe1517ad-3607-41f4-bdcf-d00b98ac443e Total jobs = 1
报错1:执行到如下就不执行了,没有显示Successfully registered new MBean. [root@slave1 bin]# /usr/local/software/flume-1.9.0/bin/flume-ng agent -n a1 -c /usr/local/softwa
虚拟及没有启动任何服务器查看jps会显示jps,如果没有显示任何东西 [root@slave2 ~]# jps 9647 Jps 解决方案 # 进入/tmp查看 [root@slave1 dfs]# cd /tmp [root@slave1 tmp]# ll 总用量 48 drwxr-xr-x. 2
报错1 hive&gt; show databases; OK Failed with exception java.io.IOException:java.lang.RuntimeException: Error in configuring object Time taken: 0.474 se
报错1 [root@localhost ~]# vim -bash: vim: 未找到命令 安装vim yum -y install vim* # 查看是否安装成功 [root@hadoop01 hadoop]# rpm -qa |grep vim vim-X11-7.4.629-8.el7_9.x
修改hadoop配置 vi /usr/local/software/hadoop-2.9.2/etc/hadoop/yarn-site.xml # 添加如下 &lt;configuration&gt; &lt;property&gt; &lt;name&gt;yarn.nodemanager.res