计算机系统应用教程网站

网站首页 > 技术文章 正文

Java修炼终极指南:102. 介绍 Vector API 的结构和术语

btikc 2024-10-17 08:42:53 技术文章 7 ℃ 0 评论


Vector API 是通过 jdk.incubator.vector 模块(以及具有相同名称的包)映射的。一个 jdk.incubator.vector.Vector 从具有类型和形状的泛型抽象组合开始。一个向量是 Vector<E> 类的实例。

向量的元素类型

Vector<E> 具有一个元素类型(ETYPE),它是 Java 原始类型之一:byte、float、double、short、int 或 long。当我们写 Vector<E> 时,我们说 E 是 ETYPE 的装箱版本(例如,当我们写 Vector<Float> 时,E 是 Float,ETYPE 是 float)。为了方便起见,Java 为每种元素类型声明了一个专门的子类型,如下图所示:


图 5.3 – 专门的向量子类型

即使 E 是装箱类型,也没有装箱和拆箱的开销,因为 Vector<E> 在内部对 ETYPE(即原始类型)进行操作。除了元素类型外,向量还具有一个形状特征。

向量的形状

向量还由一个形状(也称为 VSHAPE)表征,表示向量的位数大小或容量。它可以是 64、128、256 或 512 位。这些值中的每一个都被 VectorShape 枚举包装(例如,S_128_BIT 枚举项表示长度为 128 位的形状),旁边是一个表示平台上支持的最大长度的额外枚举项(S_Max_BIT)。这会在当前运行的 Java 平台上自动确定。

向量的种类

由元素类型和形状表征的向量确定了一个唯一的向量种类,它是 VectorSpecies<E> 的一个固定实例。这个实例被所有具有相同形状和 ETYPE 的向量共享。我们可以将 VectorSpecies<E> 视为用于创建所需元素类型和形状的向量的工厂。例如,我们可以定义一个工厂来创建大小为 512 位的 double 类型向量,如下所示:

static final VectorSpecies<Double> VS = VectorSpecies.of(  
  double.class, VectorShape.S_512_BIT);


如果你只需要一个工厂,用于创建当前平台支持的最大位大小的向量(不考虑元素类型),则依赖 S_Max_BIT:

static final VectorSpecies<Double> VS = VectorSpecies.of(  
  double.class, VectorShape.S_Max_BIT);


如果你只需要当前平台为你的元素类型(这里为 double)选择的最大向量种类,则依赖 ofLargestShape()。这个向量种类由平台选择,并且具有你元素类型可能的最大位数大小(不要与 S_Max_BIT 混淆,后者与元素类型无关):

static final VectorSpecies<Double> VS =    
  VectorSpecies.ofLargestShape(double.class);


或者,也许你需要当前平台为你的元素类型首选的向量种类。这可以通过 ofPreferred() 实现,如下所示:

static final VectorSpecies<Double> VS =  
  VectorSpecies.ofPreferred(double.class);


首选种类是在给定元素类型的当前平台上最方便的方法,当你不想麻烦指定显式形状时。

此外,为了方便起见,每个专门的 Vector(IntVector、FloatVector 等)定义了一组静态字段,以涵盖所有可能的种类。例如,静态字段 DoubleVector.SPECIES_512 可用于表示 512 位大小的 DoubleVector 实例的种类(VectorShape.S_512_BIT):

static final VectorSpecies<Double> VS =  
  DoubleVector.SPECIES_512;


如果你想要最大种类,则依赖 SPECIES_MAX:

static final VectorSpecies<Double> VS =  
  DoubleVector.SPECIES_MAX;


或者,如果你想要首选种类,则依赖 SPECIES_PREFERRED:

static final VectorSpecies<Double> VS =  
  DoubleVector.SPECIES_PREFERRED;


你可以通过 elementType() 和 vectorShape() 方法轻松检查 VectorSpecies 的元素类型和形状,如下所示:

System.out.println("Element type: " + VS.elementType());  
System.out.println("Shape: " + VS.vectorShape());


