RError.com

RError.com Logo RError.com Logo

RError.com Navigation

  • 主页

Mobile menu

Close
  • 主页
  • 系统&网络
    • 热门问题
    • 最新问题
    • 标签
  • Ubuntu
    • 热门问题
    • 最新问题
    • 标签
  • 帮助
主页 / 问题 / 1231785
Accepted
aepot
aepot
Asked:2022-01-15 19:49:19 +0000 UTC2022-01-15 19:49:19 +0000 UTC 2022-01-15 19:49:19 +0000 UTC

使用 X86 Intrinsics 时的托管代码与非托管代码

  • 772

我了解到,在使用内部时,System.Runtime.Intrinsics.X86不必使用指针来寻址数据,但您可以简单地使用 转换一个数据数组,它的工作速度与在代码System.Runtime.InteropServices.MemoryMarshal中使用指针一样快。unsafe我很惊讶并使用Benchmark.NET测试了性能。

我写了 4 个基准,一个标量来检查结果,用它来检查System.Numerics.Vector<T>性能(这很有趣),实际上是 2 个基于Vector256<int>托管和非托管代码的测试。

我做了最简单的任务——1000 万个元素的数组元素的总和。我意识到该实现有一个限制,数组的长度必须是 8 的倍数 - 256 位向量的长度(8 x 32),否则输出中的结果将是不可预测的。

public class SumTest
{
    private static readonly int[] _numbers = Enumerable.Repeat(2, 100000000).ToArray();

    public IEnumerable<object> Params
    {
        get
        {
            yield return _numbers;
        }
    }

    [Benchmark]
    [ArgumentsSource(nameof(Params))]
    public int SumScalar(int[] numbers)
    {
        int result = 0;
        for (int i = 0; i < numbers.Length; i++)
        {
            result += numbers[i];
        }
        return result;
    }

    [Benchmark]
    [ArgumentsSource(nameof(Params))]
    public int SumNumerics(int[] numbers)
    {
        Vector<int> acc = Vector<int>.Zero;
        for (int i = 0; i < numbers.Length; i += Vector<int>.Count)
        {
            Vector<int> v = new Vector<int>(numbers, i);
            acc += v;
        }
        return Vector.Dot(acc, Vector<int>.One);
    }

    [Benchmark]
    [ArgumentsSource(nameof(Params))]
    public int SumIntrinsics(int[] numbers)
    {
        ReadOnlySpan<Vector256<int>> vectors = MemoryMarshal.Cast<int, Vector256<int>>(numbers);
        Vector256<int> acc = Vector256<int>.Zero;
        for (int i = 0; i < vectors.Length; i++)
        {
            acc = Avx2.Add(acc, vectors[i]);
        }
        Vector128<int> r = Ssse3.HorizontalAdd(acc.GetUpper(), acc.GetLower());
        r = Ssse3.HorizontalAdd(r, r);
        r = Ssse3.HorizontalAdd(r, r);
        return r.GetElement(0);
    }

    [Benchmark]
    [ArgumentsSource(nameof(Params))]
    public unsafe int SumIntrinsicsUnsafe(int[] numbers)
    {
        Vector256<int> acc = Vector256<int>.Zero;
        fixed (int* numPtr = numbers)
        {
            int* endPtr = numPtr + numbers.Length;
            for (int* numPos = numPtr; numPos < endPtr; numPos += 8)
            {
                Vector256<int> v = Avx.LoadVector256(numPos);
                acc = Avx2.Add(acc, v);
            }
            Vector128<int> r = Ssse3.HorizontalAdd(acc.GetUpper(), acc.GetLower());
            r = Ssse3.HorizontalAdd(r, r);
            r = Ssse3.HorizontalAdd(r, r);
            return r.GetElement(0);
        }
    }
}

检查了输出

int[] numbers = Enumerable.Repeat(2, 100000000).ToArray();
SumTest sum = new SumTest();
Console.WriteLine(sum.SumScalar(numbers));
Console.WriteLine(sum.SumNumerics(numbers));
Console.WriteLine(sum.SumIntrinsics(numbers));
Console.WriteLine(sum.SumIntrinsicsUnsafe(numbers));
200000000
200000000
200000000
200000000

也就是说,一切正常。

我收集并启动了基准测试。

var summary = BenchmarkRunner.Run<SumTest>();

而他又吃了一惊。

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, CoreFX 5.0.220.61120), X64 RyuJIT

