LLVM 项目博客

LLVM 项目新闻和来自战壕的细节

使用 Cling 进行交互式 C++

使用 Cling 进行交互式 C++

C++ 编程语言被用于许多数值密集型科学应用。性能和稳固的向后兼容性的结合使其在过去 20 年中被许多研究软件代码使用。尽管功能强大,C++ 常常被认为难以学习,并且与快速应用程序开发不一致。在开发过程中,漫长的编辑-编译-运行周期会减慢探索和原型设计的速度。

Cling 已经成为公认的功能,它为 C++ 开发人员提供了交互性、动态互操作性和快速原型设计功能。Cling 支持完整的 C++ 功能集,包括使用模板、lambda 和虚继承。Cling 是一个交互式 C++ 解释器,构建在 Clang 和 LLVM 编译器基础设施之上。解释器支持交互式探索,使 C++ 语言更易于研究人员使用。

高能物理 (HEP) 领域中用于存储、研究和可视化科学数据的首要工具是专门的软件包 ROOT。ROOT 是一组相互关联的组件,可帮助科学家从数据存储和研究到在科学论文中发布时的可视化。ROOT 在引力波、吉萨大金字塔中的巨大空腔、大型强子对撞机发现希格斯玻色子等科学发现中发挥了重要作用。在过去 5 年中,Cling 帮助分析了 1 EB 的物理数据,是 1000 多篇科学出版物的基础,并支持跨越分布式百万 CPU 内核计算设施运行的软件。

最近,我们启动了一个项目,旨在利用我们在交互式 C++、即时编译技术 (JIT)、动态优化和大型软件开发方面的经验,极大地减少 C++ 和 Python 之间的阻抗不匹配。我们将使 Cling 通用化,以提供针对 C++ 语言互操作性的健壮、可持续和多学科解决方案。我们的目标范围是

  • 推进解释技术,提供最先进的 C++ 执行环境,
  • 启用功能,提供 C++ 和 Python(最终包括 Julia 和 Swift 等其他语言)之间的类似本机的动态运行时互操作性,
  • 允许无缝利用异构硬件(如硬件加速器)。

项目成果将被集成到广泛使用的工具 LLVM、Clang 和 Cling 中。拟议工作的结果是一个平台,它提供 C++ 编译器即服务 (CaaS),用于快速应用程序开发和计算性能。

本文的其余部分旨在演示 Cling 的设计和若干功能。想要跟着一起做吗?您可以从 conda 获取 cling

conda config --add channels conda-forge
conda install cling
conda install llvmdev=5.0.0

或者如果您还没有使用 conda,则可以从 docker-hub 获取

docker pull compilerresearch/cling
docker run -t -i compilerresearch/cling

无论哪种方式,输入“cling”启动其交互式 shell

cling
****************** CLING ******************
* Type C++ code and press enter to run it *
*             Type .q to exit             *
*******************************************
[cling]$
[cling]$ #include "cling/Interpreter/Interpreter.h"
[cling]$ gCling->allowRedefinition(false)

我们将在本文的后续部分解释这些命令的目的以及使用 cling 的其他替代方法。

解释 C++

探索式编程(或快速应用程序开发)是了解项目需求的有效方法;以减少问题的复杂性;并提供系统设计和实现的早期验证。特别是,交互式探测数据和接口使复杂库和复杂数据对用户更加容易访问。它在数据科学、计算科学和调试中很重要。它显着减少了开发过程中编辑-运行循环所消耗的时间。在实践中,只有少数编程语言同时提供编译器和解释器,将它们转换为机器代码,尽管语言是解释还是编译是实现的属性。

支持探索式编程的语言往往具有缩短编译-链接周期的解释器;这通常会导致性能明显下降。认识到探索式编程用例的语言开发人员也可能会添加语法糖,但这主要是为了方便和简洁。通过使用即时 (JIT) 或提前 (AOT) 编译技术,可以很大程度地降低性能损失。

为了本文系列的需要,解释 C++ 意味着为 C++ 启用探索式编程,同时使用 JIT 编译来降低性能损失。图 1 显示了探索式编程的示例。调整形状、选择大小和颜色或与之前的设置进行比较变得微不足道。看不见的编译-链接循环有助于交互式使用,这允许采用一些质量不同的程序开发方法并提高生产力。