到目前为止,你知道了如何创建向量种类(向量工厂)。但是,在开始创建向量并在其上应用操作之前,让我们谈谈向量通道。

向量通道

一个 Vector<E> 就像一个由通道组成的固定大小的 Java 数组。通道数由 length() 方法返回,称为 VLENGTH。通道数等于该向量中存储的标量元素的数量。如果你知道元素大小和向量的形状,我们可以计算通道数(shape/元素大小)。你应该得到与 length() 返回的相同结果。元素大小由 elementSize() 返回,形状由 vectorBitSize() 或 vectorShape().vectorBitSize() 返回。例如,一个形状为 256 位、元素类型为 float(在 Java 中为 32 位(4 字节))的向量包含 8 个 float 标量元素,因此它有 8 个通道。以下图描绘了这一说法:


图 5.4 – 计算通道数

基于这个例子,你可以很容易地计算任何其他向量配置的通道数。接下来,让我们看看了解通道为什么重要。

向量操作

在向量上应用操作是我们努力的高潮。通道数估计 SIMD 性能,因为向量操作在通道上操作。单个向量操作将通道作为工作单元。例如,如果我们的向量有 8 个通道,这意味着 SIMD 将一次执行 8 个精益操作。在下图中,你可以看到 SISD 与 SIMD 在这个上下文中的比较:


图 5.5 – SISD vs. SIMD

虽然 SISD 以单个标量作为工作单元,但 SIMD 有 8 个标量(8 个通道),这解释了为什么 SIMD 在性能上显著优于 SISD。因此,Vector<E> 在通道上操作。主要来说,我们有精益操作(如加法、除法、位移等)和跨通道操作,后者将所有通道减少到一个标量(例如,对所有通道求和)。以下图描绘了这些说法:


图 5.6 – 精益操作和跨通道操作

此外,Vector<E> 可以与 VectorMask<E> 一起操作。这是一个布尔值序列,可以由某些向量操作使用来过滤给定输入向量的通道元素的选择和操作。查看以下图(仅当掩码包含 1 时才应用加法操作):


图 5.7 – 带掩码的精益加法

谈到向量操作,你绝对应该查看 Vector 和 VectorOperators 文档。在 Vector 类中,我们有在两个向量之间应用操作的方法。例如,我们有二元操作的方法(如 add()、div()、sub() 和 mul()),比较方法(如 eq()、lt()、compare()),数学操作方法(如 abs())等。此外,在 VectorOperators 中,我们有一堆嵌套类(例如,VectorOperators.Associative)和几个常量,表示如三角函数(SIN、COS 等)、位移操作(LSHL、LSHR)、数学操作(ABS、SQRT、POW)等精益操作。在以下问题中,你将看到这些操作的一部分在实际工作中的应用,但现在,让我们触及最后一个也是必不可少的主题,即创建向量。

创建向量

我们已经知道拥有一个 VectorSpecies 就像是拥有一个用于创建所需元素类型和形状的向量的工厂。现在,让我们看看如何使用这样的工厂有效地创建向量(用标量填充它们)来解决实际问题。假设我们有以下种类(8 个通道的向量,32*8=256):

static final VectorSpecies<Integer> VS256  
  = IntVector.SPECIES_256;


接下来,让我们创建最常见的向量类型。

创建零向量

假设我们需要一个只包含零的向量。一个快速的方法是依赖 zero() 方法,如下所示:

[0, 0, 0, 0, 0, 0, 0, 0]  
Vector<Integer> v = VS256.zero();


这产生了一个具有 8 个 0 的向量。同样的事情也可以从专门的 IntVector 类通过 zero(VectorSpecies<Integer> species) 获得:

IntVector v = IntVector.zero(VS256);


你可以很容易地将这个示例推广到 FloatVector、DoubleVector 等。

创建具有相同原始值的向量

创建一个向量并用一个原始值填充它可以通过 broadcast() 方法快速完成,如下所示:

[5, 5, 5, 5, 5, 5, 5, 5]  
Vector<Integer> v = VS256.broadcast(5);


