服务粉丝

我们一直在努力
当前位置:首页 > 财经 >

一文了解 Java 中 SuperWord 技术的最新进展

日期: 来源:ImportNew收集编辑:

(给ImportNew加星标,提高Java技能)


导读


SuperWord 是一种自动向量化的技术,可以将一系列操作合并为单个指令,以提高程序的执行速度。这种技术可以在处理 SIMD(Single Instruction, Multiple Data)指令时非常有效,因为它可以将数据并行化处理。SuperWord 技术可以被编译器自动应用于源代码,也可以通过手动编写汇编代码来使用。它在许多高性能计算应用程序中得到了广泛应用。

背景:SIMD 和自动向量化


现代 CPU 具有各种 SIMD(单输入多数据)向量指令(例如 Intel 的 SSE 和 AVX,ARM 的 NEON 和 SVE)。它们利用向量寄存器,可以保存多个相同类型的值。例如,一个 avx512 寄存器(512 位)可以保存 64 个字节、16 个整数/浮点数或者 8 个长整数/双精度浮点数。因此,它们可以使用单个指令加载、存储、加法、乘法等多个值,但通常与标量(单个)值具有相同的成本(每个周期的指令数和延迟)。

因此,使用 SIMD 指令而不是它们的标量等效指令是非常有好处的,我们可以自己手动实现。

