.NET 8 中的硬件内在函数

.NET 在通过 JIT 编译器本质上理解的 API 提供对附加硬件功能的访问方面有着悠久的历史。这始于 2014 年的 .NET Framework,并随着 2019 年 .NET Core 3.0 的引入而扩展。从那时起,运行时迭代地提供了更多 API,并在每个版本中更好地利用了这一点。

简要概述:

  • 2014 年 – .NET 4.5.2 – 第一个在System.Numerics命名空间 中公开的 API
    • 介绍Vector<T>
    • 介绍Vector2Vector3Vector4Matrix4x4Quaternion、 和Plane
    • 仅 64 位
    • 另请参阅:https://devblogs.microsoft.com/dotnet/the-jit-finally-propose-jit-and-simd-are-getting-married/
  • 2019 – .NET Core 3.0 – 第一个在System.Runtime.Intrinsics命名空间 中公开的 API
    • 介绍Vector128<T>Vector256<T>
    • 引入SseSse2Sse3Ssse3Sse41Sse42AvxAvx2FmaBmi1Bmi2LzcntPopcntAes, 和Pclmulfor x86/x64
    • 32 位和 64 位支持
    • 另请参阅:https://devblogs.microsoft.com/dotnet/hardware-intrinsics-in-net-core/
  • 2020 – .NET 5 –System.Runtime.Intrinsics命名空间 中添加了 Arm 支持
    • 介绍Vector64<T>
    • 引入AdvSimdArmBaseDpRdmAesCrc32Sha1, 和Sha256for Arm/Arm64
    • 介绍/ X86Base_x86x64
    • 另请参阅:https://devblogs.microsoft.com/dotnet/announcing-net-5-0-preview-7/
  • 2021 年 – .NET 6 – Codegen 和基础设施改进
    • 介绍/ AvxVnni_x86x64
    • 重写System.Numerics实现以使用System.Runtime.Intrinsics
    • 另请参阅:https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-6/
  • 2022 – .NET 7 – 支持编写跨平台算法
    • 引入了跨平台工作的Vector64<T>Vector128<T>和类型的重要新功能Vector256<T>
    • 介绍/ X86Serialize_x86x64
    • 使上述向量类型公开的 API 表面Vector<T>达到奇偶校验
    • 另请参阅:https://devblogs.microsoft.com/dotnet/performance_improvements_in_net_7/
  • 2023 年 – .NET 8 – Wasm支持和 AVX-512
    • 介绍PackedSimdWasmBaseWasm
    • 介绍Vector512<T>
    • 引入Avx512FAvx512BWAvx512CDAvx512DQAvx512Vbmix86/x64
    • 另请参阅:本博客文章的其余部分

由于这项工作,每个版本的 .NET 库和应用程序都获得了更多的能力来利用底层硬件。在这篇文章中,我将深入介绍我们在 .NET 8 中引入的内容及其支持的功能类型。

WebAssembly 支持

WebAssembly(简称 Wasm)本质上是在浏览器中运行的代码,它比典型的解释脚本支持提供更高的性能配置文件。作为一个平台,Wasm 已开始提供底层 SIMD(单指令、多数据)支持,以便可以加速核心算法,而 .NET 相应地选择通过硬件内在函数公开对此功能的支持。

这种支持与其他平台提供的基础非常相似,因此我们不会详细讨论它。相反,您可以简单地期望您使用的现有跨平台算法Vector128<T>将在支持的情况下隐式点亮。如果您想更直接地利用 Wasm 独有的功能,那么您可以显式使用命名空间中的PackedSimd和类公开的 API 。WasmBaseSystem.Runtime.Intrinsics.Wasm

AVX-512 支持