同样的事情也可以从专门的 IntVector 类通过 broadcast(VectorSpecies<Integer> species, int e) 或 broadcast(VectorSpecies<Integer> species, long e) 获得:

IntVector v = IntVector.broadcast(VS256, 5);


当然,我们也可以使用它来广播一个零向量:

[0, 0, 0, 0, 0, 0, 0, 0]  
Vector<Integer> v = VS256.broadcast(0);  
IntVector v = IntVector.broadcast(VS256, 0);


最后,让我们看看创建向量的最常见用例。

从 Java 数组创建向量

从 Java 数组创建向量是最常见的用例。实际上,我们从 Java 数组开始,并调用一个名为 fromArray() 的方法。

使用VectorSpecies 中的 fromArray()

`fromArray()` 方法在 VectorSpecies 中可用,形式为 `fromArray(Object a, int offset)`。以下是一个从整数数组创建向量的示例:

int[] varr = new int[] {0, 1, 2, 3, 4, 5, 6, 7};   
Vector<Integer> v = VS256.fromArray(varr, 0);


由于 `varr` 的长度(8)等于向量的长度,并且我们从索引 0 开始,结果向量将包含数组中的所有标量。在以下示例中,最后 4 个标量将不会成为结果向量的一部分:

int[] varr = new int[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};  
Vector<Integer> v = VS256.fromArray(varr, 0);


标量 8、9、10 和 11 不在结果向量中。这是另一个使用 offset = 2 的示例:

int[] varr = new int[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};  
Vector<Integer> v = VS256.fromArray(varr, 2);


这次,标量 0、1、10 和 11 不在结果向量中。请注意,Java 数组的长度不应小于向量的长度。例如,以下示例将引发异常:

int[] varr = new int[]{0, 1, 2, 3, 4, 5};  
IntVector v = IntVector.fromArray(VS256, varr, 0);


由于 Java 数组长度为 6(小于 8),这将导致 `java.lang.IndexOutOfBoundsException`。因此,`varr` 的最小接受长度为 8。

使用专门向量的 fromArray()

每个专门的向量类都提供了一组 fromArray() 的变体。例如,IntVector 公开了流行的 `fromArray(VectorSpecies<Integer> species, int[] a, int offset)`,可以直接使用:

int[] varr = new int[] {0, 1, 2, 3, 4, 5, 6, 7};  
IntVector v = IntVector.fromArray(VS256, varr, 0);


如果我们更喜欢 `fromArray(VectorSpecies<Integer> species, int[] a, int offset, VectorMask<Integer> m)` 的变体,则可以通过 VectorMask 过滤 Java 数组中选择的标量。以下是一个示例:

int[] varr = new int[]{0, 1, 2, 3, 4, 5, 6, 7};  
boolean[] bm = new boolean[]{  
  false, false, true, false, false, true, true, false};  
VectorMask m = VectorMask.fromArray(VS256, bm, 0);  
IntVector v = IntVector.fromArray(VS256, varr, 0, m);


基于一对一的匹配,我们可以很容易地观察到结果向量将仅获取标量 2、5 和 6。结果向量将是 `[0, 0, 2, 0, 0, 5, 6, 0]`。

从内存段创建向量

内存段是第 X 章中作为 Foreign Function & Memory API 的一部分详细讨论的主题,但作为一个快速预览,以下是一个通过 `IntVector.fromMemorySegment()` 从内存段创建向量的示例:

IntVector v;  
MemorySegment segment;  
try (MemorySession session = MemorySession.openConfined()) {  
  segment = MemorySegment.allocateNative(32, session);  
  // 设置内存段的值...  
  v = IntVector.fromMemorySegment(VS256, segment, 0, ByteOrder.nativeOrder());  
}


在捆绑的代码中,你可以找到更多示例,用于跨通道边界操作数据,如切片、非切片、洗牌/重新排列、压缩、扩展、转换、强制类型转换和重新解释形状。

在下一个问题中,我们将开始创建利用到目前为止所学知识的完整示例。

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表