Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >厉害了!Ziglang首次落地高性能计算场景

厉害了!Ziglang首次落地高性能计算场景

作者头像
zouyee
发布于 2024-12-05 05:58:17
发布于 2024-12-05 05:58:17
89500
代码可运行
举报
文章被收录于专栏:Kubernetes GOKubernetes GO
运行总次数:0
代码可运行

Zig 编程语言以性能和安全性为核心设计目标,近年来逐渐受到欢迎。由于 Zig 基于 LLVM 构建,因此能够利用该生态系统的诸多优势,包括访问丰富的支持后端,这使得 Zig 在高性能工作负载方向具备显著潜力。然而,Zig 尚未在高性能计算(HPC)领域赢得广泛关注,其中一个原因是其缺乏基于预编译指令(pragma)实现共享内存并行计算的能力。

译|zouyee

为了帮助读者深入了解Kubernetes在各种应用场景下所面临的挑战和解决方案,以及如何进行性能优化。我们推出了<<Kubernetes经典案例30篇>>,欢迎订阅。

Zig 编程语言以性能和安全性为核心设计目标,近年来逐渐受到欢迎。由于 Zig 基于 LLVM 构建,因此能够利用该生态系统的诸多优势,包括访问丰富的支持后端,这使得 Zig 在高性能工作负载方向具备显著潜力。然而,Zig 尚未在高性能计算(HPC)领域赢得广泛关注,其中一个原因是其缺乏基于预编译指令(pragma)实现共享内存并行计算的能力。

本文描述了如何通过优化 Zig 编译器来支持 OpenMP 循环指令,并使用 NASA 的并行基准测试套件(NPB)来测试其性能表现。 Zig 与 OpenMP 的集成不仅在扩展性上可与 Fortran 和 C 的 NPB 参考实现相媲美,同时在某些场景下,Zig 的性能相较Fortran来说,提升幅度多大1.25倍。

Index Terms: Zig, OpenMP, LLVM, High Performance Computing, NAS Parallel Benchmark suite

目前Zig 社区在推动移除LLVM、LCD以及Clang代码库依赖,详见https://github.com/ziglang/zig/issues/16270

介绍

随着高性能计算(HPC)领域迈入Exascale时代,面临的一个关键问题是如何选择用于超级计算机日益复杂场景的编程语言。目前 Fortran 和 C 等传统语言仍然占据着HPC领域的绝大多数份额。

Zig 是一种系统编程语言,由 Andrew Kelly 于 2016 年创建,设计目标是追求快速和安全。近年来,围绕这一语言逐渐形成充满活力的生态。目前Zig 在 HPC 领域已有一些边缘应用,例如 Cerebras 的 CSL 编程技术(用于其 Wafer Scale Engine,WSE)的开发是基于 Zig ,但总体而言,Zig 在 HPC 中尚未被广泛采用。其未被广泛采用的原因之一是语言本身缺乏对常见 HPC 编程技术的支持。尽管 Zig 的 C 互操作性特性十分出色,使得使用 MPI 相对简单,但其缺少对于 HPC 中普遍存在的基于编译指令(pragma)的共享内存并行编程的支持,因此需要对Zig编译器进行修改。

本文探索了一种通过为Zig 编译器添加OpenMP 循环指令的支持,实现基于编译指令的共享内存并行特性。通过调用 LLVM 的 OpenMP 运行时库,我们描述了支持 OpenMP 循环指令所需的修改,并比较了 NASA 的 NAS 并行基准测试套件(NPB)中内核在 C、Fortran 和 Zig 之间的性能表现。本文的结构如下:第二部分描述了该研究的背景;第三部分探讨了为支持 OpenMP 循环指令对 Zig 的增强;第四部分重点介绍了评估方法,包括将 Zig 与 C 和 Fortran 集成的探索;第五部分分析了 Zig 与 OpenMP 在这些基准中的性能表现;最后,第六部分总结了本文的研究,并讨论了未来工作。

本文的主要贡献包括:

1. 描述如何与 Zig 编译器中集成 OpenMP 循环指令。

2. 首次探索 Zig 与 Fortran 代码的集成方式,为将 Zig 应用于更大规模的传统代码库提供了可能。

3. 对 Zig、Fortran 和 C 在三项 HPC 基准测试中的进行性能对比,其中涵盖线程化时的加速比以及运行时性能。最终证明 Zig 表现良好,是 HPC 领域的可行选择

背景与相关工作

LLVM 是一套工具和库,部分用于生成和操作LLVM-IR[1]。LLVM 上构建了丰富的后端,可以从这种内部表示生成机器代码,并支持多种硬件,包括 CPU、GPU 和 FPGA。除了在主代码库中提供针对 C 和 Fortran 的前端工具(如 Clang 和 Flang)外,LLVM 还被许多流行的编程语言使用,如 Swift[2]、Rust[3]和 Zig[4]。

LLVM 还提供了其自身的 OpenMP 库。OpenMP 是一种通过多线程实现的基于编译指令的共享内存并行编程技术,也是 HPC 中最受欢迎的编程技术之一。OpenMP 的核心功能通过指令实现,因此编译器需要修改以支持这些编译指令。OpenMP 标准[5]规定了 C、C++ 和 Fortran 程序员如何使用该技术,其中编译器指令在 C 和 C++ 中表示为预编译指令(pragma),而在 Fortran 中则表示为特殊注释。定义指令开始的这串标记被称为标志符。

II-A Zig