|              Method |          numbers |     Mean |    Error |   StdDev |
|-------------------- |----------------- |---------:|---------:|---------:|
|           SumScalar | Int32[100000000] | 83.69 ms | 0.466 ms | 0.436 ms |
|         SumNumerics | Int32[100000000] | 31.30 ms | 0.303 ms | 0.268 ms |
|       SumIntrinsics | Int32[100000000] | 28.98 ms | 0.282 ms | 0.236 ms |
| SumIntrinsicsUnsafe | Int32[100000000] | 28.80 ms | 0.191 ms | 0.169 ms |

也就是说,统计误差之间SumIntrinsics和SumIntrinsicsUnsafe内部的差异(StdDev)。

问题:这是什么动物MemoryMarshal,现在在使用内在函数时使用它是否有意义unsafe,实际上是使用向量?

如果问题是是否可以将计算结果以安全代码写入数组 - 是的,有可能,就像数组被强制转换并且写入向量的所有信息都将在数组中一样,即也就是说,工作与使用常规结构数组完全相同。换句话说,不安全代码的好处并不是立即可见的。好吧,仅当源数据最初以指针的形式出现,而不是以托管数组的形式出现时,但那里可能存在细微差别,我对此主题并不深入。

顺便说一句,我很惊喜Vector<T>。我认为在代码对性能不是超级敏感的情况下,可以使用它来Numerics支持跨处理器。


添加

我尝试再次重写该方法SumNumerics或添加另一个版本的实现SumIntrinsicsHybrid。

[Benchmark]
[ArgumentsSource(nameof(Params))]
public int SumNumerics(int[] numbers)
{
    ReadOnlySpan<Vector<int>> vectors = MemoryMarshal.Cast<int, Vector<int>>(numbers);
    Vector<int> acc = Vector<int>.Zero;
    for (int i = 0; i < vectors.Length; i ++)
    {
        acc += vectors[i];
    }
    return Vector.Dot(acc, Vector<int>.One);
}

[Benchmark]
[ArgumentsSource(nameof(Params))]
public unsafe int SumIntrinsicsHybrid(int[] numbers)
{
    ReadOnlySpan<Vector256<int>> vectors = MemoryMarshal.Cast<int, Vector256<int>>(numbers);
    Vector256<int> acc = Vector256<int>.Zero;
    fixed (Vector256<int>* numPtr = vectors)
    {
        Vector256<int>* endPtr = numPtr + vectors.Length;
        for (Vector256<int>* numPos = numPtr; numPos < endPtr; numPos++)
        {
            acc = Avx2.Add(acc, *numPos);
        }
        Vector128<int> r = Ssse3.HorizontalAdd(acc.GetUpper(), acc.GetLower());
        r = Ssse3.HorizontalAdd(r, r);
        r = Ssse3.HorizontalAdd(r, r);
        return r.GetElement(0);
    }
}

基准再次表明,在帮助下进行铸造MemoryMarshal,如果不是免费的,那么完全可以得到回报。

|              Method |          numbers |     Mean |    Error |   StdDev |
|-------------------- |----------------- |---------:|---------:|---------:|
|           SumScalar | Int32[100000000] | 83.30 ms | 0.214 ms | 0.189 ms |
|         SumNumerics | Int32[100000000] | 28.85 ms | 0.222 ms | 0.207 ms |
|       SumIntrinsics | Int32[100000000] | 28.74 ms | 0.145 ms | 0.136 ms |
| SumIntrinsicsUnsafe | Int32[100000000] | 28.14 ms | 0.234 ms | 0.195 ms |
| SumIntrinsicsHybrid | Int32[100000000] | 28.09 ms | 0.174 ms | 0.163 ms |

小阵列测试

|              Method |     numbers |      Mean |    Error |   StdDev |
|-------------------- |------------ |----------:|---------:|---------:|
|           SumScalar | Int32[1000] | 712.65 ns | 2.889 ns | 2.702 ns |
|         SumNumerics | Int32[1000] |  81.22 ns | 0.466 ns | 0.436 ns |
|       SumIntrinsics | Int32[1000] |  82.63 ns | 0.311 ns | 0.291 ns |
| SumIntrinsicsUnsafe | Int32[1000] |  60.66 ns | 0.347 ns | 0.308 ns |
| SumIntrinsicsHybrid | Int32[1000] |  61.01 ns | 0.418 ns | 0.370 ns |
c#
  • 1 1 个回答
  • 10 Views