AVX-512 是为 x86 和 x64 计算机提供的新功能集。它带来了大量以前不可用的新指令和硬件功能,包括支持 16 个附加 SIMD 寄存器、专用掩码以及一次操作 512 位数据。访问此功能需要相对较新的处理器,即需要 Intel 的 Skylake-X 或更新版本以及 AMD 的 Zen4 或更新版本。因此,可以利用此新功能的用户数量较少,但它可以为硬件带来的改进仍然很重要,并且值得支持数据繁重的工作负载。此外,JIT 将适时地将这些指令用于现有的 SIMD 代码,只要它确定存在好处。一些例子包括:

  • 当按位条件选择完成时使用vpternlog而不是( )and, andn, orVector128.ConditionalSelect
  • 使用 EVEX 编码将更多操作放入更少字节的代码中,例如嵌入式广播 ( x + Vector128.Create(5))
  • 使用 AVX-512 现在支持的较新指令,例如全角洗牌和许多longulongInt64UInt64) 操作
  • 还有其他改进,此处未列出,并且您可以期望随着时间的推移会添加更多改进
    • .NET 8 中未完成某些情况,例如Vector<T>允许扩展到 512 位

为了支持新的 512 位向量大小,.NET 引入了该Vector512<T>类型。这公开了与其他固定大小的向量类型(例如Vector256<T>. 它还继续公开Vector512.IsHardwareAccelerated允许您确定是否应该在硬件中加速通用逻辑或者是否最终通过软件回退来模拟行为的属性。

Vector512 默认情况下在 Ice Lake 和较新的硬件上使用 AVX-512 进行加速(因此Vector512.IsHardwareAccelerated报告true),其中 AVX-512 指令不会导致 CPU 显着降频;其中,利用 AVX-512 指令可能会导致基于 Skylake-X、Cascade Lake 和 Cooper Lake 的硬件的更显着的降频(另请参阅2.5.3 Skylake Server Power ManagementIntel® 64 and IA-32 Architectures Optimization Reference Manual: Volume 1)。虽然这最终有利于大型工作负载,但它可能会对其他较小的工作负载产生负面影响,因此我们默认在这些平台上进行false报告Vector512.IsHardwareAcceleratedAvx512F.IsSupported仍将报告 true,并且如果直接调用,其底层实现Vector512仍将使用AVX-512指令。这使得工作负载可以利用他们知道明显有益的功能,而不会意外地对其他人造成负面影响。

特别感谢

此功能是通过我们在英特尔的朋友的重大贡献才得以实现的。多年来,.NET 团队和英特尔进行了多次合作,我们在整体设计和实现方面继续合作,使 AVX-512 支持登陆 .NET 8。

.NET 社区还提供了大量的意见和验证,帮助我们取得成功并使发布变得更好。

如果您想做出贡献或提供意见,请加入我们的GitHub 上的dotnet/runtime存储库,并按照我们的时间表收听.NET Foundation YouTube频道上的 API 审核,您可以在其中看到我们讨论 .NET 的新增内容图书馆甚至通过聊天频道提供您自己的输入。

不只是512位吗?

与名称相反,AVX-512 不仅仅支持 512 位。附加寄存器、掩码支持、嵌入式舍入或广播支持以及新指令也都适用于 128 位和 256 位向量。这意味着您现有的工作负载可以隐式地变得更好,并且您可以显式地利用更新的功能,而这种隐式的点亮是不可能的。

当 SSE 于 1999 年首次在 Intel Pentium III 上引入时,它提供了 8 个寄存器,每个寄存器长度为 128 位。这些寄存器被称为xmm0through xmm7。当 x64 平台后来于 2003 年在 AMD Athlon 64 上引入时,它提供了 8 个可供 64 位代码访问的附加寄存器。这些寄存器xmm8通过xmm15. 最初的支持使用了一种简单的编码方案,其工作方式与通用指令非常相似,并且只允许指定 2 个寄存器。对于像加法这样需要 2 个输入的运算,这意味着其中一个寄存器既充当输入又充当输出。这意味着如果您的输入和输出需要不同,则需要 2 条指令才能完成操作。实际上,您z = x + y将成为z = x; z += y。在高级别上,这些行为是相同的,但在低级别上,有 2 个步骤而不是 1 个步骤来实现它。

2011 年,英特尔在基于 Sandy Bridge 的处理器上引入了 AVX,将支持范围扩展到 256 位,这一点得到了进一步扩展。这些较新的寄存器ymm0通过命名ymm15,只有ymm732 位代码可以访问的寄存器。这还引入了一种称为VEX(矢量扩展)的新编码,它允许对 3 个寄存器进行编码。这意味着您可以z = x + y直接编码,而不必将其分成两个单独的步骤。

随后,英特尔于 2017 年与基于 Skylake-X 的处理器一起推出了 AVX-512。这将支持扩展到 512 位,并zmm0通过zmm15. 它还引入了 16 个新寄存器,恰当地命名zmm16zmm31和 ,它们也有xmm16-xmm31ymm16-ymm31变体。与前面的情况一样,zmm732 位代码最多只能访问寄存器。它引入了 8 个新寄存器,名为k0through k7,旨在支持“屏蔽”,以及另一种名为EVEX(增强型矢量扩展)的新编码,它允许表达所有这些新信息。EVEX 编码还具有其他功能,允许以更紧凑的方式表达更常见的信息和操作。这有助于减少代码大小,同时提高性能。

存在哪些新指令?

有很多新功能,太多了,无法在这篇博文中涵盖所有内容。但一些最著名的新指令提供了以下内容:

  • 支持对 64 位整数进行 、 、 和 移位等操作Abs-Max以前Min必须使用多个指令来模拟此功能
  • 支持无符号整数和浮点类型之间的转换
  • 支持处理浮点边缘情况
  • 支持完全重新排列一个向量或多个向量中的元素
  • 支持在一条指令中执行 2 个按位运算

64 位整数支持值得注意,因为这意味着处理 64 位数据不需要使用较慢或替代的代码序列来支持相同的功能。它使您可以更轻松地编写代码并期望其行为相同,而不管您正在使用的基础数据类型如何。

由于类似的原因,浮点到无符号整数的转换支持也值得注意。double从到 的转换long需要一条指令,而从double到 的转换ulong需要许多指令。使用 AVX-512,这成为一条指令,并允许用户在处理无符号数据时获得预期的性能。这在各种图像处理或机器学习场景中很常见。

对浮点数据的扩展支持是我最喜欢的 AVX-512 功能之一。一些示例包括提取无偏指数 ( Avx512F.GetExponent) 或归一化尾数 ( Avx512F.GetMantissa)、将浮点值舍入到特定数量的分数位 ( Avx512F.RoundScale)、将值乘以 2^x ( Avx512F.Scale,在 C 中称为scalebn),执行MinMaxMinMagnitude、 和MaxMagnitude并正确处理+0-0Avx512DQ.Range),甚至执行约简,这在处理像SinCosAvx512DQ.Reduce) 这样的三角函数的大值时非常有用。