Zig 是由 Andrew Kelly 于 2016 年创建的系统编程语言,旨在成为 C 的一种更优化、更安全且更易读的替代方案[4]。Zig 的设计专注于减少程序执行时间,同时在安全性和编程体验上相比 C 提供更高的水平。Zig 使用 LLVM 编译器基础设施[6]进行代码生成,从而能够利用其优化功能[7]。这种对 LLVM 的利用使得 Zig 支持大量 CPU 架构和操作系统,其目标是支持所有由 LLVM 支持的目标平台[8]。

Zig 提供了一些安全特性来改善软件开发体验,主要包括比 C 更强的类型系统和改进的静态分析功能,以及在调试模式下编译器启用的可选运行时安全检查。静态分析功能可以帮助程序员防止常见的错误,例如解引用空指针或与整数和浮点数类型转换相关的截断和舍入错误。例如,在 C 中,对于int *ptr = 0,解引用并读取ptr是合法的,但在运行时可能导致段错误。示例1 中的两个代码示例展示了 Zig 中如何防止这一问题。这两个示例均无法编译。第一个示例尝试将整数文字赋值给指针,这种隐式转换被 Zig 的类型系统所禁止。示例 1 中的第二个示例使用内置的@intToPtr函数执行显式的整数到指针转换,这种也会失败,因为在 Zig 中,只有可空指针可以被赋值为零。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
var ptr: *i32 = 0;
_ = ptr.*;
// —————————————
var ptr: *i32 = @intToPtr(*i32, 0);
_ = ptr.*;

示例 1:在 Zig 中如何表示解引用和读取 ptr 的示例(基于 C 中的 int *ptr = 0)。然而,由于 Zig 提供的安全性,这些示例均无法编译

在编译时无法识别的错误可能会通过运行时的安全检查标记为未定义行为。Zig 为代码编译提供了两种模式:生产模式和调试模式。在调试模式下,额外的代码会被插入到可执行文件中,例如检查是否发生了数组越界或整数溢出。如果发生此类情况,会触发运行时错误。而在生产模式中,出于性能原因,不提供此类安全检查,因此未定义行为不会被捕获到。Zig建议程序员在开发代码时使用调试模式,在代码成熟后切换到生产模式。

Zig 的设计目标之一是与现有的 C 代码库实现互操作[9]。这使得开发者能够利用 C 的库和框架,例如 MPI[10],并在项目中逐步用 Zig 替代 C。为实现这种互操作,Zig 提供了一种方法,既可以调用 C 函数,也可以让 C 调用 Zig 函数。示例2展示了从 Zig 调用 C 标准库函数puts的示例。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
extern fn puts(s: [*:0]const u8) c_int;
pub fn main() void {
// calling puts
  _ = puts("hello world");
}

示例 2:从 Zig 调用 C 函数的示例

示例3 展示了函数add被导出以供 C 使用的示例。该函数可以通过编译为目标文件(object file)或静态库(so)方式,链接到 C 程序中进行访问。此外,Zig 可以自动生成一个包含所有导出函数签名的 C 头文件,供 C 程序调用。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
pub export fn add(a: c_int, b: c_int) c_int{
 return a + b;
}

示例3:导出 Zig 函数供 C 使用的示例

Zig 编译器还提供了将 C 源代码转换为 Zig 的工具,这可以加速将整个项目或部分项目迁移到 Zig 的过程。此机制也被编译器用于自动解析 C 头文件,并导入其中的函数、结构体和常量。示例4展示了一个来自示例2的修改代码,其中函数puts的显式声明被替换为通过自动翻译 C 标准库stdio.h头文件所实现的导入。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// Importing the C stdio.h header
const stdio = @cImport(@cInclude("stdio.h"));
pub fn main() void {
 // calling the puts function from the stdio.h header
 _ = stdio.puts("hello world");
}

示例4:通过 Zig 导入 C 头文件并调用其中函数的示例

Zig适配OpenMP

LLVM 提供了OpenMP 运行时库,而本文工作的目标是调用该库提供的函数,在 Zig 中实现基于 pragma 的共享内存并发编程。本节探讨了将带有 OpenMP pragma 注解的 Zig 源代码与 LLVM 的运行时库连接的方法。所有修改均基于 Zig 0.10.1版本。

III-A 词法分析与语法解析

如第 II 节所述,OpenMP 依赖于 pragma 来指定程序如何并行,但 Zig 本身并不支持 pragma语法。因此,必须将其作为一种新类型的语句添加到Zig中。我们决定将 pragma 实现为特殊的注释,这与 Fortran 中的支持方式类似。

Zig 编译管道的第一步是词法分析(tokenisation),主要决策点是选择将整个 pragma 解析为单个标记,还是将其每个字段解析为独立的标记,分别对应图 1 中的选项 A 和 B。最终决定通过标识符(sentinel),然后将 pragma 的其余部分作为常规代码进行标记化,假装标识符不存在。这种方法之所以可行,是因为 pragma 完全由 Zig 本身使用的标记组成。

图 1:解析方式选择的示意图,A) 将整个 pragma 解析为单个标记,或 B) 将 pragma 分解为多个标记

Zig 的词法分析器支持对关键字进行标记化。因此,最初计划利用此机制来解析 OpenMP 的指令和子句(例如paralleldefault)作为关键字。然而,这种方法行不通,因为在 Zig 中关键字不能用作标识符,添加这些关键字会破坏与现有代码的兼容性。因此,解决方案是将 OpenMP 的关键字存储为标识符,并在解析时将其与常规标识符区分开。

