原文地址:blog.llvm.org/posts/2020-…
原文作者:blog.llvm.org/
发布时间:2020年11月30日-阅读11分钟
交互式C++与Cling
C++编程语言被用于许多数值密集型科学应用。在过去的20年里,它的性能和坚实的向后兼容性相结合,导致它被用于许多研究软件代码。尽管C++功能强大,但它经常被视为难以学习,并且与快速应用开发不一致。在开发过程中,由于编辑-编译-运行的周期较长,探索和原型设计的速度被拖慢。
Cling的出现是公认的能力,它能让C++开发者实现交互性、动态互操作性和快速原型开发能力。Cling支持完整的C++特性集,包括模板、lambdas和虚拟继承的使用。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
或从docker-hub安装,如果你还没有使用conda。
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演示 交互式OpenGL演示,改编自这里。
设计原则
cling的一些设计目标包括。
- 不要为不用的东西付费 -- -- 优先考虑处理正确代码的性能。例如,为了提供错误恢复不要惩罚输入语法和语义正确的C++的用户;交互式C++转换只在必要的时候进行,并且可以禁用。
- 以(几乎)任何代价重用Clang与LLVM--不要重新发明轮子。如果一个功能不可用,那么试着找到一个最小化的方式来实现它,并将其提交给LLVM社区审查。否则找到最小化的补丁,即使以滥用API为代价,也要满足需求。
- 持续的功能交付--专注于一个最小的功能,它在主用例(ROOT)中的集成,在生产中部署,重复。
- 库设计--允许Cling作为第三方框架的库使用。
- 学习和进化--用用户体验做实验。在整体用户体验方面没有正式的规范或共识。应用从CINT遗留的经验教训。
架构
Cling接受部分输入,并确保编译器进程持续运行,以对输入的代码采取行动。它包括一个API,提供对最近编译的代码块属性的访问。Cling可以在执行前对每个chunk进行自定义转换。Cling按照图2描述的数据流来协调现有的LLVM和Clang基础架构。
图2.Cling中的信息流
简而言之
- 该工具通过交互式提示或允许增量处理输入的接口来控制输入基础设施 (➀)。
- 它将输入发送到底层的 clang 库进行编译 (➁)。
- Clang 将输入(可能是封装成函数)编译成 AST (➂)。
- 在必要的时候,AST会被进一步转换,以附加特定的行为 (➃)。
例如,报告执行结果,或其他与解释器相关的功能。一旦高级AST表示法准备好了,它就会被发送到LLVM特定的汇编格式,即LLVM IR (➄)。LLVM IR 是 LLVM 的及时编译基础架构的输入格式。Cling指示JIT运行指定的函数(➅),将它们转化为针对底层设备架构(例如Intel x86或NVPTX)的机器代码(MC)(➆,➇)。
C++标准是针对编译器开发的,并没有很好地覆盖交互式使用。在全局作用域上执行语句、报告执行结果和实体重新定义是在用户友好性方面最重要的三个功能。长时间运行的解释器会话很容易出现打字错误,因此无懈可击的错误恢复至关重要。更高级的用例需要在运行时有额外的灵活性,而查找规则扩展则是辅助评价式编程的。当使用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检测到wrapper #1包含一个声明,并将声明的AST节点移动到全局作用域,这样v就可以被后续输入引用。wrapper #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 conference on Compiler Construction)的会议论文发表。我们通过调用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
[cling] .L Adder.h // #1,类似于#include "Adder.h"
[cling] Add(3, 1) // int Add(int a, int b) {return a - b; }。
(int) 2
[cling] .U Adder.h // 恢复#1之前的状态。
[cling] .L Adder.h
[cling] Add(3, 1) // int Add(int a, int b) {return a + b; }。
(int) 4
在本例中,我们用.L元命令包含一个头文件;用.U "uninclude",用.L "reinclude",重新读取修改后的文件。与错误恢复的情况不同,Cling不能对机器代码降低基础设施进行围栏,需要在clang CodeGen和llvm JIT和机器代码基础设施中撤销状态变化。这个功能的实现需要LLVM工具链中很大一部分的专业知识。
结束语
Cling是实现交互式C++的系统之一,已有十多年的历史。Cling的可扩展性和快速原型功能对于高能物理研究人员来说是至关重要的,也是他们所依赖的许多技术的推动者。Cling有几个独特的功能,是为应对增量式C++带来的挑战而量身定做的。我们在交互式C++方面的工作一直在不断发展。在下一篇博文中,我们将重点介绍用于数据科学的交互式C++;Eval-Style Programming;交互式CUDA;以及笔记本中的C++。
你可以在 root.cern/cling/ 和 compiler-research.org 找到更多关于我们活动的信息。
鸣谢
作者要感谢Sylvain Corlay、Simeon Ehrig、David Lange、Chris Lattner、Wim Lavrijsen、Axel Naumann、Alexander Penev、Xavier Valls Pla、Richard Smith、Martin Vassilev,他们为这篇文章做出了贡献。
通过www.DeepL.com/Translator (免费版)翻译