图 1. 交互式 OpenGL 演示,改编自 [这里](https://www.youtube.com/watch?v=eoIuqLNvzFs)。

设计原则

cling 的一些设计目标包括

  • 不为不使用的东西付费 - 优先处理处理正确代码的性能。例如,为了提供错误恢复,不要惩罚用户输入语法和语义上正确的 C++;并且只有在必要时才会进行交互式 C++ 转换,并且可以禁用这些转换。
  • 以(几乎)任何代价重用 Clang & LLVM - 不要重新发明轮子。如果某个功能不可用,请尝试找到一种最小的实现方式,并将其提交给 LLVM 社区进行审查。否则,找到最小的补丁,即使是以误用 API 为代价,也要满足需求。
  • 持续功能交付 - 专注于最小的功能,其在主要用例 (ROOT) 中的集成,在生产中的部署,重复。
  • 库设计 - 允许从第三方框架中将 Cling 用作库。
  • 学习和发展 - 尝试用户体验。没有关于整体用户体验的正式规范或共识。应用从 CINT 中继承的经验教训。

架构

Cling 接受部分输入并确保编译器进程保持运行以对传入的代码进行操作。它包含一个 API,提供对最近编译的代码块属性的访问。Cling 可以对每个代码块应用自定义转换,然后执行。Cling 协调现有的 LLVM 和 Clang 基础设施,遵循图 2 中描述的数据流。


图 2. Cling 中的信息流

简而言之
  1. 该工具通过交互式提示或允许增量处理输入的接口来控制输入基础设施 (➀)。
  2. 它将输入发送到底层的 clang 库进行编译 (➁)。
  3. Clang 编译输入,可能将其包装到函数中,生成 AST (➂)。
  4. 如有必要,AST 将被进一步转换,以附加特定行为 (➃)。

例如,报告执行结果或其他与解释器相关的功能。一旦高级 AST 表示准备就绪,它将被发送到降低到 LLVM 特定的汇编格式,即 LLVM IR (➄)。LLVM IR 是 LLVM 的即时编译基础设施的输入格式。Cling 指示 JIT 运行指定函数 (➅),将它们转换为针对底层设备架构的机器代码 (MC)(例如 Intel x86 或 NVPTX)(➆,➇)。

C++ 标准是针对编译器开发的,没有很好地涵盖交互式使用。在全局范围内执行语句、报告执行结果以及实体重新定义是用户友好性的三个最重要的功能。长时间运行的解释器会话容易出现打字错误,并且需要完美的错误恢复。更高级的用例需要运行时额外的灵活性以及帮助 eval 风格编程的查找规则扩展。当 C++ 用作脚本语言时,高效的基于水印的代码删除非常重要。

执行语句

Cling 增量式地处理 C++。增量输入包含一个或多个 C++ 语句。C++ 不允许在全局范围内使用表达式。

[cling] #include <vector>
[cling] #include <iostream>
[cling] std::vector<int> v = {1,2,3,4,5}; v[0]++;
[cling] std::cout << "v[0]=" << v[0] <<"\n";
v[0]=2

相反,Cling 将每个输入移至唯一的包装函数。例如

void __unique_1 () { std::vector<int> v = {1,2,3,4,5};v[0]++;; } // #1
void __unique_2 () { std::cout << "v[0]=" << v[0] <<"\n";; } // #2

在构建 clang AST 后,cling 检测到包装程序 #1 包含声明,并将声明的 AST 节点移至全局范围,以便 v 可以被后续输入引用。包装程序 #2 包含语句,并按原样执行。在 Cling 内部,该示例被转换为

#include <vector>
#include <iostream>
std::vector<int> v = {1,2,3,4,5};
void __unique_1 () { v[0]++;; }
void __unique_2 () { std::cout << "v[0]=" << v[0] <<"\n";; }

Cling 在这些包装程序被编译为机器代码后运行它们。

报告执行结果

交互性的一个组成部分是打印表达式值。每次输入 printf 都很费力,并且没有自然地包含对象类型信息。相反,省略输入的最后一个语句的分号会告诉 Cling 报告表达式结果。包装输入时,Cling 会在输入的末尾附加一个分号。如果请求执行报告,则相应的包装器 AST 不包含 *NullStmt*(模拟额外的分号)。

[cling] #include <vector>
[cling] std::vector<int> v = {1,2,3,4,5} // Note the missing semicolon
(std::vector<int> &) { 1, 2, 3, 4, 5 }

转换会根据特定实体的属性注入额外的代码,例如它是否可复制、它是否是一个包装器临时对象或数组。Cling 可以通过提供“托管”存储来报告有关不可复制对象或临时对象的信息。托管存储(*cling::Value*)也用于在嵌入式设置中交换解释代码和编译代码之间的值。

实体重新定义

名称重新定义是一个重要的脚本功能。对于基于笔记本的 C++ 来说,它也是必不可少的,因为每个单元都是一个相对独立的计算。C++ 不支持实体重新定义。

[cling] #include <string>
[cling] std::string v
(std::string &) ""
[cling] #include <vector>
[cling] std::vector<int> v
input_line_7:2:19: error: redefinition of 'v' with a different type: 'std::vector<int>' vs 'std::string' (aka 'basic_string<char, char_traits<char>, allocator<char> >')
 std::vector<int> v
                  ^
input_line_4:2:14: note: previous definition is here
 std::string v
             ^

Cling 使用内联命名空间实现实体重新定义,并重新连接 clang 查找规则,以优先考虑最新的声明。该功能的完整描述已作为会议论文在 CC 2020 上发表(ACM 编译器构造会议)。我们通过调用 gCling->allowRedefinition() 来启用它

[cling] #include "cling/Interpreter/Interpreter.h"
[cling] gCling->allowRedefinition()
[cling] #include <vector>
[cling] std::vector<int> v
(std::vector<int> &) {}
[cling] #include <string>
[cling] std::string v
(std::string &) ""

无效代码。错误恢复

在交互模式下使用时,无效的 C++ 不会终止会话。相反,无效代码会被丢弃。底层的 clang 进程会将其内部数据结构中的无效 AST 节点保留下来,以便更好地进行错误诊断和恢复,期望该进程将在发出诊断后不久结束。这个特定示例更具挑战性,因为它首先包含有效和无效的构造。错误恢复应该撤消内部结构中的大量更改,例如名称查找和 AST。Cling 在许多高性能环境中使用;使用检查点不是一个可行的选择,因为它会对正确代码引入开销。

[cling] #include <vector>
[cling] std::vector<int> v; v[0].error_here;
input_line_4:2:26: error: member reference base type 'std::__1::__vector_base<int, std::__1::allocator<int> >::value_type' (aka 'int') is not a structure or union
 std::vector<int> v; v[0].error_here;
                     ~~~~^~~~~~~~~~~

为了处理该示例,Cling 将增量输入建模为一个 *Transaction*。事务表示 Clang 的内部数据结构更改的增量。Cling 侦听来自各种 Clang 回调的事件,例如声明创建、反序列化和宏定义。这些信息足以撤消更改并继续处于有效状态。实现非常复杂,在许多情况下,根据输入声明类型,需要额外的工作。

Cling 还通过代码转换来防止空指针解引用,避免会话崩溃。

[cling] int *p = nullptr; *p
input_line_3:2:21: warning: null passed to a callee that requires a non-null argument [-Wnonnull]
 int *p = nullptr; *p
                    ^
[cling]

错误恢复和代码卸载的实现仍然存在一些不足之处,并且正在不断改进。

代码删除

增量式交互式 C++ 假设长时间运行的会话,不仅会发生语法错误,还会发生语义错误。如果我们想用少量调整重新执行相同的代码,这会增加一层额外复杂性。

[cling] .L Adder.h // #1, similar to #include "Adder.h"
[cling] Add(3, 1) // int Add(int a, int b) {return a - b; }
(int) 2
[cling] .U Adder.h // reverts the state prior to #1
[cling] .L Adder.h
[cling] Add(3, 1) // int Add(int a, int b) {return a + b; }
(int) 4

在该示例中,我们使用 *。L* 元命令包含一个头文件;使用 *。U*“取消包含”它,并使用 *。L*“重新包含”它,以重新读取修改后的文件。与错误恢复情况不同,Cling 不能隔离机器代码降低基础设施,需要撤消 clang CodeGen 和 llvm JIT 以及机器代码基础设施中的状态更改。该功能的实现需要对 LLVM 工具链的大部分内容有深入的了解。

结论

Cling 已经成为支持交互式 C++ 的系统之一,已有十多年历史。Cling 的可扩展性和快速原型设计功能对于高能物理研究人员至关重要,也是他们所依赖的许多技术的推动力量。Cling 拥有许多独特的特性,专门针对增量式 C++ 中遇到的挑战。我们对交互式 C++ 的工作一直在不断发展。在下一篇博客文章中,我们将重点介绍面向数据科学的交互式 C++;Eval 风格编程;交互式 CUDA;以及笔记本中的 C++。

您可以在 https://root.cern/cling/https://compiler-research.org 了解更多关于我们活动的信息。

致谢

作者感谢 Sylvain Corlay、Simeon Ehrig、David Lange、Chris Lattner、Javier Lopez Gomez、Wim Lavrijsen、Axel Naumann、Alexander Penev、Xavier Valls Pla、Richard Smith、Martin Vassilev 对这篇文章的贡献。