vfixupimm然而,我个人最喜欢的指令之一是名为( )的指令Avx512F.Fixup。在较高级别上,该指令可让您检测许多输入边缘情况并将输出“修复”为常见输出之一,并针对每个元素执行此操作。这可以极大地提高某些算法的性能,并大大减少所需的处理量。它的工作方式是需要 4 个输入,分别称为leftrighttablecontrol。它首先对 中的浮点值进行分类,right并确定它是QNaN(0)、SNaN(1)、+/-0(2)、+1(3)、-Infinity(4)、+Infinity(5)、Negative(6) 还是Positive(7)。然后它使用它来读取4tableQNaNbeing 0,读取位0..3Negativebeing6读取位24..27)。这 4 位的值table决定了结果是什么。可能的结果(每个元素)是:

位模式定义
0b0000左[i]
0b0001右[我]
0b0010QNaN(右[i])
0b0011QNaN
0b0100-无穷
0b0101+无限
0b0110是否为负(右[i])?-无穷大 : +无穷大
0b0111-0.0
0b1000+0.0
0b1001-1.0
0b1010+1.0
0b1011+0.5
0b1100+90.0
0b1101PI/2
0b1110最大值
0b1111最小值

SSE 支持在向量中重新排列数据。举例来说,您有0, 1, 2, 3并且想要订购它3, 1, 2, 0。随着 AVX 的引入以及扩展到 256 位,这种支持也同样得到了扩展。然而,由于指令的操作方式,您实际上会执行两次相同的 128 位操作。这使得将现有算法扩展到 256 位变得很简单,因为您实际上只需执行两次相同的操作。然而,当您实际上需要整体考虑整个向量时,它会使使用其他算法变得更加困难。有一些指令可以让您在整个 256 位向量中重新排列数据,但它们通常在如何重新排列数据或它们支持的类型方面受到限制(字节元素的完全洗牌是缺少支持的一个显着示例) )。AVX-512 对于扩展的 512 位支持有许多相同的考虑因素。但是,它还引入了新的指令来填补空白,现在可以让您针对任何大小的元素完全重新排列元素。

