前言

矢量化是性能优化的重要技术,也是寄托硬件层面的优化技术。本篇来看下。

概括

一:矢量化支持的问题:

矢量化的System.Runtime.Intrinsics.X86.Sse2.MoveMask函数和矢量化的Vector128.Create().ExtractMostSignificantBits()函数返回的结果是一样的。但是前者只能在支持SSE2的128位矢量化平台上工作,而后者可以在任何支持128位矢量化平台上工作,包括Risc-V,Arm64,WASM等平台。这里以一段代码看下:

private static void Main()
{
Vector128<byte> v = Vector128.Create((byte)123);
while (true)
{
WithIntrinsics(v);
WithVector(v);
break;
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static int WithIntrinsics(Vector128<byte> v) => Sse2.MoveMask(v);
[MethodImpl(MethodImplOptions.NoInlining)]
private static uint WithVector(Vector128<byte> v) => v.ExtractMostSignificantBits();

看下它的ASM代码:

WithIntrinsics:
G_M000_IG01: ;; offset=0000H
55 push rbp
C5F877 vzeroupper
488BEC mov rbp, rsp
48894D10 mov bword ptr [rbp+10H], rcx G_M000_IG02: ;; offset=000BH
488B4510 mov rax, bword ptr [rbp+10H]
C5F91000 vmovupd xmm0, xmmword ptr [rax]
C5F9D7C0 vpmovmskb eax, xmm0 G_M000_IG03: ;; offset=0017H
5D pop rbp
C3 ret WithVector
G_M000_IG01: ;; offset=0000H
55 push rbp
C5F877 vzeroupper
488BEC mov rbp, rsp
48894D10 mov bword ptr [rbp+10H], rcx G_M000_IG02: ;; offset=000BH
488B4510 mov rax, bword ptr [rbp+10H]
C5F91000 vmovupd xmm0, xmmword ptr [rax]
C5F9D7C0 vpmovmskb eax, xmm0 G_M000_IG03: ;; offset=0017H
5D pop rbp
C3 ret

可以看到这两个函数生成的ASM几乎一模一样。

2.矢量化的一个例子

由于以上代码体现的SSE2的局限性,所以需要把一些代码矢量化,以便在任何平台上运行,这里看一个例子。

static bool Contains(ReadOnlySpan<byte> haystack, byte needle)
{
for (int i = 0; i < haystack.Length; i++)
{
if (haystack[i] == needle)
{
return true;
}
} return false;
}

查找元素,找到了然后返回。怎么对这例子进行矢量化呢?首先需要判断你代码运行的硬件是否支持矢量化,可以通过Vector.IsHardwareAccelerated的返回值来判断。其次,传入的变量长度(haystack.length)必须的大于一个向量的长度(Vector.Count,win11加VS2022这个值是32)。那么改造之后如下:

static bool Contains(ReadOnlySpan<byte> haystack, byte needle)
{
if (Vector.IsHardwareAccelerated && haystack.Length >= Vector<byte>.Count)
{
// ...
}
else
{
for (int i = 0; i < haystack.Length; i++)
{
if (haystack[i] == needle)
{
return true;
}
}
}
return false;
}

如果以上if的两个判断均为true的话,那么我们进入矢量化阶段。代码如下:

static unsafe bool Contains(ReadOnlySpan<byte> haystack, byte needle)
{
if (Vector.IsHardwareAccelerated && haystack.Length >= Vector<byte>.Count)//判断当前运行的硬件是否符合矢量化以及变量的长度不能小于矢量化里面一个向量的长度。
{
fixed (byte* haystackPtr = &MemoryMarshal.GetReference(haystack))//获取变量的头指针
{
Vector<byte> target = new Vector<byte>(needle);//向量化需要查找的变量needle
byte* current = haystackPtr;//变量haystack的头指针,以便于后面循环
byte* endMinusOneVector = haystackPtr + haystack.Length - Vector<byte>.Count;//头指针+变量的长度减去一个向量的长度。同头指针current开始到endMinusOneVector在这个里面遍历循环,查找需要查找的变量target也就是向量化的needle,这里为什么要进去Vector<byte>.Count因为向量是从0开始查找的。
do
{
if (Vector.EqualsAny(target, *(Vector<byte>*)current))//判断当前的指针是否与需要查找的变量相等
{
return true;//相等就返回true
} current += Vector<byte>.Count;//不相等指针就位移到下一个向量,继续遍历循环。
}
while (current < endMinusOneVector);//这里判断是否达到循环终点。
}
}
else
{
for (int i = 0; i < haystack.Length; i++)
{
if (haystack[i] == needle)
{
return true;
}
}
}
return false;
}

以上代码几乎完成了90%,但是依然有点点问题。那就是最后一个向量endMinusOneVector没有被查找。所以还需要加上它的查找。最后的点如下,第一个Contains是不矢量化的,第二个Contains_Vector是矢量化之后的。

static bool Contains(ReadOnlySpan<byte> haystack, byte needle)
{
for (int i = 0; i < haystack.Length; i++)
{
if (haystack[i] == needle)
{
return true;
}
} return false;
}
static unsafe bool Contains_Vector(ReadOnlySpan<byte> haystack, byte needle)
{
if (Vector.IsHardwareAccelerated && haystack.Length >= Vector<byte>.Count)
{
fixed (byte* haystackPtr = &MemoryMarshal.GetReference(haystack))
{
Vector<byte> target = new Vector<byte>(needle);
byte* current = haystackPtr;
byte* endMinusOneVector = haystackPtr + haystack.Length - Vector<byte>.Count;
do
{
if (Vector.EqualsAny(target, *(Vector<byte>*)current))
{
return true;
} current += Vector<byte>.Count;
}
while (current < endMinusOneVector); if (Vector.EqualsAny(target, *(Vector<byte>*)endMinusOneVector))
{
return true;
}
}
}
else
{
for (int i = 0; i < haystack.Length; i++)
{
if (haystack[i] == needle)
{
return true;
}
}
} return false;
}

上面的代码几乎是完美的,测试下基准

private byte[] _data = Enumerable.Repeat((byte)123, 999).Append((byte)42).ToArray();//Enumerable.Repeat表示999个123的byte,放在数组,最后又加了一个42数值到数组

[Benchmark(Baseline = true)]
[Arguments((byte)42)]
public bool Find(byte value) => Contains(_data, value); // just the fallback path in its own method [Benchmark]
[Arguments((byte)42)]
public bool FindVectorized(byte value) => Contains_Vectorized(_data, value); // the implementation we just wrote | Method | value | Mean | Error | StdDev | Ratio | Code Size |
|--------------- |------ |----------:|---------:|---------:|------:|----------:|
| Find | 42 | 508.42 ns | 2.336 ns | 2.185 ns | 1.00 | 110 B |
| FindVectorized | 42 | 21.57 ns | 0.342 ns | 0.303 ns | 0.04 | 253 B |

可以看到矢量化之后的性能,进行了夸张的25倍的增长。这段代码几乎完美,但是并不完美。这里是用的1000个元素测试,如果是小于30个元素呢?有两个方法,第一个是退回到没有矢量化的代码也就是Contains函数,第二个是把Vector切换到128位来操作。代码如下,几乎没变更:

static unsafe bool Contains128(ReadOnlySpan<byte> haystack, byte needle)
{
if (Vector128.IsHardwareAccelerated && haystack.Length >= Vector128<byte>.Count)
{
ref byte current = ref MemoryMarshal.GetReference(haystack); Vector128<byte> target = Vector128.Create(needle);
ref byte endMinusOneVector = ref Unsafe.Add(ref current, haystack.Length - Vector128<byte>.Count);
do
{
if (Vector128.EqualsAny(target, Vector128.LoadUnsafe(ref current)))
{
return true;
} current = ref Unsafe.Add(ref current, Vector128<byte>.Count);
}
while (Unsafe.IsAddressLessThan(ref current, ref endMinusOneVector)); if (Vector128.EqualsAny(target, Vector128.LoadUnsafe(ref endMinusOneVector)))
{
return true;
}
}
else
{
for (int i = 0; i < haystack.Length; i++)
{
if (haystack[i] == needle)
{
return true;
}
}
} return false;
}

来进行一个基准测试:

private byte[] _data = Enumerable.Repeat((byte)123, 29).Append((byte)42).ToArray();

[Benchmark(Baseline = true)]
[Arguments((byte)42)]
public bool Find(byte value) => Contains(_data, value); [Benchmark]
[Arguments((byte)42)]
public bool FindVectorized(byte value) => Contains_Vectorized(_data, value); | Method | value | Mean | Error | StdDev | Ratio | Code Size |
|--------------- |------ |----------:|----------:|----------:|------:|----------:|
| Find | 42 | 16.363 ns | 0.1833 ns | 0.1530 ns | 1.00 | 110 B |
| FindVectorized | 42 | 1.799 ns | 0.0320 ns | 0.0299 ns | 0.11 | 191 B |

同样的性能进行了16倍的提速。

结尾

作者:江湖评谈

欢迎关注公众号:jianghupt。文章首发。

.Net7矢量化的性能优化的更多相关文章

  1. 基础才是重中之重~LazyInitializer.EnsureInitialized对属性实现化的性能优化

    回到目录 LazyInitializer.EnsureInitialized是frameworks4.0引入的新东西,实现对属性延时初始化的功能,它作用在System.Threading命名空间下,所 ...

  2. React组件性能优化

    转自:https://segmentfault.com/a/1190000006100489 React: 一个用于构建用户界面的JAVASCRIPT库. React仅仅专注于UI层:它使用虚拟DOM ...

  3. asp.net mvc 性能优化——(1)静态化

    asp.net mvc 性能优化--(1)静态化 在改善页面性能的同时,可能会采用静态化的策略,对于不能实时静态化的内容,则采用缓存.本文主要讨论如何实现cshtml的静态化(实际上还不是完全的htm ...

  4. CSS3与页面布局学习总结(八)——浏览器兼容与前端性能优化

    一.浏览器兼容 1.1.概要 世界上没有任何一个浏览器是一样的,同样的代码在不一样的浏览器上运行就存在兼容性问题.不同浏览器其内核亦不尽相同,相同内核的版本不同,相同版本的内核浏览器品牌不一样,各种运 ...

  5. CSS3与页面布局学习笔记(八)——浏览器兼容性问题与前端性能优化方案

    一.浏览器兼容 1.1.概要 世界上没有任何一个浏览器是一样的,同样的代码在不一样的浏览器上运行就存在兼容性问题.不同浏览器其内核亦不尽相同,相同内核的版本不同,相同版本的内核浏览器品牌不一样,各种运 ...

  6. [转] 擎天哥as3教程系列第二回——性能优化

    所谓性能优化主要是让游戏loading和运行的时候不卡. 一  优化fla导出的swf的体积? 1,  在flash中,舞台上的元件最多,生成的swf越大,库里面有连接名的元件越多,swf越大.当舞台 ...

  7. [转]numpy性能优化

    转自:http://blog.csdn.net/pipisorry/article/details/39087583 http://blog.csdn.net/pipisorry/article/de ...

  8. luajit官方性能优化指南和注解

    luajit是目前最快的脚本语言之一,不过深入使用就很快会发现,要把这个语言用到像宣称那样高性能,并不是那么容易.实际使用的时候往往会发现,刚开始写的一些小test case性能非常好,经常毫秒级就算 ...

  9. 性能优化7--App瘦身

    1. 前言 如果你对App优化比较敏感,那么Apk安装包的大小就一定不会忽视.关于瘦身的原因,大概有以下几个方面: 对于用户来说,在功能差别不大的前提下,更小的Apk大小意味更少的流量消耗,也意味着更 ...

  10. 9 ArcGIS Server 性能优化

    1.系统性能影响因子 地图.服务类型.数据源.客户端技术.CPU.数据结构.网络.内存.存储.部署.架构.服务接口.SDE等. 2.ArcGIS Server性能优化 数据结构与数据源:数据结构(矢量 ...

随机推荐

  1. 基于开源的 ChatGPT Web UI 项目,快速构建属于自己的 ChatGPT 站点

    作为一个技术博主,了不起比较喜欢各种折腾,之前给大家介绍过 ChatGPT 接入微信,钉钉和知识星球(如果没看过的可以翻翻前面的文章),最近再看开源项目的时候,发现了一个 ChatGPT Web UI ...

  2. SpringBoot进阶教程(七十五)数据脱敏

    无论对于什么业务来说,用户数据信息的安全性无疑都是非常重要的.尤其是在数字经济大火背景下,数据的安全性就显得更加重要.数据脱敏可以分为两个部分,一个是DB层面,防止DB数据泄露,暴露用户信息:一个是接 ...

  3. vue 之 computed方法自带缓存踩坑1

    使用场景:ant-vue 穿梭框使用 页面使用computed方法处理组织结构数据,退出页面时,对加载数据做了set null 操作,再次进入页面时,穿梭框只显示数据,无法做左右穿梭功能. 原因:co ...

  4. 四月十七日Java基础知识点

    1.默认构造方法:如果class前面有public修饰符,则默认的构造方法也会是public的.由于系统提供的默认构造方法往往不能满足需求,所以用户可以自己定义类的构造方法来满足需要,一旦用户为该类定 ...

  5. LeeCode 319周赛复盘

    T1: 温度转换 思路:模拟 public double[] convertTemperature(double celsius) { return new double[]{celsius + 27 ...

  6. iOS16新特性 | 灵动岛适配开发与到家业务场景结合的探索实践

    作者:京东零售 姜海 灵动岛是苹果在iPhone 14 Pro和iPhone 14 Pro Max上首次提出的全新UI交互形式,创新性的让虚拟软件和硬件的交互变得更为流畅.当有来电.短信等通知时,灵动 ...

  7. spring事务里面开启线程插入,报错了是否会回滚?

    1.前言 一道非常有意思的面试题目.大概是这样子的,如果在一个事务中,开启线程进行插入更新等操作,如果报错了,事务是否会进行回滚 2.代码 示例1 @RequestMapping("/tes ...

  8. 【Python爬虫(一)】XPath

    解析方式:XPath XPath的基本使用 1 安装lxml库 conda install lxml 下载慢的话可以试一下热点或切换下载源 2 导入etree from lxml import etr ...

  9. Solon2 常用注解之 @Component 与 @ProxyComponent 的区别

    在 Solon 提倡"克制"的原则下,托管组件分为: 普通组件: 主要由内核提供的:@Configuration.@Component.@Controller.@Remoting ...

  10. 容易忽视的细节:Log4j 配置导致的零点接口严重超时

    作者:vivo 互联网服务器团队- Jiang Ye 本文详细的记录了一次0点接口严重超时的问题排查经历.本文以作者自身视角极具代入感的描绘了从问题定位到具体的问题排查过程,并通过根因分析并最终解决问 ...