标记化完成后,下一步是解析,这一步从标记生成抽象语法树(AST)。Pragma 应像其他语句一样被处理,Zig 解析器的核心是eatToken方法。该方法接受一个枚举值,表示标记的类型(称为标记标签)。如果下一个标记与标签匹配,则返回并推进解析器到下一个标记,否则返回 null。然而,由于 OpenMP 关键字未分配唯一的标签,该函数无法按正常工作。因此,添加了一组新标签来表示不同的 OpenMP 关键字,并使用字符串到关键字标记的哈希映射来识别字符串是否为关键字。我们修改了eatToken函数,使其能够接受新增关键词,并在解析 OpenMP 关键字标签时相应地解析标识符标签。

每个 OpenMP 指令都有一个 AST 节点标签,子句作为节点数据存储。子句数据存储在extra_data数组中,该数组是Zig 编译器用于注释 AST 节点的杂项数据的 32 位整数数组。所有子句数据必须能够以这种形式表示,其中所有子句都存储在单个数据结构中,其整数表示子句不同。

III-A1 处理列表子句

privatefirstprivateshared子句被定义为标识符的列表。在获取每个标识符的 AST 节点索引后,这些索引被连续存储在extra_data数组中,子句结构的开始和结束索引则存储在子句中。图 2 展示了private子句的一个示例,其中指令节点包含一个索引到extra_data数组,表示子句结构的起始位置。

图 2:将私有变量存储在 extra_data 数组中的示例

III-A2 处理压缩子句

非列表子句的存储大小是静态已知的,因此可以将它们存储在单一结构中。通过将该结构标记为压缩结构,可以将其视为一个 32 位整数并存储在extra_data数组中。这种方法允许通过读取数组的单个索引提取所有数据,无需进一步的间接访问。例如,循环调度信息存储为一个 3 位的枚举值(表示调度类型)以及一个 29 位的整数(表示区块大小),其支持多达 536,870,912 次迭代。由于区块大小必须大于 0[5],值 0 表示未指定区块大小。

有些子句可以用少于 32 位表示,并将它们组合到一个压缩结构中。例如,default子句使用 2 位的枚举表示,而nowait子句用一个布尔值表示,占用压缩结构中的 1 位。collapse子句则占用 4 位,因为用户通常不会希望折叠超过 16 层的循环。

III-B 代码生成

在将 OpenMP 的 pragma 进行词法分析和语法解析后,下一步是代码生成。支持 OpenMP 的典型编译器会在指令的位置插入对 OpenMP 运行时的调用。这里的替换需要在编译期改造AST语法树时完成。

我们最初尝试直接修改 AST 并注入所需的 OpenMP 调用。然而,在 Zig 中,AST 节点与原始源代码之间存在严格的关联性,因此无法随意添加新的节点。基于此,我们尝试了一种变通方案:在解析目标源代码之前,向其开头预置一个函数和结构定义模板,以便在代码生成期间复制这些模板来完成 OpenMP 函数和结构实例化。然而,由于当前 Zig编译器的设计,此方法不可行,因为在编译过程中很难找到这些模板的位置并将其传播到 AST中。

因此,我们采用了基于预处理器的方法,这种方法的优点在于可以轻松生成新代码,而无需手动确保每个标记和 AST 节点引用源文件都在固定位置。当然,这种预处理方法也存在一些挑战,主要是因为 Zig并未涵盖该场景的步骤。首先,所有未使用的函数参数和非全局范围的变量必须显式丢弃,这意味着只有已知会使用的变量才应生成。第二个挑战是,在预处理阶段缺乏语义上下文(例如变量类型及其用途),这一点在III-B3中有更详细讨论。

将预处理器纳入 Zig 编译器的一个核心部分,具有以下几个优点。首先,这使预处理器可以复用 Zig 编译器中内置的解析基础设施。其次,通过在文件加载后立即执行预处理器,可以在无需修改的情况下继续使用编译器的缓存机制。

我们的预处理器在多个环节运行,通过每次处理不同的 OpenMP 构造来替换相关代码。其总体算法的伪代码在清单 5中进行了描述。例如,所有并行区域在工作共享循环之前被替换。因此,只要嵌套的构造属于不同类型,就无需在预处理器中进行特殊处理。伪代码中的<<adjust source offset>>是因为节点以源代码列表的偏移量表示,因此在每次替换代码后必须调整修改的位置偏移量。此外,伪代码展示了为每个替换节点通过create-payload创建一个负载(payload)。此负载包含进行替换所需的信息,例如每个指令需要在源代码中执行替换的位置,以及该指令的具体信息。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
FUNCTION preprocess (source, step)
 ast := parse-source-into-ast(source)
 replacements := empty-list
 FOREACH node IN ast DO
 IF node IS OpenMP-node AND
 <<node matches current step>> THEN
 append(replacements, create-payload(node))
 END
 END
 IF step = parallel THEN
 FOREACH replacement IN replacements
 <<perform parallel region replacement>>
 <<adjust source offset>>
 END
 ELSE IF step == while THEN
 FOREACH replacement IN replacements
 <<perform worksharing loop replacement>>
 <<adjust source offset>>
 END
 END
 IF <<is last step>> THEN
 RETURN source
 ELSE
 RETURN preprocess(source, step)
 END

示例5:替换 OpenMP Pragma 和子句的预处理器算法的伪代码

III-B1 处理并行区域

大多数编译器通过函数分解的方式表示 OpenMP 并行区域,其中生成一个包含并行区域内容的函数[11]。访问这些区域的变量,例如默认共享的变量或通过sharedfirstprivatereduction子句显式捕获的变量,会作为参数传递给该函数。然后,该函数的指针被传递给 OpenMP 运行时库的函数,该函数会在每个线程上调用它。例如,LLVM 的 OpenMP API 使用__kmpc_fork_call实现此功能。