static void test(float[] data) {    for (int j = 0; j < N; j++) {        data[j] = 2 * data[j]; // ------------ 标量    }}static void test_unrolled(float[] data) {    // 循环展开前    for (int j = 0; j < N; j+=4) { // 每次增加4个元素        data[j + 0] = 2 * data[j + 0];        data[j + 1] = 2 * data[j + 1];        data[j + 2] = 2 * data[j + 2];        data[j + 3] = 2 * data[j + 3];    }    // 循环展开后}static void test_vectorized(float[] data) {    // 循环展开前    for (int j = 0; j < N; j+=4) { // 每次增加4个元素        vector_4floats v1 = data[j : j + 4]; // 向量加载        vector_4floats v2 = vector_4floats(2, 2, 2, 2); // 向量常数        vector_4floats v3 = v1 * v2 // 逐元素:4个并行乘法        data[j : j + 4] = v3; // 向量存储    }    // 循环展开后}

我不会详细介绍 Pre-Main-Post 循环(Pre-loop 确保 Main-loop 与内存对齐,Post-loop 执行 Main-loop 后剩余的迭代次数)。

虽然可以手动完成这项工作,但我们不希望这样做。首先,这需要大量的编程工作。其次,向量长度取决于具体的 CPU 特性,因此每个 CPU 都需要有不同的矢量化代码。因此,我们希望编译器中有一个算法来为我们完成这项工作。它应该能够检测 SIMD 并行化可能性的位置,并决定是否有益于性能。通常情况下,主要关注的是循环,因为循环往往需要花费大部分时间。但原则上,可以加速任何具有足够并行性的程序的任何部分。


SuperWord 论文

2000年,Samuel Larsen 和 Saman Amarasinghe 发表了《使用多媒体指令集来利用 SuperWord 并行性》。他们自动将基本块向量化,以便将多个标量操作打包成 SIMD 向量指令。因此,他们的算法可用于任何不包含控制流(无分支/合并,无 If 语句等)的程序部分。唯一的要求是它具有足够的并行性。对于循环,这通常可以通过 循环展开 来实现。这样,可以将多个循环迭代融合到一个基本块中,并利用不同循环迭代之间的潜在并行性。

论文链接:https://groups.csail.mit.edu/cag/slp/SLP-PLDI-2000.pdf

该算法已经在 HotSpot JVM 中 实现。但是,为了实现这一点有几件事情已经被改变。在进行了几次循环展开之后,它仅将算法应用于循环体。

第一个例子

先来看一个简单的 Java 示例(Test.java):

public class Test {    static int N = 100;
public static void main(String[] strArr) { float[] data0 = new float[N]; for (int i = 0; i < 10_000; i++){ init(data0); test(data0); } }
static void test(float[] data) { for (int j = 0; j < N; j++) { data[j] = 2 * data[j]; // <-- this is vectorized } }
static void init(float[] data) { for (int j = 0; j < N; j++) { data[j] = j; } }}

按照以下方式(使用调试版本)执行,应该会看到执行了SuperWord,以及许多其他循环优化。

./java -XX:CompileCommand=printcompilation,Test::test -XX:CompileCommand=compileonly,Test::test -Xbatch -XX:+TraceLoopOpts Test.java

执行命令并查看有关 SuperWord 算法及其步骤的更多详细信息:

./java -XX:CompileCommand=printcompilation,Test::test -XX:CompileCommand=compileonly,Test::test -Xbatch -XX:+TraceSuperWord Test.java

我用这个命令运行了它,并使用 rr 记录了它:

// -XX:LoopMaxUnroll=5 限制展开因子最大为 4。rr ./java -XX:CompileCommand=printcompilation,Test::test -XX:CompileCommand=compileonly,Test::test -Xbatch -XX:+TraceSuperWord -XX:+TraceLoopOpts -XX:LoopMaxUnroll=5 Test.javarr replay
// 在应用 SuperWord 之前设置断点:(rr) b SuperWord::SLP_extract
// 在应用 SuperWord 之前提取循环的所有节点:(rr) p lpt()->_head->dump_bfs(1000,lpt()->_head, "#A+$")
// 运行 SuperWord:(rr) finish
// 在应用 SuperWord 之后提取循环的所有节点:(rr) p cl->dump_bfs(1000,cl, "#A+$")

我可以清晰地看到两个 dump。


可以看到,标量操作被它们的向量等价物所取代:

LoadF -> LoadVectorAddF -> AddVF // 注意: "2 * x" 被替换成了 "x + x"StoreF -> StoreVector

算法的基本思路

在同一个循环示例上查看算法的基本思路,将其展开两次(4x 展开)。

for (int j = 0; j < N; j++) { data[i] = 2 * data[i]; }

下面是简化后的图表,但实质上 C2 图表看起来像这样:


我们看到两个Phi节点:一个持有 i(IV:归纳变量),另一个持有存储状态。我将所有的加载、加法和存储操作与数据数组 data 中的相应偏移量对齐。我们可以看到,这里的所有加载和存储操作都在同一个内存切片上,即 float 数组 data

到目前为止,我们在图中看不到并行性。第 i+2 次迭代的 LoadF 依赖于第 i+1 次迭代的 StoreI。但是我们可以证明它们没有访问内存中相同的位置。因此,我们进行了依赖性分析,得到了一个改进的依赖性图。在其中,我们忽略了不访问内存中相同位置的加载和存储之间的依赖关系。在我们的示例中,我们可以删除循环迭代之间的所有依赖关系。


现在,我们可以看到在原始的 Java 代码中,依赖关系图中的并行性是显而易见的。

接下来,我们需要给出一些定义和一个更精确的问题描述:

  • DAG:有向无环图。
  • 给定:具有基本块(循环体,无控制流)的 DAG
  • 目标:修补 DAG,使标量操作打包成 SIMD 指令。新的 DAG 必须保留旧的 DAG 的行为。
  • 同构:为了将标量操作打包成单个 SIMD 指令,它们必须是相似的(简化:相同的 Opcode 和 velt_type)。
  • 独立性:如果两个操作之间没有路径,则两个操作是独立的。我们只能将独立的操作打包成 SIMD 向量,因为它们是同时执行的,因此一个操作不能以任何方式使用另一个操作的输出。
  • 相邻的内存操作:具有可证明的确切偏移量 sizeof(type) 的两个内存操作。因此,可以将相邻的两个加载或两个存储器打包成单个矢量加载或存储器(我们可以避免使用使一切变得更加复杂的收集和散布操作)。
  • :一个 n 元组 [s1, ..., sn],其中 s1,...,sn 是 独立的 和 同构的
  • :是大小为二的 
  • PackSetPack 的集合。

注意:同构 和 独立性 是 SLP 向量化的必要条件。它们有助于在其图邻域中对各个节点做出“本地”决策。但是它们不足以确保没有循环。我们将稍后详细讨论此问题。

此时,我们将打包相邻的同构的独立的内存操作,这是初始的 PackSet


现在我们将 PackSet 从内存操作扩展到非内存操作。我们通过从我们已经拥有的一对开始,检查这对是否具有匹配的输入对或输出对(即 同构 且 独立性)来实现这一点。


一旦我们找到所有的配对,就可以将这些pairs组合成更大的packs,将它们缝合在一起:[A,..,B]+[B,..,C] -> [A,..B,..C]。



在这一点上,我们需要进行一些合理性检查,并确定矢量化是否确实有利可图。一些 pack 或 PackSet 将被过滤掉。最后,我们可以安排 PackSet,并用矢量节点替换其中的 C2 IR 节点。

让我们看另外两个例子。

在第一个例子中,我们存储 "backward" (i-1),在第二个例子中,我们存储 "forward" (i+1)。在第一个例子中,循环迭代是独立的,而在第二个例子中,我们看到上一次迭代的 StoreF 存储到下一次迭代的 LoadF 加载的位置。这种依赖关系必须得到尊重。现在,我们看到负载不是独立的。

这些是算法和实现中使用的基本思想。现在看起来可能很简单,但复杂性在于细节。
现在我们将更详细地查看算法的每个步骤。

算法步骤 0:循环展开

正如前面讨论的那样,该算法适用于基本块。这可以是一个循环的主体(没有任何控制流)。但它可以是任何基本块,甚至在循环外。

HotSpot JVM 中的实现当前仅将 SuperWord 算法应用于循环。由于它是即时(JIT)编译器,因此必须将花费在优化上的时间集中在最有希望的地方。这往往是循环。

为了确保可以填充 SIMD 向量的完整大小,需要展开至少是 vector_width / sizeof(type) 的倍数的循环。在 JVM 代码中,这针对每个 type 进一步细化。对于 8 字节的 long,需要展开的次数要少于 2 字节的 short。展开更少意味着算法必须处理更少的节点,并更快地处理它们。

SLP 算法还允许手动展开循环(见下面的示例)。

for (int i = 0; i < RANGE; i+=2) {    // 加 2    dataF[i+0] = dataI[i+0] + 0.33f;  // i+0    dataF[i+1] = dataI[i+1] + 0.33f;  // i+1}

算法步骤 1:对齐分析

不幸的是,这篇论文对这部分介绍得很简略,没有提供详细信息。它提到了同一作者的另一篇文章,但我并没有找到。我不得不到 JVM 实现中翻看细节。然而,这部分还有许多 bug 正在修复中,并且其中对齐分析部分是错误的,而其他的例子能提供的信息也很有限。

首先,需要从 C2 节点池中提取 依赖图。这里利用了由 逃逸分析(一种确定哪些内存访问是相关的,哪些是可证明无关的算法)发现的“内存切片”。我们可以忽略所有切片之间的内存依赖关系。在内存切片内,需要确保 C2 图中的 RAW(写后读)、WAR(写后读)和 WAW(写后写)依赖关系得到正确地处理。但是,如果可以证明访问的内存区域不重叠,则可以忽略 RAWWAR 和 WAW 依赖关系。现在,依赖图仅由这样的内存边和 C2 图中的所有数据边(依赖关系 数据->数据数据->memopmemop->数据)组成。

在 JVM 代码中,我们将循环中的内存地址表示如下:

address = base + stride*iv + const [+ invar]

base 与数组引用的基础相关联。iv 引用循环变量 Phistride 是循环迭代之间的字节距离。const 是我们与 base 的常量偏移量。可能存在一个 invar 值,在所有循环迭代中不变。有了这些信息,我们可以尝试证明内存访问是非重叠的,并且我们可以潜在地找到两个内存访问之间的字节偏移,这有助于我们确定相邻的内存操作。

第二个任务是在需要严格对齐的计算机上(当启用 AlignVector HotSpot JVM 标志时)确保严格对齐。许多 CPU 需要矢量内存访问在内存中具有一定的 X 字节对齐(例如 4 字节或 8 字节)。如果执行的矢量内存访问未对 X 字节对齐,则可能会导致性能较差。某些 CPU 还会抛出 SIGBUS 错误。其他 CPU 只是忽略较低的位,这会导致访问到与预期不同的位置(因此导致错误的结果)。

JVM 代码选择一个 packset 作为参考(best)。所有其他 packset 都必须与 best 对齐的偏移量对齐。然后,我们可以调整 Pre-loop 的迭代计数,以使 best 对于内存对齐到 X 字节。由于所有其他 packset 与 best 相对于 X 具有 X 字节对齐,它们也与内存对齐到 X 字节。

注意:当前 JVM 代码选择 X 作为最大 packset 的 vector_width。这是次优选择,应当改进。

算法步骤 2:识别相邻的内存引用(创建一对 PackSet)

应该如何将单个节点打包成 packs?“相邻”的内存访问是一个明显的起点,因为我们希望它们在同一个向量操作中,并且按照正确的顺序。我们采用归纳方法,并用“相邻的独立同构内存操作对”为种子来填充 PackSet

假设:“实际上,几乎每个内存引用最多只与其左侧和右侧的两个引用直接相邻。”

另外,应通过冗余的加载/存储消除来删除重复项(我猜这将是 LoadNode::Identity 和 StoreNode::Identity)。

注意:在 HotSpot JVM 实现中,只在此阶段执行“对齐分析”,并将其混合到同一个循环中。

算法步骤 3:扩展 PackSet(到非内存节点)

从内存 pairs 开始,我们迭代地使用非内存 pairs 扩展 PackSet,直到无法再添加新的 pairs

  • follow_use_defs: 寻找 inputs
  • follow_def_uses: 寻找 outputs

新的 pairs 必须是:同构独立的(可打包成 SIMD 向量指令)。

注1:还有一个成本模型来决定是否可行打包,它试图以最有利的方式扩展。它还应该权衡并行执行操作所带来的收益,与如果输入、输出无法矢量化,则打包
解包的潜在成本。我没有研究过这个模型。也担心它不太有效,因为在过滤阶段,我们会删除那些需要打包输入或解包输出的 packs

注2:我们只应扩展到非内存节点。如果我们还扩展到内存节点,则可能重新引入我们拒绝的负载、存储,例如由于对齐不正确而被拒绝。

算法步骤4 :合并 PackSet(将 pairs 拼接在一起)

现在我们有了所有遵循 use-def 链的 pairs。我们现在迭代地将 packs 拼接在一起。

[s1, .., sj] + [sj, .., sn] -> [s1, .., sj, .., sn]

每个节点现在最多只能在一个“pack”中。大小不是 2 的幂次方的“pack”都将被删除。

如果“pack”超过了硬件允许的大小,则将其分成多个。

细节:到目前为止,我们只显示了“pairs”是“独立”的。我们如何知道现在“packs”是“独立”的?“pair 独立性”仍然存在距离“>= 2”的依赖关系。例如,“data[i+2] = 2 * data[i]”在距离 2 处具有循环依赖关系。

该论文表明,在对齐分析期间确保了“独立性”。它假设不会添加任何跨越“对齐边界”的“pair”。没有提供更多细节。

在 JVM 代码中,目前是这样做的:如果启用了“AlignVector”,则我们已经知道所有向量都与最大向量宽度对齐。对于其余部分,我们假设硬件没有对齐要求。“SuperWord::find_align_to_ref”找到了具有“类似”引用的存储(或加载)的最大组。然后它选择具有最小偏移量的引用(组“mem_ref”)。通过分析表示为“address = base + stride * iv + const [+invar]” 的地址来执行此操作。我们对所有这样的组进行迭代。如果两个组位于同一内存片中,则检查这两个“mem_refs”是否矢量宽度对齐(同一偏移量模矢量宽度)。这确保了内存片内没有“pair”会跨越此“对齐边界”。这本身并不能真正保证独立性。但是,在 JVM 中,我们不实现打包/解包向量节点,也不实现向量排列。这意味着每个“向量通道”保持独立。

但是,如果我们使用“-XX:CompileCommand=option, package.Class::method, Vectorize”标志,则会打开“_do_vector_loop”标志。“IntStream forEach()”方法隐式启用此功能(“vmIntrinsics::_forEachRemaining”)。禁用了“mem_ref”对齐检查。因此,我们可以创建跨越“对齐边界”的“pairs”。在这种情况下,我们无法确定在“组合”之后“packs”是否独立。我们需要在“pack”级别进行额外的“独立性”过滤。

算法步骤 5: 过滤 Packset(已实现,有利可图,循环依赖)

这是一步附加的步骤,论文中没有描述,但在 JVM 中实现了。我们检查所有的 packs 是否满足以下条件:

  • 已实现:我们是否可以生成所需的 SIMD 指令?这取决于硬件。这些检查还会查询 Matcher::match_rule_supported_superword。请参阅 x86.ad 中的 Matcher::match_rule_supported_vector,了解为哪些 SSE 和 AVX CPU 特性实现了哪些矢量指令。

  • 有利可图:由于成本模型已经在 扩展 过程中应用过,因此我们现在只检查 packs 是否可以连接到所有输入和输出。评论中还有一些未完成的任务。

  • 循环依赖pack 级别上的 独立性 并不能保证 packs 之间没有循环依赖。

下面是论文中的一句话:

3.7 调度
在打包之前进行依赖分析可以确保组内语句能够安全地并行执行。但是,执行两个组可能会导致依赖冲突。图6展示了一个例子,其中如果一个组中的语句依赖于另一个组中的语句,则在组之间绘制依赖边缘。只要这个依赖图中没有循环,就可以安排所有组,以便不会发生违规情况。然而,循环表明所选组的集合无效,至少需要消除一个组。虽然实验数据显示这种情况极为罕见,但仍需注意确保正确性。

想法是这样的:在安排日程之前,我们必须确保 packs 是独立的。但是,仅在包级别上的独立性是不足够的,我们还需要确保在安排之前 packs(组)是无环的。

在论文中,他们提供了以下示例:

这个例子当前无法进行矢量化,因为它具有“矢量通道”排列,并且包不匹配。

然而,在这个调查过程中,我发现了下面这个例子。它目前被错误地矢量化了,我已经为此提交了一个 bug:

static void test(int[] dataI1, int[] dataI2, float[] dataF1, float[] dataF2) {    for (int i = 0; i < RANGE/2; i+=2) {        dataF1[i+0] = dataI1[i+0] + 0.33f;            // 1        dataI2[i+1] = (int)(11.0f * dataF2[i+1]);     // 2
dataI2[i+0] = (int)(11.0f * dataF2[i+0]); // 3 dataF1[i+1] = dataI1[i+1] + 0.33f; // 4 }}

注意dataI1 == dataI2 和 dataF1 == dataF2。我只使用了两个引用,这样 C2 就不知道,不会优化负载存储之后的负载。

第 1 行和第 4 行是同构的和独立的。第 2 行和第 3 行也是如此。我们创建了包 [1,4] 和 [2,3],并进行向量化。但是,我们有以下依赖关系:1->3 和 2->4。这在两个包之间创建了循环依赖关系。

结论:在 package 级别上的独立性是必要的条件,但不足够。我们必须明确检查包图中是否引入了任何循环,以便我们可以保证输出 DAG 的确是无环的,并具有相同的语义(相同的效果/结果)。

算法步骤 6:调度(修补图)

我们用向量操作替换包中的节点,并相应地连接它们。我没有对此进行太多调查,因此无法提供更多细节。

实施概述

这是 HotSpot JVM SuperWord 实现的快速概述,附有一些注释:

// 执行分析以确定循环应展开多少次// 基于使用的类型以及能够适合于向量的元素的数量IdealLoopTree::policy_unroll_slp_analysis
// 简化自代码:bool SuperWord::SLP_extract() { // 查找内存切片 // 构建块成员的反后序(rpo)列表 // 我们应该进行向量化吗?检查是否有存储或减少 if (!construct_bb()) {return false;}
// 为每个内存片段构建依赖图: // 对于切片中的每两个 memop,检查它们是否 "!SWPointer::not_equal"(除了 Load -> Load) dependence_graph();
// 当不需要上位位时将较窄的整数类型往回传递。 // 例如:char a,b,c; a = b + c; // AddI 得到 velt_type char。 compute_vector_element_type();
// 初始 Packset:查找相邻的同构、独立的 memop 对 // 执行对齐分析(目前正在进行一些构造/错误修复) find_adjacent_refs();
// 将 PackSet 从 memops 扩展到非 memops 对 // 按照 use->def 和 def->use 进行跟踪 extend_packlist();
// 将配对组合在一起:[a, b] + [b, c] -> [a, b, c] // 如果大于最大向量大小,则将它们拆分成多个 combine_packs();
// 实现?-> 取决于硬件 // 有盈利吗?-> 所有使用和 def 是否都可以向量化? // 循环依赖?-> packs 是否会引入循环依赖? filter_packs();
// 检查具有 packs 的图是否具有循环。如果有 -> 删除所有 packs remove_cycles();
// 改变图:用向量操作替换标量操作 schedule(); output();}

附录:未完成的任务和问题

下面是我在写这篇博客时正在处理的 bug:

  • JDK-8298935:在SuperWord :: find_adjacent_refs中修复create_pack逻辑中的独立性错误
  • JDK-8304042:C2 SuperWord:调度必须删除具有循环依赖关系的包

这是我们可以/应该考虑的更多任务:

  • 代码有很多未完成的任务(例如实现PackNode和ExtractNode)
  • 我最近添加了一些:
    • JDK-8302662:[SuperWord]循环向量化当最后一次迭代的值在循环后使用时(Jatin)
    • JDK-8302673:[SuperWord] MaxReduction和MinReduction应该对int进行向量化(Jatin)
    • JDK-8302652:[SuperWord]减少应该在循环后发生,如果可能的话(Emanuel?)
    • JDK-8303113:[SuperWord]研究是否通过默认启用_do_vector_loop可以创建加速(Emanuel?)
    • JDK-8300865:C2:ProdRed_Double中的产品缩减未进行向量化(Jatin)
  • JDK-8287087:C2:按需执行SLP缩减分析(Roberto)
  • JDK-8255622:将所有向量化测试组合到一个目录中(Vladimir K?)
  • JDK-8260943:重新审查由8076284添加的向量化优化(Vladimir K?一些有问题的代码被硬禁用)
  • 调查:我们在哪里甚至没有开始可以工作的SuperWord?我们在哪里无法在SuperWord期间进行向量化?我们能找到并修复这些情况吗?
  • 我们应该CMove更多,以吸收控制流吗?
  • 分步访问?收集/分散
  • 调查FMA何时/是否/如何工作-仅使用Math.fma
  • 更多的独立性通过更精细的内存片?推测:两个相同类型的数组是单独的对象?

附录:已集成的 SuperWord RFE 列表

Java RFE(Request For Enhancement)是 Java 语言中一个用于提议新特性的机制。

我会在发现更多的 RFE 时添加它们。当然,您也可以在历史记录中自己查找。

  • JDK 8289422:修复并重新启用矢量条件移动
  • JDK 8283091:在SLP中支持不同数据大小之间的类型转换
  • JDK 8231441:AArch64:初始SVE后端支持
  • JDK-8245158:C2:为一些手动展开的循环启用SLP(缺少测试!)
  • JDK-8192846:支持cmov矢量化用于float
  • JDK-8153998:掩码矢量后循环
  • JDK-8151573:范围检查消除的多版本
  • JDK-8149421:矢量化后循环
  • JDK-8139340:增强SuperWord以在Intel AVX cpu上支持矢量条件移动(CMovVD)
  • JDK-8135028:支持矢量化双精度sqrt
  • JDK-8129920:矢量化循环展开(在SuperWord之后再次展开)
  • JDK-8080325:SuperWord循环展开分析
  • JDK-8078563:限制缩减优化(当它有利时)
  • JDK-8076284:提高并行流(forEachRemaining)的矢量化
  • JDK-8074981:整数/ FP标量缩减优化

附录:其他工作

  • 只需要超级字级并行性:使用SLP进行系统控制流向量化(2022)
    • 使用掩码向量指令处理控制流。循环融合/合迭代:每个元素代表一个循环。基本上:通过在控制流合并时使用 CMove/select/blend 以及掩码加载·存储将控制流展平为单个块。


int x;If (condition) { x = v1; } else { x = v2; }
// translates to
c = condition; (true)v1 = …; (c)v2 = …; (!c)x = Phi(c, v1, v2); (true)
// translates to
x = CMove(condition, v1, v2);
// vectorized
c_vec = condition[i : i + 4]; // vector of conditionsv1_vec = v1[i : i + 4]; // compute both values for true / false branchv2_vec = v2[i : i + 4];x_vec = blend(c_vec, v1_vec, v2_vec); // select from true / false branch
  • goSLP - 全局优化超字级并行框架 (2018)
    • 使用整数线性规划进行语句打包。不适用于 JIT。
  • Look-ahead SLP: 在可交换操作存在的情况下进行自动矢量化 (2018)
    • 通过向前看来重新排序可交换操作,以改进同构并实现更多的矢量化。


转自:Emanuel Peter,

链接:eme64.github.io/blog/2023/02/23/SuperWord-Introduction.html



- EOF -

推荐阅读  点击标题可跳转

1、聊聊 Java 20 的新功能

2、从 JDK 8 到 JDK 18,Java 垃圾回收的十次进化

3、同步容器(如Vector)并不是所有操作都线程安全


看完本文有收获?请转发分享给更多人

关注「ImportNew」,提升Java技能

点赞和在看就是最大的支持❤️


相关阅读

  • CVE-2014-1767 分析报告

  • 本文为看雪论坛优秀文章看雪论坛作者ID:Hacksign本文首发于 https://www.debugwar.com/article/NLE-Analyze-CVE-2014-1767, 授权转载到看雪论坛,其他转载请注明出处。一简介该
  • 用ChatGPT,一天回本

  • 大家好,我是郭耀天,人称郭少前面写了一篇五分钟一篇爆文思路,看到文章的人不少,大部分人都只是看看,只是了解,根本不去深入操作,这是大部分人不赚钱原因,以为知道了,其实你只是看到,未
  • Java 20 正式发布,超神了.....

  • 老铁们,JDK 20 / Java 20 正式发布了,这版本号简直超神了。。JDK 20 是一个短期维护版本,将获得六个月的支持。尽管如此,但它仍然可用于生产环境中。根据开发计划,下一个 LTS 版
  • 无人机——新闻报道员的“空中摄影师”

  • 航拍是一种以高科技手段为基础全新的艺术表现手法,是技术美和艺术美 的完美结合。航拍产生的图片和视频,其视点高、视角新颖、独特、冲击力强、表现力强、动感强、节奏快;尤其
  • 64 位系统究竟牛在哪里?

  • 将 脚本之家 设为“星标⭐”第一时间收到文章更新来源 | 程序喵大人作者 | 程序喵大人想必大家都遇到过这样的问题:安装某个软件的时候,出现提示选择 32 位版本还是 64 位版本
  • 第二轮套利结束,交作业

  • 文 | 小鱼 (转载请注明出处)关注公众号:小鱼量化微博/雪球:小鱼量化隔了好长时间,西部利得中证500增强LOF(502000)终于进行了第二轮套利,利润不多,好在没亏。— 1 —套利原理这次小鱼

热门文章

  • “复活”半年后 京东拍拍二手杀入公益事业

  • 京东拍拍二手“复活”半年后,杀入公益事业,试图让企业捐的赠品、家庭闲置品变成实实在在的“爱心”。 把“闲置品”变爱心 6月12日,“益心一益·守护梦想每一步”2018年四

最新文章

  • 一文了解 Java 中 SuperWord 技术的最新进展

  • (给ImportNew加星标,提高Java技能)导读SuperWord 是一种自动向量化的技术,可以将一系列操作合并为单个指令,以提高程序的执行速度。这种技术可以在处理 SIMD(Single Instruction,