1 个回答

  • Voted
  1. Best Answer
    Артём Оконечников
    2022-01-16T08:06:07Z2022-01-16T08:06:07Z

    也许您的问题可以转化为Span<T>/是什么ReadOnlySpan<T>。虽然很肤浅,但是一篇概述文章给出了一个思路\u200b\u200bthis:

    • 关于 Span 的一切:探索新的 .NET 支柱

    简而言之,您感兴趣的是定义类型:

    public readonly ref struct Span<T>
    {
      private readonly ref T _pointer;
      private readonly int _length;
    
      ...
    }
    

    Span 是包含此技巧的仅堆栈结构:

    readonly ref T _pointer
    

    没有上下文,不清楚这是否更接近 C++ 术语中的链接或指针,所以我将使用指针这个词。现在,这个新的内部类型是一个跟踪指针。而且,与 operator 不同的fixed是,现在堆上没有任何东西是固定的,GC 本身会在压缩阶段之后更改此指针的地址。

    根据文档,跟踪此类指针在性能方面的成本很高,因此 Span 被制成一个ref 结构,即使作为对象的一部分也不能移动到堆中。

    这些引用称为内部指针,对于 .NET 运行时的垃圾收集器来说,跟踪它们是一项相对昂贵的操作。因此,运行时将这些 ref 限制为仅存在于堆栈上,因为它为可能存在的内部指针的数量提供了隐式的下限

    实际上,span 有高级的亲戚Memory<T>,ReadOnlyMemory<T>不仅可以包装数组。但是这篇文章对它们的揭示很差:-(


    这是我想到的其他事情。如果 GC 在压缩阶段之后动态更改此类链接的地址。但是,正如我们所知,大对象最终会在LOH中它们已经变得未重定位,那么如果您更改测试以使其适用于小数组但在垃圾收集器的背景下,那么它可能会显示更多还是不太显着的性能下降?

    • 1

相关问题

  • 使用嵌套类导出 xml 文件

  • 分层数据模板 [WPF]

  • 如何在 WPF 中为 ListView 手动创建列?

  • 在 2D 空间中,Collider 2D 挂在玩家身上,它对敌人的重量相同,我需要它这样当它们碰撞时,它们不会飞向不同的方向。统一

  • 如何在 c# 中使用 python 神经网络来创建语音合成?

  • 如何知道类中的方法是否属于接口?

Sidebar

Stats

  • 问题 10021
  • Answers 30001
  • 最佳答案 8000
  • 用户 6900
  • 常问
  • 回答
  • Marko Smith

    表格填充不起作用

    • 2 个回答
  • Marko Smith

    提示 50/50,有两个,其中一个是正确的

    • 1 个回答
  • Marko Smith

    在 PyQt5 中停止进程

    • 1 个回答
  • Marko Smith

    我的脚本不工作

    • 1 个回答
  • Marko Smith

    在文本文件中写入和读取列表

    • 2 个回答
  • Marko Smith

    如何像屏幕截图中那样并排排列这些块?

    • 1 个回答
  • Marko Smith

    确定文本文件中每一行的字符数

    • 2 个回答
  • Marko Smith

    将接口对象传递给 JAVA 构造函数

    • 1 个回答
  • Marko Smith

    正确更新数据库中的数据

    • 1 个回答
  • Marko Smith

    Python解析不是css

    • 1 个回答
  • Martin Hope
    Alexandr_TT 2020年新年大赛! 2020-12-20 18:20:21 +0000 UTC
  • Martin Hope
    Alexandr_TT 圣诞树动画 2020-12-23 00:38:08 +0000 UTC
  • Martin Hope
    Air 究竟是什么标识了网站访问者? 2020-11-03 15:49:20 +0000 UTC
  • Martin Hope
    Qwertiy 号码显示 9223372036854775807 2020-07-11 18:16:49 +0000 UTC
  • Martin Hope
    user216109 如何为黑客设下陷阱,或充分击退攻击? 2020-05-10 02:22:52 +0000 UTC
  • Martin Hope
    Qwertiy 并变成3个无穷大 2020-11-06 07:15:57 +0000 UTC
  • Martin Hope
    koks_rs 什么是样板代码? 2020-10-27 15:43:19 +0000 UTC
  • Martin Hope
    Sirop4ik 向 git 提交发布的正确方法是什么? 2020-10-05 00:02:00 +0000 UTC
  • Martin Hope
    faoxis 为什么在这么多示例中函数都称为 foo? 2020-08-15 04:42:49 +0000 UTC
  • Martin Hope
    Pavel Mayorov 如何从事件或回调函数中返回值?或者至少等他们完成。 2020-08-11 16:49:28 +0000 UTC

热门标签

javascript python java php c# c++ html android jquery mysql

Explore

  • 主页
  • 问题
    • 热门问题
    • 最新问题
  • 标签
  • 帮助

Footer

RError.com

关于我们

  • 关于我们
  • 联系我们

Legal Stuff

  • Privacy Policy

帮助

© 2023 RError.com All Rights Reserve   沪ICP备12040472号-5