我们选择采用上述方法,传递给分解函数的变量作为参数传递给 OpenMP 运行时库函数__kmpc_fork_call,后者将它们转发给分解函数的回调。__kmpc_fork_call是变参函数,接受可变数量的参数。我们的设计是将参数分为三组,每组表示为?*anyopaque指针,这是 Zig 中等同于 C 的void *的类型。这三组参数指向包含firstprivatesharedreduction子句变量的结构体。

在分解函数中,一旦?*anyopaque指针被还原为其原始类型,就会为这些结构体的每个成员变量创建变量并初始化值。例如:

  • 1. 对于firstprivate子句,值为并行区域外作用域中的变量值;
  • 2. 对于shared子句,需要通过指针访问变量,并将共享变量的访问重写为指针访问;
  • 3. 对于private变量,只需在分解函数中简单定义。

Reduction 操作更为复杂,通过使用 Zig 的标准原子类型创建一个值来实现。一个 reduction 结构体被创建,包含指向这些原子值的指针,并以与其他变量相同的方式传递给分解函数回调。分解函数为每个 reduction 变量创建一个单独的变量,并使用 reduction 变量中持有的初始值进行初始化。初始化必须符合 OpenMP 标准[5]。为了保证线程安全,基于原子类型定义了原子读-修改-写操作。

然而,这种方法受限于 Zig 支持的原子操作。目前 Zig 仅支持加法、减法、最小值、最大值、二进制与(AND)、或(OR)、非与(NAND)、异或(XOR)和比较交换(CAS)。例如,乘法和逻辑与或不支持。我们使用 CAS 循环算法[12]实现了这些缺失的 reduction 操作。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
atom := <<value to be updated>>
operand := <<value to update it with>>
old := atomic-load(atom)
new := old * operand
WHILE TRUE DO
 exchange-success, actual-value :=
 compare-and-swap(&atom, old, new)
 IF exchange-success THEN
 BREAK
 ELSE
 old = actual-value
 new = old * operand
 END
END

清单 6 展示了我们使用 CAS 算法实现乘法 reduction 的伪代码。

III-B2 处理工作共享循环

与并行区域不同,工作共享循环不需要分解函数。Clang 的 OpenMP API 提供了两种实现工作共享循环的策略:

  1. 1. 静态调度:通过__kmpc_for_static_*函数实现;
  2. 2. 动态、分布式和运行时调度:通过__kmpc_dispatch_*函数实现。

这两种策略都要求明确循环的上界、下界、增量和比较操作符:

  • 1. 比较操作符直接从 Zigwhile循环的条件中获取;
  • 2. 下界由循环计数器变量的初始值决定;
  • 3. 上界来自比较操作符右侧的值;
  • 4. 增量来自继续表达式中增量操作符右侧的值。

静态调度的__kmpc_for_static_*包括:

  • __kmpc_for_static_init:执行循环迭代;
  • __kmpc_for_static_fini:每个线程完成后调用以最终化循环。

对于动态循环,__kmpc_dispatch_next用于处理下一个批次的迭代,而__kmpc_dispatch_init接收调度类型(如kmp_sch_dynamic_chunkedkmp_sch_guided_chunkedkmp_sch_runtime)。

III-B3 变量重写

预处理器尽量利用已有的变量名和表达式,例如,在分解函数中解包privatefirstprivate变量时复用相同的变量名。但也有不尽如人意之处,例如,shared变量必须重写为指针访问,而工作共享循环的 reduction 临时变量可能不能与其对应的shared变量同名。

由于预处理时缺乏语义上下文,这种替换更具挑战性。得益于 Zig 的简单语法[13]和无变量遮蔽机制,其只需利用 AST即可实现变量重写。

III-C 封装 OpenMP 运行时功能

OpenMP 标准定义了一组运行时函数,这些函数必须由符合 OpenMP 实现的运行时库提供。这些函数旨在让用户直接调用,通过omp_前缀标识,例如omp_get_thread_numomp_get_num_threads。为了使 Zig 程序员能够使用这些函数,我们在标准库中添加了一个omp命名空间,并通过 Zig 编译器的translate-c功能将所有函数声明从 C 转换为 Zig。这些转换后的函数声明随后被重新导出,同时移除了omp_前缀。示例 7 展示了如何通过此方法在 Zig 中获取线程 ID:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const omp = @import("std").omp;
const thread_id = omp.get_thread_num();

示例 7:使用 OpenMP 库封装器在 Zig 中获取线程 ID。

除了为 Zig 程序员提供的标准 OpenMP API,我们还需要一个内部 OpenMP API 供预处理器使用,以实现将 OpenMP 编译指令映射到 LLVM OpenMP 运行时库。这些函数和常量并非由 OpenMP 标准定义,而是由运行时实现(例如 LLVM OpenMP 的libomp)提供。这些函数声明与标准 OpenMP 函数采用相同方式转换,但被放置在.omp.internal命名空间中。与标准 API 不同,这些函数在导出时没有移除前缀,它们非直接供程序员使用。

此外,.omp.internal命名空间还包含一些开发的辅助工具,这些工具被预处理器用来支持 OpenMP 功能的实现。例如,__kmpc_dispatch_*__kmpc_for_static_*函数族的通用封装器,以及 III-B1 节中描述的 CAS 循环 reduction 算法。

实现方法

