Kaleidoscope conclusion
学习内容
完成对 https://llvm.org/docs/tutorial/MyFirstLanguageFrontend/LangImpl10.html 的学习。
练习
总结本次实现待改进点:
基本功能:
1 补充测试用例(lexer,loc等),新增系统测试以覆盖更复杂的场景(可以直接参考llvm代码中的test/Examples/Kaleidoscope/Chapter4.test)
2 loc中filename冗余消除
3 ast的dump功能
4 智能指针的使用改进,shared_ptr是否能转回unique?能否实现自动类型转换,避免大量的get?
5 添加简单的入参解析流程,help信息改进
6 建立行缓冲,在遇到解析错误时,把错误行打印出来,帮助调试
7 头文件中using namespace,可能导致污染,需要去掉。
8 调试信息bugfix,var/for中的变量还未进行声明,operator = 的信息生成还有问题 (已修复)
9 尝试接入方舟的Maple IR,尝试实现Vistor模式
…
把教程中的一些严重问题,如for的语义,调试信息不正确的问题邮件反馈。
新的有趣扩展
1 添加global variables实现
2 添加类型系统typed variables
3 添加arrays, structs, vectors的支持,练习LLVM getelementptr instruction的使用
3 实现辅助的runtime功能,例如IO?
4 内存管理memory management
5 异常支持exception handling support
Kaleidoscope debug info
学习内容
完成对 https://llvm.org/docs/tutorial/MyFirstLanguageFrontend/LangImpl09.html 的学习。
练习
看懂原网页后,实现同样的功能。
扩展
1 调试信息生成时,提到Kaleidoscope语言的abi接近C的abi。从哪里可以明确这一点?
2 source_loc的实现是否合理?有无改进空间?
string的存储?
3 能否将core_lib中的operator 先放到parser中解析,完成后再解析输入的用户文件?通过合并ast后再codegen,应该可以静默的实现语言扩展的operator。
4 var变量中的变量是否正确生成了调试信息
进展
为减少工作了,剔除原示例悄悄添加的ast dump功能(文字没有介绍,代码新增了)。
实现了debug信息的添加,并修复了原实现中的逻辑错误。
实现
实现中的主要改动如下:
1 出于节约工作量考虑,删除了原示例中的AST的dump功能,暂未实现;
2
原文实现时的调试信息发射有问题,会导致部分指令的调试信息错误。
如下binary op的发射代码所示,原示例在函数头部emitLocation,其信息会马上被随后的LHS/RHS codegen覆盖(他们也会emitLocation)。这样一来,真正属于operator的CreateFAdd指令会位于最后一个emitLocation指向的location(也就是RHS的location)。
1 | Value *BinaryExprAST::codegen() { |
使用如下的方法编译llvm中的示例代码。
1 | cd llvm-project/llvm/examples/Kaleidoscope/Chapter9 |
然后用下面的测试代码测试toy程序(输入后ctrl+D结束程序)
1 | def binary , 1 (left right) right |
可以获得其输出的LLVM-IR打印如下。
1 | define double @te(double %y) !dbg !13 { |
可以看到te函数中的两个’,’ operator,其对应的行号都指向了rhs的位置,完全和源代码对不上。
使用我们的实现编译代码(因为已经内置了’,’,去掉了其定义)。
1 | extern kout(x) |
获得的输出如下。
1 | efine double @te(double %y) !dbg !3 { |
可以看到,修正后’,’的位置(参考)可以与源代码吻合。
%”binary,with_prio_1” = call double @”_binary,_with_prio_1”(double %callkout, double 5.000000e+00), !dbg !12 这里说明第一个’,’的location在!dbg !12中给出。
而!12 = !DILocation(line: 4, column: 8, scope: !3)准确给出了,’,’在代码的第4行第8列(scope: !3可以继续看到其属于te函数)。
3 为token也添加了location信息,ast的location从token中获取,不直接与lexer打交道层级更清晰。
4 新增了调试信息发射的控制流程和开关变量
5
在发射函数的IR时,我们的实现为了方便控制调试信息的发射,
将调试信息的发射拆分成了两块。args的调试信息在args的store指令之后发射。
这会导致verifyFunction是发生下面错误。
1 | Expected no forward declarations! |
使用 def foo (x y) x+y即可复现。
看起来dbg.declare需要放到对应store指令的前面。
参考https://stackoverflow.com/questions/34236034/how-to-track-down-llvm-verifyfunction-error-expected-no-forward-declarations/60656058#60656058后,
在添加verifyFunction前添加finalizeSubprogram,可更正错误。
扩展问题
1 调试信息生成时,提到Kaleidoscope语言的abi接近C的abi。从哪里可以明确这一点?
语言目前没有明确设计ABI,在LLVM-IR的生成过程中,其实也不需要配置这些内容。当需要具体生成代码时,LLVM的处理流程会用默认值来工作。对于函数的 calling conventions 来说,可以用setCallingConv方法来专门进行设置。通过追踪设置函数可以看到,其初始值应该为0。
1 | void setCallingConv(CallingConv::ID CC) { |
0值对应的意义可以在llvm/IR/CallingConv.h中找到
1 | /// A set of enums which specify the assigned numeric values for known llvm |
这样看起来,原文的说法是基本正确的。在没有设置ABI的情况下,LLVM应该是用了C的配置作为默认值。
2 source_loc的实现是否合理?有无改进空间?
为了简单,source_loc当前存在大量的冗余信息。
至少其中大量重复的filename string应该合并到一个上,改用idx指向一个vector。
最终的方案可能是参考gcc等成熟编译器,将source_loc整个设计为一个idx,要取用的时候再组装成完整的信息。内部储存时,可以合并冗余的string,甚至还可以进一步采用压缩编码方式来记录行号和列号(例如使用基础值+偏移值的方式来记录)。
3 能否将core_lib中的operator 先放到parser中解析,完成后再解析输入的用户文件?通过合并ast后再codegen,应该可以静默的实现语言扩展的operator。
可以,但是不能通过合并ast来实现。
当前用户自定义operator的功能需要lexer的支持,parse正式代码时lexer不知道新增了哪些operator,会导致unknown token的出现。
目前实现的方案是,在parser中静默导入了自定义operator的extern声明,这样用户可以像使用内置operator一样直接使用这些扩展operator。自定义operator的实现放到了core_support_lib中,封装脚本会链入实现。
当前实现的主要问题是,operator很多都是短小语句,应该inline优化的,但是拆开成库的形式后,只有lto优化才能达到效果。
后续可能的改进是,直接把def定义灌入parser,通过直接修改lexer的loc信息(或者先关掉调试信息输出生成operator定义部分,再codegen剩下的部分),解决调试信息的冲突问题。
4 var变量中的变量是否正确生成了调试信息
没有,原示例var和for中的变量都没有做declare,所以没有对应的调试信息。需要参考args中的处理方法,逐个添加。
出于工作量考虑,本次实现也暂时还未添加这些调试信息。
实现中遇到的问题
1 ranged loop 内部定义的变量,无法跨过循环体保存值。如下代码
1 |
|
输出的结果将是
1 | 0:1 |
而不是预期的1:1,2:2,3:3。并且,打开Wall -Wextra时也没有告警。。。
2
实现时再次测试了using namespace std;在头文件中的作用范围
1 |
|
会报如下错误。说明头文件中的using会污染和其相同的命名空间。控制using namespace的作用范围仍然是一个有意义的功能。
1 | tt.cpp:4:26: error: ‘string’ was not declared in this scope |
Kaleidoscope object generation
学习内容
完成对 https://llvm.org/docs/tutorial/MyFirstLanguageFrontend/LangImpl08.html 的学习。
练习
看懂原网页后,实现同样的功能。
扩展
1 原示例使用了通用的机器模型进行编译,能否实现针对本地机器的更细粒度优化?
2 能否以较低代价实现支持交叉编译?
进展
完成了原示例中的功能。
新增了功能:针对native机器的细粒度优化(打开本地cpu支持的特性)。
新增了基于环境变量的选项控制功能:默认生成object文件后,可以用选项控制保留LLVM-IR中间文件。
实现
实现的两个主要变更:
1
原示例基于通用cpu的特性来生成代码,优化没有充分利用本地cpu的能力。本次实现改为了基于native去探测本地cpu的能力,选择最合适的指令(相当于使用l了-march=native)。具体实现过程使用了llvm/CodeGen/CommandFlags.inc中的getCPUStr函数来探测cpu。但是该inc文件似乎并不是一个稳定的开放接口文件,在工程内多次包含启动时会有冲突,同时其设置内部变量的方法也比较粗暴。实现时通过将其隔离到单个cpp文件来规避了该问题,可能并不是最好的解决方法。
2
添加object文件生成功能后,按照编译器的通常约定,将默认输出从LLVM-IR修改为object文件。同时,为了调试编译器本身的逻辑,查看编译过程中生成的LLVM-IR也是很有帮助的。
为了支持这样的控制逻辑,添加了一个基于环境变量的选项控制框架。实现代码如下。
1 | class control_flags |
其基本思路很简单,借鉴自golang编译器的实践。从环境变量中获取输入,避开繁琐的输入选项解析。再利用sstream提供的通用类型转换功能,可以完成大多数情况下的flag数值设置。后续只需要按需增加callback函数做输入的合法性检查即可。
在此框架下添加控制选项只需新增如下一行即可。引用flags时,只需将定义一个全局变量 control_flags global_flag,然后引用global_flag.save_temps等名称即可。
1 | //变量类型,变量名称,变量默认值,用于控制该变量的环境变量名称,变量作用描述 |
扩展问题
1 原示例使用了通用的机器模型进行编译,能否实现针对本地机器的更细粒度优化?
使用llvm提供的机制即可自动探测本地cpu的能力,细节可参考实现1中的描述。
2 能否以较低代价实现支持交叉编译?
llvm框架中交叉编译是默认配置,本地配置只不过一种特化场景。因此,要支持交叉编译非常容易,只需要新增一个入参指定目标代码的三元组TargetTriple(如x86_64-linux-gnu)即可。但是考虑到新增这个特性后,需要一并添加正确性检查,修改optimizer中的逻辑,工作量稍大。在完成主体工作前,可以稍缓一点实现。
Kaleidoscope mutable variable
学习内容
完成对 https://llvm.org/docs/tutorial/MyFirstLanguageFrontend/LangImpl07.html 的学习。
练习
看懂原网页后,实现同样的功能。
扩展
1 使用CreateAlloca在function的头部创建栈空间,是否会导致不必要的额外栈空间占用?
进展
完成了原示例的功能,添加了对应的简单测试。
以库的方式添加了’,’和’!=’等核心的operator,尚未自动添加到源代码中。
一种可行的简单方式是以源代码方式直接把这些operator的def直接include到代码的头部,然后再编译。但是,需要考虑这样操作对源代码行号的干扰。
等待后续生成调试信息的章节一并考虑。
实现
实现时做了如下两个主要的改进:
1 var中变量默认数值设置为0,原示例是在LLVM-IR 生成时构造的。从逻辑上看,这个应该是语法层面的规定,不应该放到codegen的流程中决定。本次实现时,在parser中parse_var时将未初始化的变量value设置为了0。codegen时只管按值生成就可以了。
2 在处理var中的变量shadow前面已定义变量的情况时。原示例为了简单,是直接把var中声明的所有变量都缓冲到了OldBindings中,如果没有shadow,则把nullptr缓冲到OldBindings中。生成完body后,直接把OldBindings中的所有条目写回NamedValues中。如下片段所示。
1 | std::vector<AllocaInst *> OldBindings; |
本次实现使用了更安全的find来替代[] operator,同时改进了缓存结构。
通过存储named_var中被shadow变量alloca字段的地址,消除了恢复
named_var时的map查找动作,如下片段所示。
1 | vector<std::pair<AllocaInst **, AllocaInst *>> saved_name_vec; |
3 实现测试时发现了原示例中存在内存泄漏的可能,如下代码所示。
1 | Value *IfExprAST::codegen() { |
上面代码片段中,ElseBB和MergeBB创建后没有立即挂入function的链表中。如果函数在类似于ThenV的异常流程中return了,则没有人能释放掉这两个指针了。我们的实现中也有类似问题。
要比较完整的修复该问题,有两个点需要同时考虑:
首先,BB创建后都立即挂到Function中去是否会有不良影响?如果没有,创建就挂上是最好的解决方案;
如果不能立即挂上,除了在函数内考虑释放资源外,还要考虑 ir_builder.CreateBr(merge_bb);等语句在异常发生时可能引用悬空指针的问题。需要重排对这些指针的引用,将其都放到末尾。
扩展问题
1 使用CreateAlloca在function的头部创建栈空间,是否会导致不必要的额外栈空间占用?
使用如下的c语言片段进行了测试。发现clang生成代码时也会把alloca都放到头部,并且生成的x86-64和mips64的代码也都是一次在头部把sp预留够。
这里的权衡可能是动态扩展栈变量无法节约多少内存,但是会浪费操作sp的指令,另外也使得分析stack frame变得更为困难,得不偿失。
1 | extern int x ; |
实现中遇到的问题
1 map 的operator[]会改写原map
在改写for代码生成流程中,named_var map 记录和恢复idt var的部分时,
注意到了下面一对代码。
1 | auto old_val = named_var[idt_name]; |
这段逻辑是直接从原示例中拷贝过来的。
重构时注意到named_var[idt_name]的初始值问题。
当idt_name这个key不在map中时,读取其value的语义是比较模糊的。
参考https://en.cppreference.com/w/cpp/container/map/operator_at , 发现[]这个operator竟然会静默的insert,即使这个operator是用在取右值的动作中。
用下面的示例,可以较为直观展示出这个出人意料的行为。
1 |
|
这个程序的输出如下。
1 | size before access:0 |
可以看到使用[]访问map时,确实会有insert的动作,并且当key不在map中时,返回的vale是一个默认初始化的值。
在这样的语义下,原示例的代码片段虽然不会导致严重的逻辑错误,但是仍有两个明显的问题。
第一,引入了冗余的insert动作,拖慢了编译器工作速度。
第二,在退出清理map时,如果初始key不存在就不会erase。这样一来map中会残留一个错误的映射项目idt_var –> nullptr。后续流程如果直接使用count这类存在性接口去测试,会得到错误的结果。这是一个潜在的错误来源。
2 std::move作用于const vector不生效
如下测试代码
1 |
|
上面程序的输出为。
1 | org:0x7f39d2d5f010 |
==可以看出按值传参时需要在调用点和内部都用move才能避免内存分配。==
==按引用传参时,只需在子函数内move即可。==
==而以const 引用传参时,move不会生效,并且也不会有任何告警。==
3 如何使用string_view作为key来访问string为key的map
需要使用map<string, int, less<>>这样的方式建立map,否则find必须使用string做key。
当使用map<string, int, less<>> 建立map时,调用find实际上是把string_view透传到std:less这个模板中。
后续在find的过程中,less会把map中每一个待比较的string转为string_view,然后进行两个string_view的比较。
如果是调用find的时候,把string_view先转为string再传入,则find内部就不再需要构建临时object。
使用下面的示例进行对比测试,使用string_view的版本由于有string转string_view的过程,其速度要略微慢于string的版本。下面是g++-7 -O2编译的结果。性能差距在1%以内。
./t3 string_view
102400
Time taken by function: 1254672 microseconds
./t4 string版本
102400
Time taken by function: 1243754 microseconds
1 |
|
ELFGO 编译和测试
编译过程
下载代码
从https://github.com/pytorch/ELF 下载代码
在ELF的根目录下git submodule sync && git submodule update --init --recursive获取第三方代码
使用docker方式进行构建
为减少对本地系统的冲击,可以使用docker 方式构建。
先进入ELF的根目录。
在编译前,需要对Dockerfile做一些小调整。
在原来的conda install后新增如下一行
RUN conda install pytorch torchvision cudatoolkit=10.0 -c pytorch
然后使用如下命令进行构建 (为了下载提速,挂了代理)
sudo docker build --network host --build-arg HTTP_PROXY=socks5://127.0.0.1:1080 --build-arg HTTPS_PROXY=socks5://127.0.0.1:1080 -t elf_go:vtrunk .
运行时问题解决
启动制作好的image,拷贝到host中,以便使用GPU
sudo docker images 查看刚刚构建出的镜像id
启动如下的示例命令启动image,然后将我们需要的内容拷贝到host上
1 | sudo docker run -dit --name elf_go ${your_image_id} |
准备host机的路径
由于docker构建时使用了root用户,构建出的binary依赖了一些根路径下的目录。有很多办法可以解决这类问题。
治本的方案是修改编译体系,去除这些对根目录的依赖,将一个用户可控制目录作为根目录(或者使用相对路径)。但是修改的工作量比较大。
规避的办法也有一些,使用patchelf可以修改一些搜索路径,也可以考虑使用fakeroot等技术重定向文件访问的路径。修改的工作量还是略大。
这里用最粗暴的方法,用mount bind来建立一个满足访问要求的映射路径。
先行建立所需的路径。
1 | sudo mkdir -p /root/miniconda3 |
进入到我们拷贝路径${your_host_elf_dir},使用如下命令完成映射
1 | sudo mount -o bind ./miniconda3/ /root/miniconda3 |
最后设置搜索路径
1 | export PATH=$PATH:/root/miniconda3/bin |
下载v2版本模型
主线elf版本只能使用v2版本的模型,Dockerfile中下载的v0版本无法使用。可从
https://dl.fbaipublicfiles.com/elfopengo/pretrained_models/pretrained-go-19x19-v2.bin 下载后,放入${your_host_elf_dir}/go-elf/ELF中。
适配python版本
elf构建时docker内使用的python版本可能和host中的不一致。需要修改/go-elf/ELF/scripts/elfgames/go/gtp.sh中的启动命令,确保使用(dockerfile构建时)conda中安装的python版本。
1 | majiang@majiang-All-Series:~/hd/opensource/ELF_GO/builded_env/go-elf/ELF/scripts/elfgames/go$ git diff gtp.sh |
如果出现python版本不匹配问题,可能出现一些莫名其妙的模块找不到错误,如下所示。
1 | ModuleNotFoundError: No module named 'torch._C' |
其原因是,python的动态库对python版本有依赖,参考https://github.com/pytorch/pytorch/issues/574。host机器的python3如果不是conda所用的python3.7就会出现找不到库问题。
启动elf自带的脚本
1 | cd /go-elf/ELF/scripts |
解决代码问题
v.pin_memory()处的cuda out of memory问题
加了print (v)语句。go-elf/ELF/src_py/elf/utils_elf.py 44行
print (v)
if gpu is not None:
with torch.cuda.device(gpu):
v = v.pin_memory()
v.fill_(1)
似乎是偶发故障,后续没有再复现
启动后使用genmove B开始下棋,utils_elf.py 210出现了数据维度不匹配的问题
genmove B会有异常,经过检查可能是下面utils_elf.py 210出现了数据维度不匹配的问题,修改接口后解决。
1 | #bk[:] = v.squeeze_() |
错误现象
1 | > /go-elf/ELF/src_py/elf/utils_elf.py(192)copy_from() |
GTP协议问题
gtp脚本启动后可以在文本界面下使用GTP协议的指令下棋了。
但是sabaki围棋前端界面无法正常与eflgo进行GTP通信。
查看GTP协议,是ELFGO的额外打印干扰了协议解析。
参考https://github.com/pytorch/ELF/compare/master...Narsil:master,调整了console_lib.py 中的
1 | def print_msg(self, ret, msg): |
启动gtp脚本时,可以添加–loglevel off选项关闭大量额外打印。
封装脚本
为了sabaki等前端能比较方便的启动ELFGO后端,可以使用如下的封装脚本一次完成启动。
1 | !/bin/bash |
遗留问题
在sabaki中,当前elfgo还是只能 执黑。如果执白还是会卡死,可能协议文本的输出还有问题。
另外,说明中提到elfgo只能走贴7.5目的规则。
另一个围棋AI,可以给出胜率估计的leela-zero
下载并解压Lizzie.0.7.2.Mac-Linux.zip,然后进入解压后的目录
1 | export http_proxy="socks5://127.0.0.1:1080" |
hexo升级和插入图片
插入图片失败
参考https://blog.csdn.net/u010996565/article/details/89196612 等文章的方法。
1 | npm install hexo-asset-image --save |
然后在文本中插入
1 | <img src="/images/图片名" width="50" height="50"> |
均无法正确显示图片。
hexo sever启动的终端可以看到如下提示
1 | update link as:-->/.com//xxxx.png |
打开图片的地址也是/.com//xxxx.png。
搜索到如下的网页介绍了类似的问题。
https://blog.csdn.net/xjm850552586/article/details/84101345
但是其提供的方案也无法生效。
考虑到安装 hexo-asset-image时曾经提到需要hexo4.0以上版本,而当前hexo版本是3.9,所以考虑先行升级版本
升级hexo
1 | npm cache clean -f |
2 安装node版本管理工具 n
1 | npm install -g n |
3 安装最新稳定版本的node
1 | n stable |
// 安装最新版本使用 n latest
// 安装指定版本的使用 n {version},例如 n 11.2.0
// 删除指定版本的node使用 n rm {version},例如 n rm 11.2.0
4 更新npm版本
1 | npm install npm@latest -g |
5 进入blog目录,然后执行
1 | npm outdated |
6 根据第5步输出,手工记录各个组件的最新版本号,然后逐一在package.json配置文件中修改版本号到最新版本。
7 更新模块
1 | npm install --save |
8 确认升级结果
1 | hexo -v |
升级后的问题解决
升级后图片仍然无法正常显示。
异常现象变为,路径正常时,网页上就直接显示路径文本,而不是显示图片。
而使用部分博客提到的语法,则生成的网页中直接就是空白(查看html文件,整段内容直接被忽略了)。
1 | {% asset_img slug [title] %} |
考虑到部分博客提到hexo4.0把很多插件并入了核心代码,担心插件中有冲突,于是使用下面的命令卸载掉第三方插件。
1 | npm remove hexo-asset-link --save |
然后再在文件中使用markdown的语法,就能正确显示图片了。
并且主页中的图片也能正常显示,应该是hexo4.0把image link并入核心代码后做了改进(pacakge.json中显示多了一个hexo-image-link组件),解决了markdown语法的问题。
1 | ![Alt Text](图片文件 "Title Text") |
Kaleidoscope user defined operator
学习内容
完成对 https://llvm.org/docs/tutorial/MyFirstLanguageFrontend/LangImpl06.html 的学习。
练习
看懂原网页后,实现同样的功能。
扩展
1 命名时没有限制operator是否能是常规字符(如abc,123等),会否产生静默的关键字冲突?
2 考虑如何处理两个特殊字符组成的操作符,如== !=等。
3 示例中的自定义operator是否能作为库使用?如果不能应该如何改进?
进展
完成了原示例自定义binary和unary operator的功能,但是未支持用户定义”unary-“。
尚未支持unary-的原因是为了阻止用户犯错,我们禁止了用户定义使用保护字符(包括内置运算符、分隔符等)的operator。这里的检查规则比较简单,仅仅实现了基于char的比较,实际上过于严格,以至于阻止了用户定义unary-这样的合法要求。后续细化保护规则后(拆分运算符和受保护的字符两种类型,对运算符区分binary和uary类型),即可支持unary-。
实现
实现过程中发现原示例确实精炼,仅用很少的代码便实现了用户自定义operator的功能。但是,其精炼的代价是该功能仅仅具有演示价值而几乎没有实用价值。
例如,原示例没有在lexer做token分层,parser会直接读取输入的char。这样一来lexer可以非常非常简单。并且,在新增语法功能时,parser由于可以直接穿透处理char,需要新增的配合代码也很少。
但是这样的设计导致代码维护困难,parser中要直接处理token拆分逻辑,一旦语法变复杂,新增功能或更改已有逻辑(直接操作char的地方会很多)就会很困难。
另外,原示例没有错误防护的逻辑,这会导致用户犯错很难定位原因。
实现过程中针对前面扩展中提出的问题,一一进行了改进。详细情况可参考扩展问题一节中的描述。
从实现后的实际情况看,改进引入了相当多的额外代码,工作量较大。作为示例,相较于引入大量的细节流程,原作者使用精简的方案确实是更好的权衡(示例丧失了扩展为真正可实用编译器的潜力,但仍然展现了主要的原理,解决了核心问题)。
扩展问题
1 命名时没有限制operator是否能是常规字符(如abc,123等),会否产生静默的关键字冲突?
原示例几乎没有对自定义operator做错误防护。这将导致用户错误定义operator,很难察觉和定位到根因。
如下所示,覆盖内置操作符的优先级和语义将导致二义性和混乱。
1 | ready> def binary + 80 (a b) a+b; |
定义保留字符’,’,parser无法理解。
1 | ready> def unary , (a) a+1; |
定义保留字符’(‘,parser无法理解。
1 | ready> def unary ( (a) a+1; |
为了避免这些错误,在实现时专门针对binary/unary的名称做了正确性校验(verify_operator_sym函数)。
在错误第一现场阻止错误,并给出清晰的提示。
2 考虑如何处理两个特殊字符组成的操作符,如== !=等。
原示例由于使用了char来作为operator的opcode,实际上就无法支持两字符操作符。但使用单char作为operator的opcode,大大简化了构建operator token的难度。因为只需要在parser中按需读出一个char就可以,无需考虑在lexer中如何正确切分的问题。
为了支持!= 和==等显然有意义的两字符operator,本次实现时在lexer中添加了一个map查找机制,如下所示。
1 | if (cur_token == TOKEN_BINARY || cur_token == TOKEN_UNARY) |
install_user_defined_operator函数,在binary和unary关键字后识别并注册(就是插入到map中)用户自定义的operator token。
get_user_defined_operator中就可以查找已经注册的用户自定义operator,并给出正确的token类型(用户自定义unary或者binary)。
3 示例中的自定义operator是否能作为库使用?如果不能应该如何改进?
从实现原理上来说,因为operator 也是在proto中解析的,与函数一样。
所以自定义的operator只要遵循先extern申明,再引用的规则,就可以以库的方式正常使用(定义在单独的库文件中,使用时链接二进制文件)。
但是,原示例没有考虑容错问题。
parser在工作时是按照extern声明的优先级工作。如果定义时的优先级和extern时的不一致,则程序的逻辑将静默改变。用户要发现这样的问题可能很困难(这个和C中没有正确包含头文件的情况类似)。
为了解决这个问题,参考C++的方法,把prio作为一个强制信息加入到binary/unary函数的名称中。这样一旦extern声明和def的定义不一致,链接时会找不到函数定义。用户将不能得到可工作(但逻辑错误)的二进制文件。
实现中遇到的问题
1 prototype_tab的key从string切换到string_view后,遇到了内存数据错误的问题。详细记录如下。
使用string_view作为map的key时,我们调用了tab.insert(make_pair(a_string, my_val));。原以为string会自动转为string_view,tab中的key(string_view)数据应该指向a_string。实际发现key的数据指向了一个insert所在的函数栈。
使用下面的小型测试用例即可复现出问题,pair中的key并不是指向我们希望的全局数据。
1 |
|
使用g++ save-temps -fdump-tree-all-details -std=c++17编译上面的代码,可以比较清楚地看出问题。
这里的核心错误在于:
make_pair是一个模板函数,(为了提供泛型能力)它并不知道(或者提供这个约束)第一个参数应该是string_view。当我们提供一个string作为第一个参数时,test_view(make_pair(global,1));的调用并没有变成我们希望的test_view(make_pair((string_view)global,1))。而是变成了如下的序列。
tmp_pair = make_pair<string, int>(global, 1);
tmp_pair_to_call = pair<string_view, int> (tmp_pair);
test_view(tmp_pair_to_call);
这样传到test_view中pair中的string_view实际上指向栈上创建的临时变量。一旦tmp_pair所在的函数return,string_view中的key就乱掉了(use after free)。
从这次经历看string_view确实是非常容易出错的点,使用时需要特别谨慎。参考 https://alexgaynor.net/2019/apr/21/modern-c++-wont-save-us/ 的如下示例,string_view允许指向临时对象,这确实很容易导致错误,并且很难发现。
1 | int main() { |
2 切换到clang8编译器后,优化LLVM-IR时程序段错误。
google搜索到了下面的邮件,这是一个gcc/clang的abi兼容性问题。
http://lists.llvm.org/pipermail/llvm-dev/2019-January/129567.html
需要将llvm库用clang重新编译(或者升级到最新的llvm)。
升级到最新的llvm代码后确实解决了该问题。
3 升级到最新的llvm后,遇到了asan报大量indirect leak问题。
仔细检查代码后,发现llvm_optimizer::optimize_function等函数中,
使用了createTargetMachine创建的TargetMachine *。
添加了delete TargetMachine *的语句后,leak告警消失。
老版本为何没报leak,暂时没有进一步核查。
Kaleidoscope control flow
学习内容
完成对 https://llvm.org/docs/tutorial/MyFirstLanguageFrontend/LangImpl05.html 的学习。
练习
看懂原网页后,实现同样的功能。
扩展
原示例中for引入了循环变量,但是仍然没有引入scope之类的概念,而是直接将所有变量名称放到function的NamedValues map(我们的实现是cur_func_args)中。这个是否会产生命名冲突,是否一定需要建立分级的命名查找表?
进展
完成了原示例的if和for添加。
实现过程中发现原示例的for逻辑有问题,参考if的写法做了重写。
实现
大部分语言使用’,’ 作为顺序求值的符号,后续Kaleidoscope也会增加多语句的支持。因此实现时将原示例中的for表达式中的分隔符从”,” 改为了”:”,将”,”预留出来。
实现时的主要变更是改写了for的语义,具体情况如下:
原示例中的for循环展开时,是先执行完成loop后再做结束检查,这和通常语言的for定义都冲突。
使用原示例的代码,如下循环仍然会打印出数字1
1 | def forWrong() |
这很明显不是多数人理解的for语义。
实现for的IR生成代码时,重写了其逻辑,修改后的逻辑框架如下:
1 | preheader_bb: |
修改后的for工作逻辑与c等语言保持了一致。
扩展问题
在展开for表达式时,原示例代码先保存了for指示变量可能会覆盖的变量名,离开for展开流程时,再恢复原始值(如果没有重名,那就把for定义的指示变量从名称查找map named_var中移除)。这样看来,遵循底层覆盖上层的原则,并且不提供访问上层被覆盖值的机制,那么确实无需专门定义scope的概念。
如下示例:
1 | def xt(i) |
即使在for表达式后,还可以添加其他语句。那么其他语句也可以正常访问到入参i的值。
Kaleidoscope optmizer
学习内容
完成对 https://llvm.org/docs/tutorial/MyFirstLanguageFrontend/LangImpl04.html 的学习。
练习
看懂原网页后,实现同样的功能(但将原示例的JIT方式改为传统编译流程)。
扩展
1 在dump出LLVMIR后,能否使用编译好的llvm组件实现编译运行等功能?应该如何做?
2 编写一个driver封装(可以使用shell),把main函数作为入口编译成可执行程序
进展
完成了原示例中的优化部分,删掉了原示例中的JIT部分。
实现
实现了如下的主要变更:
1 删掉了JIT功能,原实例中的扩展库函数没有实际意义也删掉了(有extern语法就肯定能扩展,与JIT功能无关)
2 新增了模块级别的优化功能,可以对单个源代码文件进行整体优化(如inline等)
3 将Passmanager的实现从示例的legacy切换到了新的实现上
4 实现了输入文件的编译
扩展问题
1 玩具前端codegen到出LLVM-IR后,可以使用LLVM提供的独立opt工具进行优化,优化后的输出可以是LLVM-IR的文本形式(-S参数)也是可以bitcode。然后再使用clang编译成二进制文件,就可以进行通常的链接了。也就是说,如果不介意多一次IR导出和IR导入的性能损耗,其实优化是完全可以不必在toy_compiler中实现的。
2 编写了一个compiler.sh,可以把入参文件编译为可执行文件(需要提供main函数)。添加了一个kout函数专门用于打印函数返回值。