最后,我个人最喜欢的另一条指令是名为vpternlogAvx512F.TernaryLogic) 的指令。该指令允许您进行任意 2 个按位运算并将它们组合起来,以便它们可以在一条指令中执行。例如,您可以这样做(a & b) | c。它的工作方式是需要 4 个输入,abccontrol。然后您需要记住 3 个键:A: 0xF0B: 0xCCC: 0xAA。为了表示所需的操作,您只需control通过在这些键上执行该操作来构建 。所以,如果你想简单地返回a,你可以使用0xF0. 如果你想做的话a & b,你会使用(byte)(0xF0 & 0xCC). 如果你想做的话(a & b) | c,那就可以了(byte)((0xF0 & 0xCC) | 0xAA。总共有 256 种可能的不同操作,基本构建块是这些键和以下按位操作:

手术定义
不是〜x
坐标
与非〜x和y
或者X
也不〜x
自由的x^y
异或〜x^y

考虑到上述基本操作,还支持一些特殊操作,并且可以进一步扩展。

手术定义
错误的0x00 的位模式
真的0xFF 的位模式
主要的如果两个或多个输入位为 0,则返回 0;如果两个或多个输入位为 1,则返回 1
次要的如果两个或多个输入位为 1,则返回 0;如果两个或多个输入位为 0,则返回 1
条件选择从逻辑上讲(x & y) | (~x & z),这是有效的,因为它是(x and y) or (x nand y)

在 .NET 8 中,我们没有完成对隐式识别和折叠这些模式以发出的支持vpternlog。我们预计它将在 .NET 9 中首次亮相。

什么是屏蔽支持?

在最简单的层面上,编写矢量化代码涉及使用 SIMD在一条指令中对Count不同类型的元素执行相同的基本操作。T当需要对所有数据执行相同的操作时,这非常有效。然而,并非所有数据都一定是统一的,有时您需要以不同的方式处理特定的输入。例如,您可能想要对正数和负数执行不​​同的操作。如果用户传入了NaN等等,你可能需要返回不同的结果。在编写常规代码时,您通常会使用分支来处理此问题,这非常有效。然而,在编写矢量化代码时,此类分支会破坏使用 SIMD 指令的能力,因为您必须独立处理每个元素。.NET 在各个位置都利用了这一点,包括新的TensorPrimitivesAPI,它允许我们处理原本不适合完整向量的尾随数据。

典型的解决方案是编写“无分支”代码。最简单的方法之一是计算两个答案,然后使用按位运算来选择正确的答案。您可以将其视为三元条件cond ? result1 : result2。为了在 SIMD 中支持这一点,存在一个名为的 API ConditionalSelect,它接受掩码和两个结果。掩码也是一个向量,但其值通常为AllBitsSetZero。当你有了这个模式,那么实施ConditionalSelect就是有效的(cond & result1) | (~cond & result2)。这分解为从result1相应位 incond所在的位置获取位,否则从(当位 in为 时)1获取相应位。因此,如果您想将所有负值转换为,您将拥有类似于常规代码和矢量化代码的东西。它有点冗长,但也可以提供显着的性能改进。result2cond00(x < 0) ? 0 : xVector128.ConditionalSelect(Vector128.LessThan(x, Vector128.Zero), Vector128.Zero, x)

当硬件首次开始支持 SIMD 时,您必须通过执行 3 条指令来支持这种屏蔽and, nand, or:随着更新的硬件的出现,添加了更多优化的版本,允许您在单个指令中执行此操作,例如blendv在 x86/x64 和bslArm64 上。然后,AVX-512 更进一步,引入了专用硬件支持来表达掩码并在寄存器中跟踪它们(前面提到的k0-k7)。然后,它提供了额外的支持,允许将这种屏蔽作为几乎任何其他操作的一部分来完成。因此,您不必指定vcmpltps; vblendvps; vaddps(比较、掩码,然后添加),而是可以直接将该掩码编码为加法的一部分(从而发出vcmpltps; vaddps)。这使得硬件能够在更小的空间内表示更多的操作,提高代码密度,并更好地利用预期的行为。

值得注意的是,我们在这里并没有直接公开与底层硬件的一对一概念以进行屏蔽。相反,JIT 继续获取并返回规则向量以进行比较结果,并基于此进行相关的模式识别和随后的掩蔽特征的机会性点亮。这使得暴露的 API 表面显着更小(减少了 3000 多个 API),现有代码基本上可以“正常工作”并利用较新的硬件支持,而无需明确的操作,并且对于想要支持 AVX-512 的用户来说,不需要学习新概念或以新方式编写代码。

AVX-512 的实际使用示例怎么样?

AVX-512 可用于加速与 SSE 或基于 AVX 的场景相同的所有场景。确定 .NET 库已在何处使用此加速的简单方法是搜索我们正在调用的位置,这可以使用source.dot.netVector512.IsHardwareAccelerated来完成。

我们加速了以下案例:

.NET 库和通用 .NET 生态系统中还有其他示例,无法一一列出和涵盖。这些包括但不限于颜色转换、图像处理、机器学习、文本转码、JSON 解析、软件渲染、光线追踪、游戏加速等场景。

下一步是什么?

我们计划在有意义的时间和地点继续改进 .NET 中的硬件内在支持。请注意,以下内容是前瞻性和推测性的。该列表并不完整,我们不保证这些功能中的任何一个都会登陆,也不保证它们何时会发布。

我们的长期路线图上的一些项目包括:

  • SVE以及适用于 Arm64 的 SVE2
  • AVX10适用于 x86/x64
  • 允许Vector<T>隐式扩展至 512 位
  • ISimdVector<TSelf, T>允许更好地重用 SIMD 逻辑的接口
  • 帮助鼓励用户使用语义相同的跨平台 API 的分析器(使用x + y代替Sse.Add(x, y)
  • 一个分析器,用于识别可能具有更佳替代方案的模式(value + value代替value * 2Sse.UnpackHigh(value, value)代替Sse.Shuffle(value, value, 0b11_11_10_10)
  • 在各种 .NET API 中对硬件内在函数的其他显式使用
  • 额外的跨平台 API 有助于抽象常见操作
    • 获取掩码中第一个/最后一个匹配的索引
    • 获取掩码中的匹配数
    • 确定是否存在任何匹配项
    • Shuffle允许像或这样的情况出现非确定性行为ConditionalSelect
    • 这些 API 在当今的所有平台上都有明确定义的行为,例如Shuffle将任何超出范围的索引视为将目标元素归零
    • 新的 API,例如ShuffleUnsafe,将允许对超出范围的索引采取不同的行为
    • 对于这种情况,Arm64 将具有相同的行为,而 x64 仅在设置了最高有效位时才具有相同的行为
  • 针对以下情况的附加模式识别
    • 嵌入式掩蔽(AVX512、AVX10、SVE/SVE2)
    • 组合按位运算(vpternlog在 AVX512 上)
    • 有限的 JIT 时间常数折叠机会

如果您希望在 .NET 中使用硬件内在函数,我们鼓励您尝试System.Runtime.Intrinsics 命名空间中提供的 API ,针对您认为缺少或可以改进的功能记录API 建议,并参与我们的活动预览版本可在发布前试用其功能,以便您可以帮助使每个版本比上一个版本更好!

文章作者 | Tanner Gooding(坦纳·古丁 [MSFT])

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值