本文中的所有基准测试均在 Cray-EX ARCHER2 超级计算机的单节点上进行。每个节点由两个 64 核 AMD EPYC 7742 处理器组成,每个核包含32KB 的 L1 数据缓存、32KB 的 L1 指令缓存和 512KB 的 L2 缓存,以及由四个核共享的 16.4MB 的 L3 缓存。基准测试中使用的 OpenMP 运行时为基于 LLVM 13.0.0 的 libomp,它被 AMD 优化的 C 和 Fortran 编译器(AOCC)使用。C 和 Fortran 基准测试的参考实现分别通过 AOCC 的 Clang 和 Flang 编译器进行编译,使用相同的 OpenMP 运行时。与 Zig 版本基准测试集成的 Fortran 代码由 GNU gfortran 编译器(版本 7.5.0)编译,所有 C 代码均由 AMD Clang(版本 13.0.0)编译。AOCC 和 gfortran 的版本均为基准测试时 ARCHER2 平台上可用的最新版本。

每项基准测试针对每种线程数运行 5 次,并报告 5 次运行的平均值。执行时间使用参考实现中的内部计时器测量。

由于目前尚无现成的 Zig 高性能计算 (HPC) 基准测试,本文决定使用其他成熟 HPC 语言(如 Fortran 和 C)创建的基准测试,并将其转换为 Zig。为将 C 代码转换为 Zig,本文使用了 Zig 提供的translate-c子命令。然而,该工具实际使用中存在一些限制。首先,它会忽略所有 C 的编译指令(pragma),导致所有 OpenMP 相关信息丢失。其次,translate-c会在转换代码为 Zig 之前执行 C 的预处理器,这意味着所有通过#include引入的头文件也会被转换并出现在生成的 Zig 代码中。例如,示例 8 展示了一段定义返回预处理器定义常量的 C 代码,而清单 9 显示了转换后的 Zig 代码,尽管CONSTANT被定义了,但并未直接使用,取而代之的是其展开后的值。因此,尽管该工具有一定帮助,但这些限制意味着部分代码仍需手动移植并进行严格验证。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#define CONSTANT (37 + 5)
int foo(void) { return CONSTANT; }

示例 8: 用 translate-c 子命令转换的 C 程序

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
pub export fn foo() c_int {
    return @as(c_int, 37) + @as(c_int, 5);
}
pub const CONSTANT = @as(c_int, 37) + @as(c_int, 5);

示例 9: 使用 translate-c 子命令从 C 代码转换的 Zig 程序

Zig 编译器没有提供 Fortran 等效的 translate-c,因此所有 Fortran 代码都需要手动移植。然而,Zig 和 Fortran 之间有几个主要区别,最显著的是 Fortran 中的数组是从 1 开始索引的,且 DO 循环的上界是包含在内的,而 Zig 中则不是。因此,在这样的移植过程中,所有数组索引和循环下界都需要调整,这增加了复杂性。

尽管以前从未这样做过,但从 Zig 调用 Fortran 过程的过程类似于调用 C 函数,所有参数类型都更改为指针。此外,为了符合 LLVM 的名称修饰方案,必须在函数名的末尾添加一个下划线。同样,也可以从 Fortran 调用 Zig 函数,但必须再次注意名称修饰方案。例如,只有 GNU gfortran 提供可预测的全局变量条目名称。

结论

在这项工作中,我们利用了 NAS 并行基准测试 (NPB) 套件中的内核,该套件包括基于计算流体动力学 (CFD) 应用中常见算法模式的各种内核。

V-A 共轭梯度 (CG)

共轭梯度 (CG) 是我们选择的第一个基准测试,它利用了我们支持的大量 OpenMP 特性。我们将 Fortran 中的 conj_grad 子程序移植到 Zig 中,该子程序占据了大约 95% 的运行时间。这个子程序包括并行和工作共享指令、private、shared 和 firstprivate 变量共享子句、nowait 子句,以及在并行区域和工作共享循环上的归约操作。此外,这个内核还代表了在高性能计算 (HPC) 中形成大量工作负载的迭代算法。

图 3:CG 基准测试(C 类)在不同线程数下的加速比(包括我们在 Zig 中的方法和 Fortran 参考实现)

图3展示了在不同线程数下进行强缩放时,在C类问题规模下CG 内核的加速比。两种语言在 64 个线程以内通常遵循阿姆达尔定律,但在 96 和 128 个线程上运行时表现显著优于预期。这似乎是算法的固有特性,因为我们的 Zig 移植版和参考实现都遵循几乎相同的加速曲线,表明在这个基准测试中,Fortran 中的 OpenMP 和 Zig 中的 OpenMP 之间的性能非常相似。

表 I:在强缩放时,Zig 和 Fortran NPB CG 基准测试(C 类)在不同线程数下的运行时间

Number of threads

Zig runtime (s)

Fortran runtime (s)

1

149.40

170.17

2

82.34

83.35

16

21.85

21.80

32

11.26

11.28

64

5.83

5.98

96

2.80

2.98

128

1.81

2.07

表I显示了 Zig 和 Fortran 中 CG 基准测试的运行时间,可以看出,Zig 版本在单核上比 Fortran 代码快 1.15 倍,随后在所有其他线程数下性能大致相等,尽管 Zig 往往比 Fortran 略快。

V-B 极易并行 (EP)

极易并行 (EP) 内核仅关注计算性能,不需要线程之间的同步,并具有高效的内存访问模式。除了计时和验证例程外,我们将整个代码从 Fortran 移植到 Zig。这个内核利用了 private 和 firstprivate 变量共享子句,以及并行区域归约。此外,还使用了 threadprivate 和 atomic 指令。

