当前位置: 首页 > news >正文

[C#] 复数乘法的跨平台SIMD硬件加速向量算法(不仅支持X86的Sse、Avx、Avx512,还支持Arm的AdvSimd)

文章目录

  • 一、简单算法
  • 二、向量算法
    • 2.1 算法思路
      • 2.1.1 复数乘法的数学定义
      • 2.1.2 复数的数据布局
      • 2.1.3 第1步:计算 `(a*c) + (-b*d)i`
      • 2.1.4 第2步:计算 `(a*d) + (b*c)i`
      • 2.1.5 第3步:计算结果合并
    • 2.2 算法实现(UseVectors)
    • 2.3 深入探讨
      • 2.3.1 YGroup2Transpose的深入探讨
      • 2.3.2 HorizontalAdd(水平加法)算法与本算法的区别
    • 2.4 用引用代替指针, 摆脱unsafe关键字(UseVectorsSafeDo)
    • 2.5 循环展开(UseVectorsX2Do)
    • 2.6 512位算法(UseVector512sX2Do)
  • 三、基准测试结果
    • 3.1 X86 架构
      • 3.1.1 更深入的说明
    • 3.2 Arm 架构
  • 附录

将复数乘法改造为SIMD向量算法,是稍微有一些的难度的。首个难点是需要重新调整元素的位置,才能满足复数乘法公式。而“调整元素的位置”与内存中数据布局有关,不同办法的性能不同。还需考虑优化内存访问等细节。

最近知乎有个 帖子 讨论了该话题,且 hez2010 给出了修正后的基于Avx指令集 HorizontalAdd(水平加法)的向量算法。

于是我来说说基于 Shuffle(换位)的向量算法吧。且这些算法是跨平台的,同一份源代码,能在 X86(Sse、Avx等指令集)及Arm(AdvSimd指令集)等架构上运行,且均享有SIMD硬件加速。本文还介绍了512位向量算法,用于对比测试Avx512指令集的硬件加速。

一、简单算法

先回顾一下 .NET 里怎么做复数乘法。

.NET 4.0 开始,提供了 Complex类型。于是不必手工地根据数学知识来编写复数乘法函数,而是可以使用Complex类型来做复数运算,简化了不少。

为了便于进行基准测试,可以将一个数组作为输入参数,随后对各个元素自乘并进行累加。最后返回累加的结果。

public static Complex mul(Complex[] a) {Complex c = 0;for (int i = 0; i < a.Length; i++) {c += a[i] * a[i];}return c;
}

二、向量算法

2.1 算法思路

2.1.1 复数乘法的数学定义

向量类型并未提供复数乘法的方法,于是需要手工地根据数学知识来编写复数乘法函数了。

先来回顾一下复数乘法的数学定义。

(a + bi)*(c + di) = (ac – bd) + (ad + bc)i

在这里插入图片描述

a + bi 是乘法的左侧参数, c + di 是乘法的右侧参数。

数学表达式一般喜欢省略“乘法运算符”。例如上式里,“ac”其实表示“a*c”。为了能使乘法运算更清晰,上式可以写成下面的形式。

(a + bi)*(c + di) = (a*c – b*d) + (a*d + b*c)i

可以看出,复数乘法是由4次实数乘法,以及若干个加减运算所组成。

2.1.2 复数的数据布局

Complex 是一个结构体,其中顺序存放了2个Double类型的成员,分别为“实部”与“虚部”。Double类型是64位的,2个Double就是128位,于是 Complex类型是128位的,即16字节。

由于大多数架构都支持128位向量的SIMD硬件加速。所以 C# 程序在使用Complex类型时,其实已经享受到SIMD硬件加速了。这就是为什么手写的向量算法,有时还比不过 Complex的原因。于是需要更仔细的进行优化。

对于 Complex数组,数据是连续存放的。于是使用向量类型从Complex数组加载数据时,能加载到整数个Complex。

  • 对于128位向量,能存放1个Complex。以Double元素的视角来看,向量中的元素依次是 [a, b]。注:“a”代表复数的“实部”,“b”代表复数的“虚部”。
  • 对于256位向量,能存放2个Complex。以Double元素的视角来看,向量中的元素依次是 [a0, b0, a1, b1]
  • 对于512位向量,能存放4个Complex。以Double元素的视角来看,向量中的元素依次是 [a0, b0, a1, b1, a2, b2, a3, b3]

由于数据都是这样连续存放的,对于上面这种数据布局,本文将它简称为 a + bi 形式。后面将会经常使用这种简称。

例如将实部与虚部交换,变为 b + ai 形式。那么代表的是下面这样的数据布局。

  • 128位向量:[b, a]
  • 256位向量:[b0, a0, b1, a1]
  • 512位向量:[b0, a0, b1, a1, b2, a2, b3, a3]

2.1.3 第1步:计算 (a*c) + (-b*d)i

先来观察一下 向量乘法(Vector.Multiply)对复数数据的运算效果。

假设已经将复数数据分别加载到向量a(a + bi)、向量c(c + di)之中,随后对这2个向量进行向量乘法运算。由于向量乘法依然是对逐个地对相同位置的元素做乘法处理,所以计算结果为 (a*c) + (b*d)i。可以观察到,它正好包含了“复数乘法内部的4个实数乘法”中的2项——a*cb*d

表示这个思路是正确的。但是可以注意到,复数乘法需要使用 -b*d,而上面的 b*d是不带“负号”的。于是我们需要对数据进行进一步的处理,将奇数位置(虚部)的元素做一次“求负”处理。Vector类型里没有单独提供这种处理的方法,于是需要自行编写算法。

对于“求负”处理,性能最高的办法是直接翻转浮点类型的符号位。在IEEE754浮点数标准里,规定了最高位是符号位,该位为0时表示正数,为1时表示负数。于是IEEE754浮点数标准里还能表达 -0.0(负零),它的最高位符号位为1,剩余数据位(阶码s、尾数m)均为0。

二进制的Xor(异或运算)可以使相关二进制进行翻转。于是,将某个浮点数,与 -0.0(负零)执行Xor运算,就能将符号位翻转,这正好是“求负”运算。

由于仅需对奇数位置求负,于是我们可以预先准备一个向量mask,它的偶数位置为 0(正零),奇数位置为 -0.0(负零)。向量c(c + di)与mask进行Xor运算后,结果是 c + (-d)i。随后再与向量a(a + bi)执行乘法,结果是 (a*c) + (-b*d)i。满足所需。

Vector<double> mask = Vectors.CreateRotate(0.0, -0.0);Vector<double> a = *p; // a + bi
var c = a; // c + di
var e = Vector.Multiply(a, Vector.Xor(c, mask)); // (a*c) + (-b*d)i

由于 Vector 的长度不是固定的,手工给mask赋值不太方便。于是这里使用 VectorTraits 库的 Vectors.CreateRotate 方法来简化赋值,它会循环将各个参数依次放置在向量的各个元素里。从而满足了“偶数为正零,奇数为负零”的要求。

Vector<double> a = *p 表示根据指针p,将数据加载到向量a。由于是复数数据,于是a的数学意义为 a + bi,即复数乘法的左侧参数。

为了与前面的“简单算法”保持一致,于是将a复制给了c。此时c的数学意义为 c + di,即复数乘法的右侧参数。

随后根据上面的经验,算出 (a*c) + (-b*d)i,赋值给向量e。

2.1.4 第2步:计算 (a*d) + (b*c)i

根据上一节的经验,使用2个步骤就能计算出 (a*d) + (b*c)i——

  1. 将c的实部与虚部交换,得到 (d) + (c)i,赋值给向量f。
  2. 对 a、f 执行向量乘法(Vector.Multiply),便能得到 (a*d) + (b*c)i

第2步容易实现,但第1步遇到了困难——.NET 的向量方法里,没有合适的方法来做这一工作。

.NET 7.0开始,Vector128等向量类型增加了 Shuffle 方法。用该方法,可以给向量内的元素进行换位。但是直至 .NET 8.0,Shuffle都没有硬件加速,而是使用了标量回退代码。

为了解决 Shuffle 方法没有硬件加速的问题,我开发了VectorTraits 库。它使用了各个架构的shuffle类别的指令,从而使 Shuffle 方法具有硬件加速。具体来说,它分别使用了以下指令。

  • X86: 使用 _mm_shuffle_epi8 等指令.
  • Arm: 使用 vqvtbl1q_u8 指令.
  • Wasm: 使用 i8x16.swizzle 指令.

VectorTraits 不仅为固定大小的向量类型(如 Vector128)提供了Shuffle方法,它还为自动大小的向量类型(Vector)也提供了Shuffle方法。

虽然VectorTraits的Shuffle方法是有硬件加速的,但它不是最佳办法。VectorTraits还提供了YShuffleG2方法,专门用来处理2元素组的换位。它用起来更简单,且大多数时候的性能更高。

YShuffleG2方法通过ShuffleControlG2参数来控制换位模式,例如 ShuffleControlG2.YX 表示将“XY”顺序给换成“YX”顺序,即我们所需的“实部与虚部交换”。

根据这些信息,便能完成第2步的代码了。

var f = Vectors.YShuffleG2(c, ShuffleControlG2.YX); // (d) + (c)i
f = Vector.Multiply(a, f); // (a*d) + (b*c)i

2.1.5 第3步:计算结果合并

经过上面的步骤,已经算出了 (a*c) + (-b*d)i(a*d) + (b*c)i。复数乘法规则是 (a*c – b*d) + (a*d + b*c)i,实数乘法的步骤均处理完了,还剩下加法与数据变换的处理。

向量加法与向量乘法一样,是对逐个地对相同位置的元素做相加处理。于是我们需要将上面的那2组数据,变换为 (a*c) + (a*d)i(-b*d) + (b*c)i,这样才好交给向量加法去处理。

可以观察到,这种变换,就是 2x2矩阵的转置操作。将数据写成矩阵形式,便能清晰看出它是转置操作。

[(a*c) (-b*d)] --> [(a*c) (a*d)]
[(a*d) (b*c)] --> [(-b*d) (b*c)]

VectorTraits库提供了YGroup2Transpose方法,用它便能实现2x2矩阵的转置操作。

var g = Vectors.YGroup2Transpose(e, f, out var h); // g is {(a*c) + (a*d)i}; h is {(-b*d) + (b*c)i}
g += h; // (a*c - b*d) + (a*d + b*c)i

自此,完成了复数乘法的计算。

2.2 算法实现(UseVectors)

有了上面的思路,便能编写出对数组计算向量乘法累加的算法了。源代码如下。

public static unsafe Complex UseVectorsDo(Complex[] numbers) {int blockWidth = Vector<double>.Count / 2; // Complex is double*2int cntBlock = numbers.Length / blockWidth;int cntRem = numbers.Length - (cntBlock * blockWidth);// -- Processs body.Complex result;Vector<double> acc = Vector<double>.Zero;Vector<double> mask = Vectors.CreateRotate(0.0, -0.0);fixed (Complex* pnumbers = numbers) {Vector<double>* p = (Vector<double>*)pnumbers; // Set pointer to numbers[0].Vector<double>* pEnd = p + cntBlock;while (p < pEnd) {// -- Complex multiply: (a + bi)*(c + di) = (ac – bd) + (ad + bc)iVector<double> a = *p; // a + bivar c = a; // c + divar e = Vector.Multiply(a, Vector.Xor(c, mask)); // (a*c) + (-b*d)ivar f = Vectors.YShuffleG2(c, ShuffleControlG2.YX); // (d) + (c)if = Vector.Multiply(a, f); // (a*d) + (b*c)ivar g = Vectors.YGroup2Transpose(e, f, out var h); // g is {(a*c) + (a*d)i}; h is {(-b*d) + (b*c)i}g += h; // (a*c - b*d) + (a*d + b*c)i// Sumacc += g;// Next++p;}// Vector to a Complex.double re = 0.0, im = 0.0;for (int i = 0; i < Vector<double>.Count; i += 2) {re += acc[i];im += acc[i + 1];}result = new Complex(re, im);// -- Processs remainder.Complex* q = (Complex*)pEnd;for (int i = 0; i < cntRem; i++) {result += (*q) * (*q);// Next++q;}}return result;
}

该方法是用指针来处理数据的。代码分为4个部分。

  1. 最前面的部分,是变量初始化。例如计算 cntBlock(有多少个块)、cntRem(剩余部分有多少个复数)等。
  2. “Processs body”是向量处理的主体代码,它构造好mask,并计算好指针地址,随后循环处理主要数据(向量类型的整数倍的数据)。用的就是上面提到的算法。
  3. “Vector to a Complex”是将向量里存放的复数数据,转换为单个复数。
  4. “Processs remainder”是剩余部分的处理。

2.3 深入探讨

2.3.1 YGroup2Transpose的深入探讨

此时,有些读者可能会产生疑问——Vector类型有可能是256位(CPU支持Avx2指令集时),这时向量里不只是2个Double,而是4个Double,YGroup2Transpose还能正常工作吗?

答案是——YGroup2Transpose当然能正常工作。

当 Vector 为256位时,4个Double被分为了2组。既可以看作有2组2x2矩阵,随后分别进行了矩阵转置处理。正好Complex类型是128位的,由2个Double所组成,与2x2矩阵相匹配。

从中可以看出规律——假设向量里可以存放 N*2 个元素(如Double元素),那么 YGroup2Transpose可以对 N组2x2矩阵进行转置。且“N个Complex”做复数乘法时,能使用YGroup2Transpose方法。

对于X86架构,YGroup2Transpose是用Shuffle类别的指令来实现的。对于Arm架构,它是使用 AdvSimd中转置类指令 TRN1TRN2 来实现的,效率很高。

另注:为了使方法名的可读性更高,未来计划将 YGroup2Transpose 改名为 YMatrix2Transpose。初步计划将在VectorTraits库下一个大版本,做这个改名。

2.3.2 HorizontalAdd(水平加法)算法与本算法的区别

HorizontalAdd(水平加法)算法与本算法是非常相似的,仅“第3步:计算结果合并”不同。

先来看看128位向量时的运算情况。HorizontalAdd 会将 相邻2个元素相加,并把结果放在1个元素里,于是2个向量在处理后被合并成了1个向量。

经过前面2步,已经算出了 (a*c) + (-b*d)i(a*d) + (b*c)i。此时进行 HorizontalAdd,便会算出 (a*c – b*d) + (a*d + b*c)i,正好是复数乘法的运算结果!

随后改为用Avx指令集的256位向量来实现,虽然也能正确算出结果,但其实此时 HorizontalAdd 是Avx指令集的特殊版本——它不是对整个向量进行水平加法的,而是按每128位小道(lane)来做水平加法的。

例如Double类型的源数据是 [x0, x1, x2, x3][y0, y1, y2, y3],那么这2种水平加法的结果是不同的——

  • 整256位向量:[x0+x1, x2+x3, y0+y1, y2+y3]
  • 每128位小道:[x0+x1, y0+y1, x2+x3, y2+y3]

第1种方式(整256位向量)比较符合常规思路,但第2种方式( 每128位小道)在很多时候更有用。例如复数类型Complex是128位,要使Complex的运算结果正确,于是需要第2种方式( 每128位小道)的水平加法。幸好Avx指令集,提供的就是第2种方式的水平加法,从而方便了计算。

虽然仅需要1条水平加法指令就能实现“第3步:计算结果合并”,但由于该指令牵涉2个向量的水平操作,所以处理器会稍微多花一些时间。下面摘录了Intel手册对水平加法指令(vhaddpd)的说明,“Latency and Throughput”就是介绍延迟与吞吐率的,可以发现它的值比较高。

__m256d _mm256_hadd_pd (__m256d a, __m256d b)
#include <immintrin.h>
Instruction: vhaddpd ymm, ymm, ymm
CPUID Flags: AVX
Description
Horizontally add adjacent pairs of double-precision (64-bit) floating-point elements in a and b, and pack the results in dst.
Operation
dst[63:0] := a[127:64] + a[63:0]
dst[127:64] := b[127:64] + b[63:0]
dst[191:128] := a[255:192] + a[191:128]
dst[255:192] := b[255:192] + b[191:128]
dst[MAX:256] := 0Latency and Throughput
Architecture	Latency	Throughput (CPI)
Alderlake	5	2
Icelake Intel Core	6	2
Icelake Xeon	6	2
Sapphire Rapids	5	2
Skylake	7	2

对于现代处理器,水平加法算法与本算法的性能,一般是是差不多的。详见“三、基准测试结果”。

而且Avx512系列指令集里,尚未提供512位的水平加法指令,故更推荐使用本算法来处理复数加法。

2.4 用引用代替指针, 摆脱unsafe关键字(UseVectorsSafeDo)

从 C# 7.3开始,可以用引用代替指针, 摆脱unsafe关键字与fixed语句。其实编程思路与指针差不多的,只是一些地方需要换一种写法。具体办法可参考《C# 使用SIMD向量类型加速浮点数组求和运算(4):用引用代替指针, 摆脱unsafe关键字,兼谈Unsafe类的使用》。

改造后的代码如下。

public static Complex UseVectorsSafeDo(Complex[] numbers) {int blockWidth = Vector<double>.Count / 2; // Complex is double*2int cntBlock = numbers.Length / blockWidth;int cntRem = numbers.Length - (cntBlock * blockWidth);// -- Processs body.Vector<double> acc = Vector<double>.Zero;Vector<double> mask = Vectors.CreateRotate(0.0, -0.0);ref Vector<double> p = ref Unsafe.As<Complex, Vector<double>>(ref numbers[0]); // Set pointer to numbers[0].ref Vector<double> pEnd = ref Unsafe.Add(ref p, cntBlock);while (Unsafe.IsAddressLessThan(ref p, ref pEnd)) {// -- Complex multiply: (a + bi)*(c + di) = (ac – bd) + (ad + bc)iVector<double> a = p; // a + bivar c = a; // c + divar e = Vector.Multiply(a, Vector.Xor(c, mask)); // (a*c) + (-b*d)ivar f = Vectors.YShuffleG2(c, ShuffleControlG2.YX); // (d) + (c)if = Vector.Multiply(a, f); // (a*d) + (b*c)ivar g = Vectors.YGroup2Transpose(e, f, out var h); // g is {(a*c) + (a*d)i}; h is {(-b*d) + (b*c)i}g += h; // (a*c - b*d) + (a*d + b*c)i// Sumacc += g;// Nextp = ref Unsafe.Add(ref p, 1);}// Vector to a Complex.double re = 0.0, im = 0.0;for (int i = 0; i < Vector<double>.Count; i += 2) {re += acc[i];im += acc[i + 1];}Complex result = new Complex(re, im);// -- Processs remainder.ref Complex q = ref Unsafe.As<Vector<double>, Complex>(ref pEnd);for (int i = 0; i < cntRem; i++) {result += q * q;// Nextq = ref Unsafe.Add(ref q, 1);}return result;
}

上面的代码,与指针版代码(UseVectorsDo)是等价的。程序的性能,也是差不多的。

2.5 循环展开(UseVectorsX2Do)

对于小循环,循环时的跳转处理也是一笔不小的开销。此时可以使用循环展开的办法,在循环内处理多条数据,从而使循环开销的占比减低。优化了性能。

这里选用了2倍循环展开。它还能带来一个好处,就是能将2元素组换位(YShuffleG2),改为4元素组换位(YShuffleG4X2)。因为一些架构上有“4元素组换位”的专业指令(例如 Avx2.Permute4x64),性能很高。

当初因为自动大小向量有时只有128位,只能存放2个Double,无法满足“4元素组换位”的要求,故谨慎的使用了YShuffleG2方法。而现在有了2倍数据,即使自动大小向量只有128位,也能保证至少共有4个元素,故可以使用 YShuffleG4X2方法。

YShuffleG4X2方法通过ShuffleControlG4参数来控制换位模式,例如 ShuffleControlG4.YXWZ 表示将“XYZW”顺序给换成“YXWZ”顺序,即我们所需的“实部与虚部交换”。(其实, ShuffleControlG4.YXWZ 就是 Avx2.Permute4x64 指令的参数 0b10110001,现在用枚举来描述,代码的可读性更好)

由于现在所用的ShuffleControlG4参数是固定的常数,于是还可以使用 YShuffleG4X2_Const,它的性能一般更好。

根据上面的经验,便能编写出2倍循环展开时的算法了。源代码如下。

public static Complex UseVectorsX2Do(Complex[] numbers) {const int batchWidth = 2; // X2int blockWidth = Vector<double>.Count * batchWidth / 2; // Complex is double*2int cntBlock = numbers.Length / blockWidth;int cntRem = numbers.Length - (cntBlock * blockWidth);// -- Processs body.Vector<double> acc = Vector<double>.Zero;Vector<double> acc1 = Vector<double>.Zero;Vector<double> mask = Vectors.CreateRotate(0.0, -0.0);ref Vector<double> p = ref Unsafe.As<Complex, Vector<double>>(ref numbers[0]); // Set pointer to numbers[0].ref Vector<double> pEnd = ref Unsafe.Add(ref p, cntBlock * batchWidth);while (Unsafe.IsAddressLessThan(ref p, ref pEnd)) {// -- Complex multiply: (a + bi)*(c + di) = (ac – bd) + (ad + bc)iVector<double> a0 = p; // a + bivar a1 = Unsafe.Add(ref p, 1);var c0 = a0; // c + divar c1 = a1;var e0 = Vector.Multiply(a0, Vector.Xor(c0, mask)); // (a*c) + (-b*d)ivar e1 = Vector.Multiply(a1, Vector.Xor(c1, mask));var f0 = Vectors.YShuffleG4X2_Const(c0, c1, ShuffleControlG4.YXWZ, out var f1); // (d) + (c)if0 = Vector.Multiply(a0, f0); // (a*d) + (b*c)if1 = Vector.Multiply(a1, f1);var g0 = Vectors.YGroup2Transpose(e0, f0, out var h0); // g is {(a*c) + (a*d)i}; h is {(-b*d) + (b*c)i}var g1 = Vectors.YGroup2Transpose(e1, f1, out var h1);g0 += h0; // (a*c - b*d) + (a*d + b*c)ig1 += h1;// Sumacc += g0;acc1 += g1;// Nextp = ref Unsafe.Add(ref p, batchWidth);}acc += acc1;// Vector to a Complex.double re = 0.0, im = 0.0;for (int i = 0; i < Vector<double>.Count; i += 2) {re += acc[i];im += acc[i + 1];}Complex result = new Complex(re, im);// -- Processs remainder.ref Complex q = ref Unsafe.As<Vector<double>, Complex>(ref pEnd);for (int i = 0; i < cntRem; i++) {result += q * q;// Nextq = ref Unsafe.Add(ref q, 1);}return result;
}

2.6 512位算法(UseVector512sX2Do)

.NET 8.0 新增了对 X86架构的Avx512系列指令集的支持,且新增了 Vector512类型。

VectorTraits 3.0版已经支持了Avx512系列指令集,于是能够很容易将自动大小向量的算法,改造为512位向量的算法。只需要做文本替换,将“Vector”替换为“Vector512”,便基本完成了改造,顶多有少量报错需修正。而不用学习复杂的Avx512指令集,大大降低了门槛。源代码如下。

#if NET8_0_OR_GREATER[Benchmark]
public void UseVector512sX2() {if (!Vector512s.IsHardwareAccelerated) throw new NotSupportedException("Vector512 does not have hardware acceleration!");_destination = UseVector512sX2Do(_array);
}public static Complex UseVector512sX2Do(Complex[] numbers) {const int batchWidth = 2; // X2int blockWidth = Vector512<double>.Count * batchWidth / 2; // Complex is double*2int cntBlock = numbers.Length / blockWidth;int cntRem = numbers.Length - (cntBlock * blockWidth);// -- Processs body.Vector512<double> acc = Vector512<double>.Zero;Vector512<double> acc1 = Vector512<double>.Zero;Vector512<double> mask = Vector512s.CreateRotate(0.0, -0.0);ref Vector512<double> p = ref Unsafe.As<Complex, Vector512<double>>(ref numbers[0]); // Set pointer to numbers[0].ref Vector512<double> pEnd = ref Unsafe.Add(ref p, cntBlock * batchWidth);while (Unsafe.IsAddressLessThan(ref p, ref pEnd)) {// -- Complex multiply: (a + bi)*(c + di) = (ac – bd) + (ad + bc)iVector512<double> a0 = p; // a + bivar a1 = Unsafe.Add(ref p, 1);var c0 = a0; // c + divar c1 = a1;var e0 = Vector512s.Multiply(a0, Vector512s.Xor(c0, mask)); // (a*c) + (-b*d)ivar e1 = Vector512s.Multiply(a1, Vector512s.Xor(c1, mask));var f0 = Vector512s.YShuffleG4X2_Const(c0, c1, ShuffleControlG4.YXWZ, out var f1); // (d) + (c)if0 = Vector512s.Multiply(a0, f0); // (a*d) + (b*c)if1 = Vector512s.Multiply(a1, f1);var g0 = Vector512s.YGroup2Transpose(e0, f0, out var h0); // g is {(a*c) + (a*d)i}; h is {(-b*d) + (b*c)i}var g1 = Vector512s.YGroup2Transpose(e1, f1, out var h1);g0 += h0; // (a*c - b*d) + (a*d + b*c)ig1 += h1;// Sumacc += g0;acc1 += g1;// Nextp = ref Unsafe.Add(ref p, batchWidth);}acc += acc1;// Vector to a Complex.double re = 0.0, im = 0.0;for (int i = 0; i < Vector512<double>.Count; i += 2) {re += acc[i];im += acc[i + 1];}Complex result = new Complex(re, im);// -- Processs remainder.ref Complex q = ref Unsafe.As<Vector512<double>, Complex>(ref pEnd);for (int i = 0; i < cntRem; i++) {result += q * q;// Nextq = ref Unsafe.Add(ref q, 1);}return result;
}#endif // NET8_0_OR_GREATER

注意,从.NET 8.0才开始支持 Vector512,故需要使用条件编译符号NET8_0_OR_GREATER进行判断。

由于现在有不少处理器尚未支持 Avx512系列指令集,于是需要用if语句判断一下“Vector512s.IsHardwareAccelerated”是否为真。否则就不要执行了。

三、基准测试结果

3.1 X86 架构

X86架构下的基准测试结果如下。

BenchmarkDotNet v0.14.0, Windows 11 (10.0.26100.2605)
AMD Ryzen 7 7840H w/ Radeon 780M Graphics, 1 CPU, 16 logical and 8 physical cores
.NET SDK 9.0.101[Host]     : .NET 8.0.11 (8.0.1124.51707), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMIDefaultJob : .NET 8.0.11 (8.0.1124.51707), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI| Method           | Count | Mean     | Error    | StdDev   | Ratio | Code Size |
|----------------- |------ |---------:|---------:|---------:|------:|----------:|
| CallMul          | 65536 | 44.45 us | 0.329 us | 0.308 us |  1.00 |     128 B |
| CallMul2         | 65536 | 92.50 us | 0.104 us | 0.087 us |  2.08 |        NA |
| UseVectors       | 65536 | 22.48 us | 0.068 us | 0.061 us |  0.51 |        NA |
| UseVectorsSafe   | 65536 | 22.47 us | 0.084 us | 0.070 us |  0.51 |        NA |
| UseVectorsX2     | 65536 | 17.95 us | 0.080 us | 0.075 us |  0.40 |        NA |
| UseVector512sX2  | 65536 | 17.26 us | 0.179 us | 0.167 us |  0.39 |        NA |
| Hez2010Simd_Mul2 | 65536 | 23.69 us | 0.206 us | 0.193 us |  0.53 |        NA |
| Hez2010Simd      | 65536 | 23.01 us | 0.151 us | 0.134 us |  0.52 |     298 B |

方法说明——

  • CallMul: 简单算法。
  • CallMul2: 知乎帖子提出给出的有问题的Avx向量算法。
  • UseVectors: 指针写法的向量算法(2.2 算法实现)。
  • UseVectorsSafe: 引用写法的安全向量算法(2.4 用引用代替指针, 摆脱unsafe关键字)。
  • UseVectorsX2: 2倍循环展开的向量算法(2.5 循环展开)。
  • UseVector512sX2: 2倍循环展开的512位向量算法(2.6 512位算法)。
  • Hez2010Simd_Mul2: hez2010将CallMul2修正后的向量算法。
  • Hez2010Simd: hez2010的安全向量算法。

现在来对比一下各个方法的性能。

  • CallMul2: 44.45/92.50 ≈ 0.4805(倍)。
  • UseVectors: 44.45/22.48 ≈ 1.9773(倍)。
  • UseVectorsSafe: 44.45/22.47 ≈ 1.9782(倍)。
  • UseVectorsX2: 44.45/17.95 ≈ 2.4763(倍)。性能是UseVectorsSafe的 22.47/17.95 ≈ 1.2518(倍)
  • UseVector512sX2: 44.45/17.26 ≈ 2.5753(倍)。性能是UseVectorsSafe的 22.47/17.26 ≈ 1.3019(倍)
  • Hez2010Simd_Mul2: 44.45/23.69 ≈ 1.8763(倍)。
  • Hez2010Simd: 44.45/23.01 ≈ 1.9318(倍)。

首先可以注意到,UseVectors、UseVectorsSafe的性能几乎是一样的。这是因为不论是指针写法,还是引用写法,它们的算法是相同的,所以性能是一样的。而且若观察JIT生成汇编代码,你会发现它们基本是一样的。

其次,可以发现 Hez2010Simd_Mul2、Hez2010Simd、UseVectors、UseVectorsSafe 这4种方法的耗时很接近,都是23us左右。差距很小,可看作测试误差范围内。故可以得出结论,水平加法算法与本算法的性能是几乎是一样的。Avx是256位向量宽度,比Sse的128位向量宽度翻了一倍,理论性能是2倍。现在的测试结果,很接近这个理论值。

再来看 UseVectorsX2,会发现它的性能又有提升,比起普通向量算法(UseVectorsSafe)快了20%左右。看来此时做循环展开,确实有效。

最后来看 UseVector512sX2。Avx512是512位向量宽度,是Sse的128位向量宽度的4倍,理论性能应该是4倍。但是实际测试时,它仅比 UseVectorsX2 稍微快了一点点。

这是因为当前处理器是 Zen4微架构的,它并没有专门的512位运算单元,而是通过2个256位运算单元组合而成的,还不能完全发挥Avx512指令集的潜力。若换成 Zen5Sapphire Rapids等具有专门512位运算单元的微架构的处理器,能获得进一步性能提升。

3.1.1 更深入的说明

仔细观察一下上面的测试结果,会发现本算法(UseVectorsSafe)比起水平加法算法(Hez2010Simd),稍微快一点点。

其实这跟CPU微架构有关。在AMD的处理器上,很多时候是本算法稍微快一点;而在Intel的处理器上,很多时候是水平加法算法稍微快一点。

而且在 Intel 处理器上测试 UseVectorsX2 算法时,有时它的性能与 UseVectorsSafe 相差不大。这也是CPU微架构的差别。

3.2 Arm 架构

同样的源代码可以在 Arm 架构上运行。基准测试结果如下。

BenchmarkDotNet v0.14.0, macOS Sequoia 15.1.1 (24B91) [Darwin 24.1.0]
Apple M2, 1 CPU, 8 logical and 8 physical cores
.NET SDK 8.0.204[Host]     : .NET 8.0.4 (8.0.424.16909), Arm64 RyuJIT AdvSIMDDefaultJob : .NET 8.0.4 (8.0.424.16909), Arm64 RyuJIT AdvSIMD| Method           | Count | Mean     | Error    | StdDev   | Ratio | RatioSD |
|----------------- |------ |---------:|---------:|---------:|------:|--------:|
| CallMul          | 65536 | 56.30 us | 0.051 us | 0.045 us |  1.00 |    0.00 |
| CallMul2         | 65536 |       NA |       NA |       NA |     ? |       ? |
| UseVectors       | 65536 | 56.90 us | 0.468 us | 0.415 us |  1.01 |    0.01 |
| UseVectorsSafe   | 65536 | 56.32 us | 0.019 us | 0.017 us |  1.00 |    0.00 |
| UseVectorsX2     | 65536 | 47.18 us | 0.025 us | 0.024 us |  0.84 |    0.00 |
| UseVector512sX2  | 65536 |       NA |       NA |       NA |     ? |       ? |
| Hez2010Simd_Mul2 | 65536 |       NA |       NA |       NA |     ? |       ? |
| Hez2010Simd      | 65536 |       NA |       NA |       NA |     ? |       ? |

首先可以注意到,CallMul2、Hez2010Simd_Mul2、Hez2010Simd 都无法执行基准测试。这是因为它们都依赖Avx指令集,但现在是Arm架构的处理器,没有Avx指令集,于是报错了。

此时便能体现出本文介绍的算法的优势了——支持跨平台。同一份源代码,能在 X86(Sse、Avx等指令集)及Arm(AdvSimd指令集)等架构上运行,且均享有SIMD硬件加速。

UseVector512sX2是我们主动抛出了异常。虽然同一份源代码可以运行,但由于此时没有硬件加速,测试起来没有意义。故不必测试。

随后来对比一下各个方法的性能。

  • UseVectors: 56.30/56.90 ≈ 0.9895(倍)。
  • UseVectorsSafe: 56.30/56.32 ≈ 0.9996(倍)。
  • UseVectorsX2: 56.30/47.18 ≈ 1.1933(倍)。

UseVectors、UseVectorsSafe的耗时,与CallMul的结果几乎相同。前面提到过,Complex类型内部是已经使用了128位向量加速的。由于Arm架构的AdvSimd指令集是 128位的,于是此时 Vector类型的宽度也是128位的。所以此时用Vector类型实现的算法,理论上跟Complex类型的性能是一样的。

而UseVectorsX2比CallMul快20%左右。这表示此时做循环展开,确实有效。

附录

  • 完整源代码: https://github.com/zyl910/VectorTraits.Sample.Benchmarks/blob/main/VectorTraits.Sample.Benchmarks.Inc/Complexes/ComplexMultiplySumBenchmark.cs
  • YGroup2Transpose 的文档: https://zyl910.github.io/VectorTraits_doc/api/Zyl.VectorTraits.Vectors.YGroup2Transpose.html
  • YShuffleG2 的文档: https://zyl910.github.io/VectorTraits_doc/api/Zyl.VectorTraits.Vectors.YShuffleG2.html
  • YShuffleG4X2_Const 的文档: https://zyl910.github.io/VectorTraits_doc/api/Zyl.VectorTraits.Vectors.YShuffleG4X2_Const.html
  • VectorTraits 的NuGet包: https://www.nuget.org/packages/VectorTraits
  • VectorTraits 的在线文档: https://zyl910.github.io/VectorTraits_doc/
  • VectorTraits 源代码: https://github.com/zyl910/VectorTraits
  • C#simd使用Avx类的代码比普通的for循环代码慢,什么原因呢? - hez2010的回答 - 知乎
  • C# 使用SIMD向量类型加速浮点数组求和运算(4):用引用代替指针, 摆脱unsafe关键字,兼谈Unsafe类的使用
  • 《[C#] 24位图像水平翻转的跨平台SIMD硬件加速向量算法的关键——YShuffleX3Kernel源码解读(如Avx2解决shuffle的跨lane问题、Avx512优化等)》

相关文章:

[C#] 复数乘法的跨平台SIMD硬件加速向量算法(不仅支持X86的Sse、Avx、Avx512,还支持Arm的AdvSimd)

文章目录 一、简单算法二、向量算法2.1 算法思路2.1.1 复数乘法的数学定义2.1.2 复数的数据布局2.1.3 第1步&#xff1a;计算 (a*c) (-b*d)i2.1.4 第2步&#xff1a;计算 (a*d) (b*c)i2.1.5 第3步&#xff1a;计算结果合并 2.2 算法实现&#xff08;UseVectors&#xff09;2.…...

curl 放弃对 Hyper Rust HTTP 后端的支持

curl 放弃了对使用 Rust 编写 Hyper HTTP 后端的支持&#xff0c;因为用户和开发者对此功能的需求很少。 curl 创始人兼核心开发者 Daniel Stenberg 表示&#xff0c;尽管这项工作最初由 ISRG 赞助并且看起来很有希望&#xff0c;但 Hyper 支持多年来一直处于实验阶段&#xf…...

RK3506开发板:智能硬件领域的新选择,带来卓越性能与低功耗

在现代智能硬件开发中&#xff0c;选择一款性能稳定、功耗低的开发板是确保产品成功的关键。Rockchip最新推出的RK3506芯片&#xff0c;凭借其卓越的能效比、多功能扩展性和优秀的实时性能&#xff0c;已经成为智能家电、工业控制、手持终端等领域的热门选择。而基于RK3506的Ar…...

RBAC权限控制

1、Spring Security 是一个功能强大的Java安全框架&#xff0c;它提供了全面的安全认证和授权的支持。 2 SpringSecurity配置类&#xff08;源码逐行解析&#xff09; Spring Security的配置类是实现安全控制的核心部分 开启Spring Security各种功能&#xff0c;以确保Web应…...

Linux高并发服务器开发 第六天(rwx 对于目录和文件的区别 gcc编译器 动态库静态库)

目录 1.rwx 对于目录和文件的区别 2.gcc 编译器 2.1编译过程 2.2gcc 的其他参数 3.动态库和静态库 3.1函数库 1.rwx 对于目录和文件的区别 r 文件的内容可以被查看。支持cat、more、head...vim &#xff1b;目录的内容可以被查看。ls、tree …...

如何使用远程控制工具管理你的计算机系统

在现代工作环境中&#xff0c;远程控制技术越来越重要&#xff0c;尤其是对于系统管理员、技术支持人员以及需要远程工作的人来说。远程控制不仅仅是便捷&#xff0c;更是提高工作效率、快速解决问题的重要手段。今天&#xff0c;我们将讨论一些常见的远程控制工具&#xff0c;…...

在K8S中,CNI有什么作用?

在kubernetes中&#xff0c;Container Network Interface(CNI)起着至关重要的作用&#xff0c;主要解决了容器网络配置及通信的问题&#xff0c;确保了Pod间网络连通性及其外部世界的通信。CNI的具体作用包括但不限于以下几个方面。 1. 网络配置自动化&#xff1a; 当kuberne…...

C语言性能优化:从基础到高级的全面指南

引言 C 语言以其高效、灵活和功能强大而著称&#xff0c;被广泛应用于系统编程、嵌入式开发、游戏开发等领域。然而&#xff0c;要写出高性能的 C 语言代码&#xff0c;需要对 C 语言的特性和底层硬件有深入的了解。本文将详细介绍 C 语言性能优化的背后技术&#xff0c;并通过…...

JS中Symbol (符号)数据类型详解和应用场景

JavaScript中Symbol数据类型详解 Symbol是ES6引入的一种原始数据类型&#xff0c;表示唯一的标识符。它是通过Symbol()函数生成的&#xff0c;每次调用都会返回一个独一无二的值。Symbol值的主要用途是为对象的属性提供唯一标识&#xff0c;以避免属性名冲突。 特点 唯一性 每…...

Go gin框架(详细版)

目录 0. 为什么会有Go 1. 环境搭建 2. 单-请求&&返回-样例 3. RESTful API 3.1 首先什么是RESTful API 3.2 Gin框架支持RESTful API的开发 4. 返回前端代码 go.main index.html 5. 添加静态文件 main.go?改动的地方 index.html?改动的地方 style.css?改…...

Linux系统 —— 进程控制系列 - 进程的等待:wait 与 waitpid

目录 1. 进程的等待 1.1 为什么需要等待 2. 进程等待的方法 1. wait 2. waitpid 3. 获取子进程status 4. 阻塞与非阻塞等待 续接前文&#xff1a; Linux系统 —— 进程控制系列 - 进程的创建与终止 &#xff1a;fork与exit-CSDN博客https://blog.csdn.net/hedhjd/artic…...

blender中合并的模型,在threejs中显示多个mesh;blender多材质烘培成一个材质

描述&#xff1a;在blender中合并的模型导出为glb&#xff0c;在threejs中导入仍显示多个mesh&#xff0c;并不是统一的整体&#xff0c;导致需要整体高亮或者使用DragControls等不能统一控制。 原因&#xff1a;模型有多个材质&#xff0c;在blender中合并的时候&#xff0c;…...

探索多模态大语言模型(MLLMs)的推理能力

探索多模态大语言模型&#xff08;MLLMs&#xff09;的推理能力 Multimodal Large Language Models (MLLMs) flyfish 原文&#xff1a;Exploring the Reasoning Abilities of Multimodal Large Language Models (MLLMs): A Comprehensive Survey on Emerging Trends in Mult…...

[Wireshark] 使用Wireshark抓包https数据包并显示为明文、配置SSLKEYLOGFILE变量(附下载链接)

wireshark 下载链接&#xff1a;https://pan.quark.cn/s/eab7f1e963be 提取码&#xff1a;rRAg 链接失效&#xff08;可能会被官方和谐&#xff09;可评论或私信我重发 chrome与firefox在访问https网站的时候会将密钥写入这个环境变量SSLKEYLOGFILE中&#xff0c;在wireshark…...

单片机实物成品-007 汽车防盗系统(代码+硬件+论文)

汽车尾气监测系统&#xff08;温度震动传感器 红外热释电GPS三个指示灯蜂鸣器正常模式防盗模式wifi传输控制送APP源码 &#xff09; 把该系统划分为两个不同设计主体&#xff0c;一方面为硬件控制主体&#xff0c;通过C语言来编码实现&#xff0c;以STM32开发板为核心控制器&a…...

redis开发与运维-redis0401-补充-redis流水线与Jedis执行流水线

文章目录 【README】【1】redis流水线Pipeline【1.1】redis流水线概念【1.2】redis流水线性能测试【1.2.1】使用流水线与未使用流水线的性能对比【1.2.2】使用流水线与redis原生批量命令的性能对比【1.2.3】流水线缺点 【1.3】Jedis客户端执行流水线【1.3.1】Jedis客户端执行流…...

windows系统下使用cd命令切换到D盘的方法

windows系统下使用cd命令切换到D盘的方法 系统环境配置 win10系统原装C盘后期自己安装的硬盘D盘 python3.8安装在D盘中 问题说明 winR打开终端&#xff0c;使用 cd d:命令&#xff0c;无法将当前目录切换到D盘 解决方法 方法一&#xff1a;使用下面这条命令 cd /d d:运…...

word参考文献第二行缩进对齐

刚添加完参考文献的格式是这样&#xff1a; ”段落“—>缩进修改、取消孤行控制 就可以变成...

Springboot关于格式化记录

日期格式化 返回前端日期需要格式化 <dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-databind</artifactId><version>2.9.2</version> </dependency>JsonFormat(pattern "yyyy-MM-dd…...

1.business english--build rapport

build rapport with someone 建立融洽关系 Salespeople often try to build rapport with customers to boost sales. user a variety of appropriate questions. answer the question according to your experience. Do you know how to make a good connection with others…...

发明专利与实用新型专利申请过程及自助与代办方式对比

申请专利&#xff08;发明专利、实用新型专利、外观设计专利&#xff09;有两种方式&#xff1a;1、自己直接向国家知识产权局申请。2、通过专利代办处申请。以下是对这两种专利类型&#xff08;发明专利、实用新型专利&#xff09;申请过程及两种申请方式的详细介绍和对比,参考…...

设计模式-创建型-工厂方法模式

什么是工厂方法模式&#xff1f; 工厂方法模式&#xff08;Factory Method Pattern&#xff09;是 创建型设计模式之一&#xff0c;目的是通过定义一个用于创建对象的接口&#xff0c;让子类决定实例化哪个类。简而言之&#xff0c;工厂方法模式通过延迟对象的创建过程到子类来…...

如何判断一个学术论文是否具有真正的科研价值?ChatGPT如何提供帮助?

目录 1.创新性与学术贡献的超级加分✔ 2.科研过程中的各个环节—从0到1✔ 3.创新性与理论深度的完美结合✔ 4.论证与写作的清晰性✔ 5.数据整理和文献回顾——效率与精准并存✔ 6.创新性要求辅助✔ 总结 宝子们&#xff0c;学术论文写作的旅程是不是感觉像是走进了迷雾森…...

Linux驱动开发--字符设备驱动开发

一、概述 字符设备是 Linux 驱动中最基本的一类设备驱动,字符设备就是一个一个字节,按照字节 流进行读写操作的设备,读写数据是分先后顺序的。比如我们最常见的点灯、按键、 IIC、 SPI, LCD 等等都是字符设备,这些设备的驱动就叫做字符设备驱动。 Linux 应用程序对驱动程…...

Java 网络原理 ①-IO多路复用 || 自定义协议 || XML || JSON

这里是Themberfue 在学习完简单的网络编程后&#xff0c;我们将更加深入网络的学习——HTTP协议、TCP协议、UDP协议、IP协议........... IO多路复用 ✨在上一节基于 TCP 协议 编写应用层代码时&#xff0c;我们通过一个线程处理连接的申请&#xff0c;随后通过多线程或者线程…...

828华为云征文|使用sysbench对Flexus X实例对mysql进行性能测评

目录 一、Flexus X实例概述 1.1?Flexus X实例 1.2?在mysql方面的优势 二、在服务器上安装MySQL 2.1 在宝塔上安装docker 2.2 使用宝塔安装mysql 2.3 准备测试数据库和数据库表 三、安装sysbench并进行性能测试 3.1 使用yum命令sysbench 3.2?运行?sysbench 并进行…...

数据结构:堆

目录 1.堆的概念 2.堆的结构 3.堆的初始化 4.堆的销毁 5.堆的插入 6.堆的删除 7.判断堆是否为空 1.堆的概念 堆的性质&#xff1a; 堆中某个结点的值总是不大于或不小于其父结点的值&#xff1b; 堆总是一棵完全二叉树。 以下堆的结构默认大堆 &#xff1a; 2.堆的结…...

洪水灾害多智能体分布式模拟示例代码

1. 环境定义&#xff1a;支持灾害动态、地理数据和分布式架构 import numpy as np import random import matplotlib.pyplot as plt# 新疆主要城市及邻接关系 XINJIANG_CITIES {Urumqi: [Changji, Shihezi],Changji: [Urumqi, Shihezi, Turpan],Shihezi: [Urumqi, Changji, K…...

基于 Ragflow 搭建知识库-初步实践

基于 Ragflow 搭建知识库-初步实践 一、简介 Ragflow 是一个强大的工具&#xff0c;可用于构建知识库&#xff0c;实现高效的知识检索和查询功能。本文介绍如何利用 Ragflow 搭建知识库&#xff0c;包括环境准备、安装步骤、配置过程以及基本使用方法。 二、环境准备 硬件要…...

Selenium实践总结

1.使用显示等待而不是隐式等待 隐式等待可能会导致不可预测的测试行为&#xff0c;尤其是在动态 Web 应用程序中。显式等待&#xff0c;它允许您 等待特定条件发生后再继续测试&#xff0c;这种方法提供了更多的控制和可靠性。 WebDriverWait wait new WebDriverWait(drive…...

华为麦芒5(安卓6)termux记录 使用ddns-go,alist

下载0.119bate1 安卓5和6版本,不能换源,其他源似乎都用不了,如果root可以直接用面具模块 https://github.com/termux/termux-app/releases/download/v0.119.0-beta.1/termux-app_v0.119.0-beta.1apt-android-5-github-debug_arm64-v8a.apk 安装ssh(非必要) pkg install open…...

Springboot jar包加密加固并进行机器绑定

获取机器码&#xff0c;通过classfinal-fatjar-1.2.1.jar来获取机器码 命令&#xff1a;java -jar classfinal-fatjar-1.2.1.jar -C 对springboot打包的jar进行加密功能 java -jar classfinal-fatjar-1.2.1.jar -file lakers-ljxny-3.0.0.jar -packages com.lygmanager.laker…...

【Microi吾码】开源力量赋能低代码创新,重塑软件开发生态格局

我的个人主页 文章专栏&#xff1a;Microi吾码 一、引言 在当今数字化浪潮汹涌澎湃的时代&#xff0c;软件开发的需求呈现出爆发式增长。企业为了在激烈的市场竞争中脱颖而出&#xff0c;不断寻求创新的解决方案以加速数字化转型。传统的软件开发方式往往面临着开发周期长、技…...

系统思考—冰山模型

“卓越不是因机遇而生&#xff0c;而是智慧的选择与用心的承诺。”—— 亚里士多德 卓越&#xff0c;从来不是一次性行为&#xff0c;而是一种习惯。正如我们在日常辅导中常提醒自己&#xff1a;行为的背后&#xff0c;隐藏着选择的逻辑&#xff0c;而选择的根源&#xff0c;源…...

Java读取InfluxDB数据库的方法

本文介绍基于Java语言&#xff0c;读取InfluxDB数据库的方法&#xff0c;包括读取InfluxDB的所有数据库&#xff0c;以及指定数据库中的measurement、field、tag等。 首先&#xff0c;创建一个Java项目&#xff0c;用于撰写代码。如果大家是基于IDEA来创建项目&#xff0c;则可…...

【mybatis-plus问题集锦系列】在mybatisplus中无法autowired的原因排查及解决

mybatisplus简化了我们做数据操作&#xff0c;大大提升了我们的开发速度&#xff0c;但是今天在做测试的时候&#xff0c;突然报了这么个错误&#xff0c;排查好久才找到解决方案&#xff0c;特此记录下 问题复现 这里的测试方法报错&#xff0c;通过不了测试 org.springf…...

python中Windows系统使用 pywin32 来复制图像到剪贴板,并使用 Selenium 模拟 Ctrl+V 操作

步骤 1&#xff1a;安装必要的库 首先&#xff0c;安装 pywin32 和 selenium&#xff1a; pip install pywin32 selenium 如果使用的是 macOS&#xff0c;可以安装 pyobjc&#xff1a; pip install pyobjc 步骤 2&#xff1a;使用 pywin32 复制图像到剪贴板 在 Windows 系统中…...

uniapp——微信小程序,从客户端会话选择文件

微信小程序选择文件 文章目录 微信小程序选择文件效果图选择文件返回数据格式 API文档&#xff1a; chooseMessageFile 微信小程序读取文件&#xff0c;请查看 效果图 选择文件 /*** description 从客户端会话选择文件* returns {String} 文件路径*/ const chooseFile () &g…...

点亮核心板小灯 STM32U575

将核心板上的运行状态指示灯点亮 任务分析 灯如何点亮 如何看开发板原理图 开发板上的灯硬件组成 原理图 原理图&#xff08;Schematic Diagram&#xff09;&#xff0c;也称为电路图或电气图&#xff0c;是一种图形表示方法&#xff0c;用于展示电子系统或电路的工作原理和…...

“图书馆服务自动化”:基于SSM框架的图书借阅系统开发

3.1系统的需求分析 需求分析阶段是设计系统功能模块的总方向&#xff0c;可以这样来说&#xff0c;系统的整个的开发流程以及设计进度&#xff0c;基本上都是以需求分析为基本依据的[10]。需求分析阶段可以确定系统的基本功能设计&#xff0c;以及在最后的系统验收阶段&#xf…...

顶顶通呼叫中心中间件的三种呼叫方式(mod_cti基于FreeSWITCH)

顶顶通呼叫中心共有三种呼叫方式&#xff1a; 手拨呼叫点击呼叫自动外呼 联系我们 有意向了解呼叫中心中间件的用户&#xff0c;可以点击该链接添加工作人员&#xff1a;https://blog.csdn.net/H4_9Y/article/details/136148229 手拨呼叫 手拨呼叫属于常规的呼叫方式&…...

HCIA笔记9--NAT、ACL与链路聚合

1. ACL ACL: 访问控制列表, Access Control List。 通过定义规则来允许或拒绝流量的通过。 1.1 ACL分类 1.2 配置实例 如图所示&#xff0c;对R2的访问只允许192.168.1.0/24网段。 我们可以配置基本acl来限制 acl 2000 acl number 2000 rule 5 permit source 192.168.1.0 0…...

【笔记】在虚拟机中通过apache2给一个主机上配置多个web服务器

&#xff08;配置出来的web服务器又叫虚拟主机……&#xff09; 下载apache2 sudo apt update sudo apt install apache2 &#xff08;一&#xff09;ip相同 web端口不同的web服务器 进入 /var/www/html 创建站点一和站点二的目录文件&#xff08;目录文件名自定义哈&#x…...

“校园健康数据管理”:疫情管控系统的信息收集与分析

3.1可行性分析 通过对系统实行的目的初步调查和分析&#xff0c;提出可行性方案并对其一一进行论证。我们在这里主要从技术可行性、经济可行性、操作可行性等方面进行分析。 3.1.1 技术可行性 1.硬件可行性分析 校园疫情管控系统系统的硬件要求方面不存在特殊的要求&#xff0c…...

MySQL 中存储金额数据一般使用什么数据类型

在 MySQL 中存储金额数据时&#xff0c;应该谨慎选择数据类型&#xff0c;以确保数据的精度和安全性。以下是几种常用的数据类型及其适用性&#xff1a; DECIMAL 类型&#xff1a; 描述&#xff1a;DECIMAL 类型是专门为存储精确的小数而设计的。它可以指定小数点前后的数字位数…...

使用 .NET 6 或 .NET 8 上传大文件

如果您正在使用 .NET 6&#xff0c;并且它拒绝上传大文件&#xff0c;那么本文适合您。 我分享了一些处理大文件时需要牢记的建议&#xff0c;以及如何根据我们的需求配置我们的服务&#xff0c;并提供无限制的服务。 本文与 https://blog.csdn.net/hefeng_aspnet/arti…...

帝国cms电脑pc站url跳转到手机站url的方法

本文讲解一下帝国cms电脑网站跳转到手机动态网站和手机静态网站的方法,笔者以古诗词网 www.gushichi.com为例&#xff0c;为大家介绍操作步骤。方法一&#xff1a;帝国pc站跳转到手机静态站 1、假设我们有帝国cms 电脑网站www.XXX.com&#xff0c;手机网站m.XXX.com &#xf…...

D类音频应用EMI管理

1、前言 对于EMI&#xff0c;首先需要理解天线。频率和波长之间的关系&#xff0c;如下图所示。   作为有效天线所需的最短长度是λ/4。在空气中&#xff0c;介电常数是1&#xff0c;但是在FR4或玻璃环氧PCB的情况下&#xff0c;介电常数大约4.8。这种效应会导致信号在FR4材…...

【行业发展报告】2024大数据与智能化行业发展浅析

回首 2024&#xff0c;大数据智能化浪潮汹涌。海量数据宛如繁星&#xff0c;在智能算法的苍穹下汇聚、碰撞&#xff0c;释放出洞察市场与用户的强大能量&#xff0c;精准勾勒出商业新航线。我们精心雕琢技术架构&#xff0c;从数据存储的坚固基石到处理分析的高效引擎&#xff…...

闲谭Scala(3)--使用IDEA开发Scala

1. 背景 广阔天地、大有作为的青年&#xff0c;怎么可能仅仅满足于命令行。 高端大气集成开发环境IDEA必须顶上&#xff0c;提高学习、工作效率。 开整。 2. 步骤 2.1 创建工程 打开IDEA&#xff0c;依次File-New-Project…&#xff0c;不好意思我的是中文版&#xff1a;…...