学习内容 完成对 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 2 3 4 5 6 7 8 std ::vector <AllocaInst *> OldBindings; OldBindings.push_back(NamedValues[VarName]); NamedValues[VarName] = Alloca; ....body_gen.... for (unsigned i = 0 , e = VarNames.size (); i != e; ++i) NamedValues[VarNames[i].first] = OldBindings[i];
本次实现使用了更安全的find来替代[] operator,同时改进了缓存结构。 通过存储named_var中被shadow变量alloca字段的地址,消除了恢复 named_var时的map查找动作,如下片段所示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 vector <std ::pair<AllocaInst **, AllocaInst *>> saved_name_vec; .... if (auto it = named_var.find (var_name); it != named_var.end ()) { saved_name_vec.push_back( std ::make_pair(&(it->second), it->second)); it->second = var_allocas[i]; } else named_var[var_name] = var_allocas[i]; } ...bodygen.... for (size_t i = 0 ; i < saved_name_vec.size (); ++i) { auto old_alloca_addr = saved_name_vec[i].first; *old_alloca_addr = saved_name_vec[i].second; }
3 实现测试时发现了原示例中存在内存泄漏的可能,如下代码所示。
1 2 3 4 5 6 7 8 9 10 11 Value *IfExprAST::codegen () { .... BasicBlock *ThenBB = BasicBlock::Create(TheContext, "then" , TheFunction); BasicBlock *ElseBB = BasicBlock::Create(TheContext, "else" ); BasicBlock *MergeBB = BasicBlock::Create(TheContext, "ifcont" ); .... Value *ThenV = Then->codegen(); if (!ThenV) return nullptr ;
上面代码片段中,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 2 3 4 5 6 7 8 9 extern int x ;void test () { char buf[1024 ] = {2 }; if (x) { char nb[1024 ]= {1 }; } }
实现中遇到的问题 1 map 的operator[]会改写原map 在改写for代码生成流程中,named_var map 记录和恢复idt var的部分时, 注意到了下面一对代码。
1 2 3 4 5 6 7 auto old_val = named_var[idt_name]; named_var[idt_name] = idt_var; ..... if (old_val != nullptr ) named_var[idt_name] = old_val; else named_var.erase(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 2 3 4 5 6 7 8 9 10 11 12 #include <map> #include <iostream> using namespace std ;int main () { map <int , int *> t1; cout << "size before access:" << t1.size () << endl ; cout << "uninitialized pointer is: " << t1[0 ] << endl ; cout << "size after access:" << t1.size () << endl ; cout << "count after access:" << t1.count(0 ) << endl ; return 0 ; }
这个程序的输出如下。
1 2 3 4 size before access:0 uninitialized pointer is: 0 size after access:1 count after access:1
可以看到使用[]访问map时,确实会有insert的动作,并且当key不在map中时,返回的vale是一个默认初始化的值。 在这样的语义下,原示例的代码片段虽然不会导致严重的逻辑错误,但是仍有两个明显的问题。 第一,引入了冗余的insert动作,拖慢了编译器工作速度。 第二,在退出清理map时,如果初始key不存在就不会erase。这样一来map中会残留一个错误的映射项目idt_var –> nullptr。后续流程如果直接使用count这类存在性接口去测试,会得到错误的结果。这是一个潜在的错误来源。
2 std::move作用于const vector不生效 如下测试代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 #include <iostream> #include <vector> using namespace std ;class testc_ref { public : vector <int > inner; testc_ref(vector <int >& in) : inner(std ::move (in)) {} }; class testc_move { public : vector <int > inner; testc_move(vector <int > in) : inner(std ::move (in)) {} }; class testc_const_ref { public : vector <int > inner; testc_const_ref(const vector <int >& in) : inner(std ::move (in)) {} }; int main () { vector <int > mytest (100000 , 1 ) ; cout << "org:" << &(mytest[0 ]) << endl ; testc_move t1 (mytest) ; cout << "t1:" << &(t1.inner[0 ]) << endl ; cout << &(mytest[0 ]) << endl ; testc_move t2 (std ::move (mytest)) ; cout << "t2:" << &(t2.inner[0 ]) << endl ; cout << &(mytest[0 ]) << endl ; vector <int > mytest1 (100000 , 1 ) ; cout << "org:" << &(mytest1[0 ]) << endl ; testc_ref t3 (mytest1) ; cout << "t3:" << &(t3.inner[0 ]) << endl ; cout << &(mytest1[0 ]) << endl ; vector <int > mytest2 (100000 , 1 ) ; cout << "org:" << &(mytest2[0 ]) << endl ; testc_const_ref t4 (mytest2) ; cout << "t4:" << &(t4.inner[0 ]) << endl ; cout << &(mytest2[0 ]) << endl ; return 0 ; }
上面程序的输出为。
1 2 3 4 5 6 7 8 9 10 11 org:0x7f39d2d5f010 t1:0x7f39d2cfd010 0x7f39d2d5f010 t2:0x7f39d2d5f010 0 org:0x7f39d2c9b010 t3:0x7f39d2c9b010 0 org:0x7f39d2c39010 t4:0x7f39d1e45010 0x7f39d2c39010
==可以看出按值传参时需要在调用点和内部都用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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 #include <map> #include <string> #include <string_view> #include <iostream> #include <chrono> using namespace std ;using namespace std ::chrono; int main () { string_view key = "hello" ; map <string , int , less<>> coll; for (int i=0 ;i < 102400 ;i++) coll.insert(make_pair(to_string(i),i)); cout << coll.size ()<< endl ; long unsigned rep = 1024 *1024 * 10 ; auto re = coll.find (key); auto start = high_resolution_clock::now(); while (rep--) { re = coll.find (key); } auto stop = high_resolution_clock::now(); auto duration = duration_cast<microseconds>(stop - start); cout << "Time taken by function: " << duration.count() << " microseconds" << endl ; return 0 ; }