图 4:EP 基准测试(C 类)在不同线程数下的加速比(包括我们在 Zig 中的方法和 Fortran 参考实现)

图4显示C类问题规模时进行强缩放时,Zig 移植版和 Fortran 参考实现版本的 EP 基准测试的加速比。可以看出,对于 Zig 移植版和参考实现,因为该算法不需要线程之间的通信,所以加速比与线程数成正比。例外情况出现在 128 个线程时,Fortran 参考实现的加速比超过了 128 倍,意味着该基准测试受益于超线性缩放,而在 Zig 移植版中未观察到这种情况。这可能是由于 Fortran 版本在更多线程数下更好地利用了缓存,因为每个线程的问题规模减少了。

表 II:在强缩放时,Zig 和 Fortran NPB EP 基准测试(C 类)在不同线程数下的运行时间

Number of threads

Zig runtime (s)

Fortran runtime (s)

1

147.66

185.26

2

76.17

94.90

16

9.84

11.83

32

4.72

5.92

64

2.29

2.84

96

1.57

1.97

128

1.36

1.42

表II显示了在强缩放时,Zig 移植版和 Fortran版本实现的 EP 基准测试的运行时间,可以看出,Zig 版本平均比Fortan版本快 1.2 倍。这与 CG 基准测试类似,基于 Fortran 在这些科学工作负载中的流行程度,我们对此结论感到惊讶。尽管 Fortran 版本在 128 核时表现变好,但其执行速度仍然比 Zig 版本的基准测试慢。

V-C 整数排序 (IS)

整数排序 (IS) 内核包含间接内存访问,旨在对内存子系统施加压力。该内核利用了 private 和 firstprivate 共享指令,并使用了 static,1 调度。IS 基准测试与本文考虑的其他基准测试的主要区别在于,它是用 C 语言编写的,我们将占总运行时间约 70% 的 rank 函数移植到了 Zig。

图 5:IS 基准测试(C 类)在不同线程数下的加速比(包括我们在 Zig 中的方法和 C 参考实现)

图5显示了在C类问题规模下进行强缩放时,Zig 移植版与 C 参考实现的 IS 基准测试在不同线程数下的加速比。可以看出,这两个版本的基准测试在整个线程数范围内遵循非常相似的缩放模式,然而 Zig 版本在初始阶段缩放得更好,因此在较多线程数时能够提供更大的加速比。

表 III:在强缩放时,Zig 和 Fortran NPB IS 基准测试(C 类)在不同线程数下的运行时间

Number of threads

Zig runtime (s)

Fortran runtime (s)

1

11.87

9.29

2

6.12

4.76

16

1.05

0.93

32

0.55

0.54

64

0.33

0.31

96

0.29

0.28

64

0.27

0.24

表III显示了在强缩放时,IS 基准测试的运行时间。可以看出,与 Fortran 基准测试相比,对于用 C 实现的基准测试,C 版本在单线程上表现最佳。虽然在并发1场景下运行时差异明显,在更多线程数时,两种语言的性能非常接近。

后续工作

在本文中,我们探讨了通过添加 OpenMP 的循环共享结构来增强 Zig。Zig 最初设计为一种系统编程语言,并利用了 LLVM 生态系统,该语言的一个主要特点是提供性能和安全性,这使其成为高性能计算(HPC)未来的一个非常有趣的潜在编程语言。虽然在 Zig 中调用 C 函数的能力意味着与 MPI 的集成相对简单,但支持基于 pragma 的 OpenMP 方法需要对编译器进行额外工作,但这对于该语言被 HPC 社区采纳却至关重要。

在描述了我们通过在编译器中支持 OpenMP 循环指令来为 Zig 添加基于 pragma 的共享内存并行性的方法之后,我们进行了使用 NASA 的 NPB 基准测试套件的性能对比。我们证明了该方法提供了类似于 C 和 Fortran 编译器的线程缩放,通过观察发现 Zig 基准测试的运行时间低于其 Fortran 对应版本。鉴于 Fortran 在科学计算中的应用范围,这些结果令我们感到惊讶。

我们认为推动 Zig 在 HPC 中落地的关键性条件将是为 Zig 编译器添加支持分析功能。目前,Zig 编译器使用 Tracy 库[17]进行分析,该库的 Zig 接口是编译器本身的一部分,不能在应用程序中使用。修改编译器以自动为应用程序添加调用该库的代码,提供类似于 gprof 的功能。此外,增强 Zig 与 Fortran 之间的互操作性非常重要,这将使 Zig 能够集成到现有的大型 Fortran 代码库中。虽然在本文中我们已经证明了这种集成是可行的,但这需要在编译器内部进行额外的工作,并可能扩展 Fortran 标准,以确保这种方法的可靠性和一致性。

总之,我们得出结论,Zig 编程语言所提供的性能和安全性组合使其有潜力应用于 HPC 工作负载场景。通过增强编译器以支持 OpenMP 循环指令,我们提供了在 Zig 中基于 pragma 的共享内存并行性的能力,并证明了其缩放性与其他语言相当。此外,对于 HPC 工作负载,性能甚至超过原有语言。

由于笔者时间、视野、认知有限,本文难免出现错误、疏漏等问题,期待各位读者朋友、业界专家指正交流。

参考文献

  1. 1. C. Lattner, Architecture of open source applications / structure, scale, and a few more fearless hacks. Lulu Com, 2012, vol. 1. [Online]. Available:http://www.aosabook.org/en/llvm.html
  2. 2. Apple, “Swift compiler,” 2023. [Online]. Available:https://www.swift.org/swift-compiler/
  3. 3. “The rustc book,” 2023. [Online]. Available:https://doc.rust-lang.org/rustc/what-is-rustc.html
  4. 4. A. Kelly, “Introduction to the Zig programming language,” Feb 2016. [Online]. Available:https://andrewkelley.me/post/intro-to-zig.html
  5. 5. OpenMP Application Programming Interface, nov 2021. [Online]. Available:https://www.openmp.org/wp-content/uploads/OpenMP-API-Specification-5-2.pdf
  6. 6. The LLVM Compiler Infrastructure, 06 2023. [Online]. Available:https://llvm.org/
  7. 7. The LLVM Compiler Infrastructure, 08 2023. [Online]. Available: LLVM’s Analysis and Transform Passes
  8. 8. A. Kelley, “bindings.zig,” Mar 2023. [Online]. Available:https://github.com/ziglang/zig/blob/master/src/codegen/llvm/bindings.zig
  9. 9. A. Kelly, “Complete C ABI Compatibility,” Feb 2016. [Online]. Available:https://andrewkelley.me/post/intro-to-zig.html#c-abi
  10. 10. MPI: A Message-Passing Interface Standard Version 4.0, Message Passing Interface Forum, jun 2021. [Online]. Available:https://www.mpi-forum.org/docs/mpi-4.0/mpi40-report.pdf
  11. 11. 13.10 Implementing PARALLEL construct. [Online]. Available:https://gcc.gnu.org/onlinedocs/libgomp/Implementing-PARALLEL-construct.html
  12. 12. H. Sutter, “How to write a CAS loop using std::atomics,” aug 2012. [Online]. Available:https://herbsutter.com/2012/08/31/reader-qa-how-to-write-a-cas-loop-using-stdatomics/
  13. 13. Zig Language Reference. [Online]. Available:https://ziglang.org/documentation/0.10.1/#Grammar
  14. 14. LLVM/OpenMP, 2023. [Online]. Available:https://openmp.llvm.org/
  15. 15. Programming languages — Fortran, ISO/IEC, 2004. [Online]. Available:https://j3-fortran.org/doc/year/04/04-007.pdf
  16. 16. “NAS Parallel Benchmarks,” Jul 2023. [Online]. Available:https://www.nas.nasa.gov/software/npb.html
  17. 17. B. Taudul, R. Kupstys, A. Machizaud, and A. Gerstmann, “Tracy Profiler,” aug 2023. [Online]. Available:https://github.com/wolfpld/tracy
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2024-12-04,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 DCOS 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
10 亿次嵌套循环,TOP10编程语言性能大对比,Python 表现居然"最差"!
从上图可以看出 C 和 Rust 并列第一,耗时 0.50 秒,性能最好,Python 表现最差,耗时 74.42 秒。
测试开发技术
2025/01/13
8500
10 亿次嵌套循环,TOP10编程语言性能大对比,Python 表现居然"最差"!
【独家】并行计算性能分析与优化方法(PPT+课程精华笔记)
[导读]工业4.0、人工智能、大数据对计算规模增长产生了重大需求。近年来,中国高性能计算机得到突飞猛进的发展,从“天河二号”到“神威·太湖之光”,中国超级计算机在世界Top500连续排名第一。云计算、人工智能、大数据的发展对并行计算既是机遇又是挑战。如何提高应用的性能及扩展性,提高计算机硬件的使用效率,显得尤为重要。从主流大规模并行硬件到能够充分发挥其资源性能的并行应用,中间有着巨大的鸿沟。 本次讲座由清华-青岛数据科学研究院邀请到了北京并行科技股份有限公司研发总监黄新平先生,从高性能并行计算发展趋势,
数据派THU
2018/01/29
2.9K0
【独家】并行计算性能分析与优化方法(PPT+课程精华笔记)
【OpenMP学习笔记】更多指令和子句介绍
flush指令主要用于处理内存一致性问题. 每个处理器(processor)都有自己的本地(local)存储单元:寄存器和缓存, 当一个线程更新了共享变量之后, 新的值会首先存储到寄存器中, 然后更新到本地缓存中. 这些更新并非立刻就可以被其他线程得知, 因此在其它处理器中运行的线程不能访问这些存储单元. 如果一个线程不知道这些更新而使用共享变量的旧值就行运算, 就可能会得到错误的结果. 通过使用flush指令, 可以保证线程读取到的共享变量的最新值. 下面是语法形式:
零式的天空
2022/03/02
9880
OpenMP并行编程入门指南
在C++中使用openmp进行多线程编程 - DWVictor - 博客园 (cnblogs.com)
用户9831583
2023/02/27
1.9K0
OpenMP并行编程入门指南
MIT开源高性能自动微分框架Enzyme:速度提升4.5倍
当前,PyTorch、TensorFlow 等机器学习框架已经成为了人们开发的重要工具。计算反向传播、贝叶斯推理、不确定性量化和概率编程等算法的梯度时,我们需要把所有的代码以微分型写入框架内。这对于将机器学习引入新领域带来了问题:在物理模拟、游戏引擎、气候模型中,原领域组件不是由机器学习框架的特定领域语言(DSL)编写的。因此在将机器学习引入科学计算时,重写需求成为了一个挑战。
机器之心
2021/01/06
9320
MIT开源高性能自动微分框架Enzyme:速度提升4.5倍
[062][译]Auto-Vectorization in LLVM
最近遇到一个性能问题,与Auto-Vectorization in LLVM有关,翻译一下官方介绍 http://llvm.org/docs/Vectorizers.html
王小二
2020/12/14
3.5K0
[062][译]Auto-Vectorization in LLVM
英特尔最新版 C/C++ 编译器采用 LLVM 架构,性能提升明显
下一代英特尔 C/C++ 编译器的表现会更加出色,因为它们将使用 LLVM 开源基础架构。
深度学习与Python
2021/10/13
1.1K0
OpenMP基础----以图像处理中的问题为例
1.循环语句中的循环变量必须是有符号整形,如果是无符号整形就无法使用,OpenMP3.0中取消了这个约束
流川疯
2022/05/10
1.4K0
C++与并行计算:利用并行计算加速程序运行
在计算机科学中,程序运行效率是一个重要的考量因素。针对需要处理大量数据或复杂计算任务的程序,使用并行计算技术可以大幅度加速程序的运行速度。C++作为一种高性能的编程语言,提供了多种并行计算的工具和技术,可以帮助开发人员充分利用计算资源,提高程序的性能。
大盘鸡拌面
2023/12/05
1.1K0
多核程序设计的相关基础知识----以误差扩散算法为例
    本文从基础入手,主要阐述基于桌面电脑的多核程序设计的基础知识,包括一些向量化运算,虚拟机算,多线程等的相关知识总结。
流川疯
2019/01/18
7940
现代CPU性能分析与优化-性能分析方法- Roofline 性能模型
Roofline 性能模型是一个以吞吐量为导向的性能模型,在 HPC 领域广泛使用。它于 2009 年在加州大学伯克利分校开发。模型中的“roofline”表示应用程序的性能不能超过机器的能力。程序中的每个函数和每个循环都受到机器的计算或内存容量的限制。这个概念在下图中有所体现。应用程序的性能始终会受到某条“roofline”函数的限制。
王很水
2024/08/19
8220
现代CPU性能分析与优化-性能分析方法- Roofline 性能模型
PGI OpenACC 2018版:原来你是这样的编译器
对于CUDA Fortran用户来说,PGI编译器是必然要用到的。 其实PGI编译器不仅仅可以支持Fortran,还可以支持C/C++。而对于集群用户来说,要将上万行的代码加速移植到GPU集群上,PG
GPUS Lady
2018/04/02
3.5K0
PGI OpenACC 2018版:原来你是这样的编译器
【OpenMP学习笔记】基本使用
OpenMP 是基于共享内存模式的一种并行编程模型, 使用十分方便, 只需要串行程序中加入OpenMP预处理指令, 就可以实现串行程序的并行化. 这里主要进行一些学习记录, 使用的书籍为: Using OpenMP: Portable Shared Memory Parallel Programming 和OpenMP编译原理及实现技术
零式的天空
2022/03/02
1.3K0
OpenMP 并行编程初探
在当今多核处理器的时代,利用并行计算的能力以最大化性能已成为程序员的重要任务之一。OpenMP 是一种并行编程模型,可以让我们更容易地编写多线程程序。本文将深入浅出地探讨 OpenMP 的工作原理、基本语法和实际应用。
运维开发王义杰
2023/08/15
1.6K0
OpenMP 并行编程初探
一文入门高性能计算HPC-详解2
接上文: https://cloud.tencent.com/developer/article/2508936
晓兵
2025/03/29
1170
一文入门高性能计算HPC-详解2
现代CPU性能分析与优化-性能分析方法-代码插桩
有读者反馈介绍的很不清晰。这里把翻译完整发出来。大家先看个大概,所有翻译都发一遍之后会做总结。预计这个内容起码发一个月吧
王很水
2024/08/08
2600
现代CPU性能分析与优化-性能分析方法-代码插桩
一文入门高性能计算HPC-详解1
高性能计算(HPC) 是使用多组尖端计算机系统执行标准商用计算系统无法实现的复杂模拟、计算和数据分析的艺术和科学。
晓兵
2025/03/29
2110
一文入门高性能计算HPC-详解1
【OpenMP学习笔记】编译制导指令
OpenMP通过在串行程序中插入编译制导指令, 来实现并行化, 支持OpenMP的编译器可以识别, 处理这些指令并实现对应的功能. 所有的编译制导指令都是以#pragma omp开始, 后面跟具体的功能指令(directive)或者命令. 一般格式如下所示:
零式的天空
2022/03/02
2.3K0
【C++】基础:OpenMP并行编程入门
OpenMP是一种用于并行编程的开放标准,它旨在简化共享内存多线程编程的开发过程。OpenMP提供了一组指令和库例程,可以将顺序程序转换为可并行执行的代码。
DevFrank
2024/07/24
9860
Python高性能计算库——Numba
摘要: 在计算能力为王的时代,具有高性能计算的库正在被广泛大家应用于处理大数据。例如:Numpy,本文介绍了一个新的Python库——Numba, 在计算性能方面,它比Numpy表现的更好。 最近我在观看一些SciPy2017会议的视频,偶然发现关于Numba的来历--讲述了那些C++的高手们因为对Gil Forsyth和Lorena Barba失去信心而编写的一个库。虽然本人觉得这个做法有些不妥,但我真的很喜欢他们所分享的知识。因为我发现自己正在受益于这个库,并且从Python代码中获得了令人难以置信
IT派
2018/03/28
2.7K0
Python高性能计算库——Numba
推荐阅读
相关推荐
10 亿次嵌套循环,TOP10编程语言性能大对比,Python 表现居然"最差"!
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验