热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

node插件rust(rust代码里引入python)

编程语言之争是开发者们热议的永恒话题,在不同语言的选择和设计决定上也都观点不一。那么在面对大型项目时该如何选择具体实现呢?本文的作者借课程项目之机,比较了Rust、Haskell、


编程语言之争是开发者们谈论的永恒话题,根据语言的选择和设计决定,观点也不同。 那么,面对大型项目时,该如何选择具体的实现呢? 本文作者借课程项目的机会,比较了用Rust、Haskell、OCaml、c、Python、Scala等语言编写的编译器的差异,结果发现这些语言在代码量和功能的实现上千差万别




以下是译文:


滑铁卢大学的最后一个学期选择了CS444 :编译原理这门课程。 课程项目是编写编译器,将Java语言的子集编译成x86代码,由3人分组,自由选择语言。


这是一个难得的机会,我可以在同一个大型项目下比较不同的实现,而且我朋友们的水平也和我很接近,所以可以利用这个机会看到不同的设计和语言选择。 我从这个项目得到了很多心得。 这个比较并不完美,但总比只从个人角度比较编程语言的人好。


我们的编译器是用Rust写的。 首先,我们与另一组使用Haskell的人进行了比较。 我认为他们的编译器应该更简洁,但是实际的代码行数很少。 使用OCaml与其他团队的比较也得到了同样的结果。 然后和使用c的团队进行比较,正如预想的那样,由于有头文件,以及缺乏集约型和模式匹配的支持,他们的编译器增长了30%。 下面是与我朋友的Python实现的比较。 他的代码量不到我们的一半。 这要归功于元编程和动态类型。 另一个朋友的团队使用的是Scala,实现的编译器代码量也比我们少。 最令人惊讶的比较是与另一个使用相同Rust的团队的比较。 他们的代码量是我们的三倍。 由于他们采用了不同的设计决定,它最终导致了同样功能所需代码的批量生产上的巨大差异!


本文首先说明这次比较的含义,介绍各个项目的基本情况,然后说明编译器大小差异的部分原因。 最后,谈谈从各自的比较中学到的东西。


比较的意思


你可能认为代码行数(我同时比较了代码行数和字节数)是很糟糕的度量,但我认为这个度量在这个项目中可以提供有用的信息。 至少代码行数应该是不同团队在同一个大项目中工作时最可控的变量。


在我们的项目完成之前,包括我在内,没有人知道我会计算代码的行数,所以没有人修改行数的测量。 每个人都尽了最大努力迅速准确地完成了项目。 每个人(除了后面提到的使用Python的项目外)都实现了相同的程序。 由于目的只是在同一截止日期之前通过同一个自动化测试套件,所以一个组也不会尝试解决不同的问题,也不会尝试解决更难的问题。 各小组在这个项目上花了几个月。 大家正在逐步增加功能,通过已知和未知的测试。 这意味着代码清晰易读,没有巧妙之处。


通过的课程测试以外的代码不用于其他用途,也不允许任何人阅读。 此外,由于只能编译Java语言的子集,因此也没有其他用途。 不能使用标准库以外的库。 此外,如果标准库不包含此功能,则也无法使用帮助分析的库。 这意味着只有一些团队使用的强大的编译器库也不会干扰比较。


最终提交截止日期后,将执行秘密测试。 我看不到这个测试。 也就是说,通过自己编写测试用例测试代码,可以保证编译器的强健性和准确性,也可以应对边界情况。


虽然参加的每个人都是学生,但我讨论的这些团队都是我认为非常优秀的程序员。 每个人都有至少两年的全职实习经验,大部分在高端科技公司,有些公司开发编译器。 大多数人有7-13年的编程经验,热衷于在线阅读课程以外的东西。


虽然没有聚集自动生成的代码,但是聚集了生成的语法文件和代码。


因此,关于各个项目花费的劳力,以及如果是长期项目的话,维护需要多少劳力,我认为代码量是很好的近似统计。 我认为微小的差异也反映了很大的问题。 例如,如上所述,用Haskell编写的编译器代码量不到c的一半。


Rust (比较基准) )

tps://p3.toutiaoimg.com/origin/dfic-imagehandler/398d458c-cfbe-4f50-9997-e8370cb773c3?from=pc">

我和团队里的另一名成员以前分别写过1万多行的Rust代码,另一个成员在某次编程马拉松项目上写过大约500行Rust。我们的编译器用wc -l统计的结果是6806行,其中包括5900代码行(不包括空行和注释),wc -c的结果为220kb。

我发现的一个问题是,这几项度量的比例在其他项目中也是相似的,只有一些微小的差异(过会儿我会介绍)。下文中提到代码行数时,我指的都是wc -l的结果,但上述结论表明,代码行数按照哪个规则进行统计其实是无所谓的(除非特别指出),你可以通过比例进行换算。

我写过另一篇关于设计的文章(http://thume.ca/2019/04/18/writing-a-compiler-in-rust/),这个设计通过了所有公开和秘密的测试。它还包括几个额外的特性,这些特性我们仅仅是出于兴趣而开发,并没有想着通过测试。这些特性大概占用了400行。我们总共的单元测试和测试用的代码大约占了500行。

Haskell

Haskell团队由我的两个朋友组成,他们每个人大概写过几千行Haskel,还阅读过许多网上的Haskell内容,以及许多其他类似的语言,如OCaml和Lean。他们还有另一个我不太熟悉的团队成员,但似乎是个很厉害的程序员,以前也用过Haskell。

他们编译器的wc -l结果是9750行,357kb,7777 SLOC(源代码行数)。这个团队的度量比例的差别也最大,他们的编译器中行数为1.4倍,SLOC为1.3倍,字节数为1.6倍。他们并没有实现任何额外功能,但通过了所有公开和秘密的测试用例。

需要指出的重要的一点是,只有把测试用例统计在内,对这个团队才公平,因为他们的代码是最正确的,包含了1600行测试用例,并且捕获了好几个团队未能捕获的边界情况,只不过是课程提供的测试用例没有覆盖到这些边界情况而已。所以,如果两者都不统计测试用例的话,他们的代码是8.1k行,我们的是6.3k行,仅是我们的1.3倍。

为了让度量更合理,我还统计了字节数,因为Haskell项目平均每行要更长,而且没有许多只有结束括号的行,它的单行函数也不会被rustfmt分解成多行。

在与团队里的另一个朋友深入挖掘了代码大小的问题后,我们找到了以下理由来解释代码大小的差异:

我们采用了手写的词法分析器和递归下降分析(recursive descent parsing),他们采用的是NFA到DFA的词法生成器,以及一个LR分析器,然后再扫描一遍将解析树转换成AST(抽象语法树,是更方便的代码表示形式)。这需要占用更多代码,占了2677行,比我们的1705行大约多了1k行。他们使用的是更漂亮的通用AST类型,能转换成不同的类型参数,因为每次解析都会添加更多信息。这需要更多的辅助函数,因此导致了他们的AST代码比我们的实现多了500行——我们在解析并添加信息时使用的只是结构字面量,和可修改的Option<_>字段。他们大约有400多行代码用于实现更高的抽象程度,从而用纯粹的函数式方式来实现代码生成和组合,而我们是直接修改字符串。

这些差异再加上测试用例的差异,就导致了代码行数的差别。实际上,我们的文件在中间解析阶段(如常量折叠、作用域解析等)的大小跟他们的非常接近。但依然产生了字节数上的区别,原因是行的平均长度,我估计原因是他们需要更多的代码,在每次解析时重写整个树,而我们只需要访问并修改即可。

我认为,考虑到Rust和Haskell的设计决定非常相似,都是表达性的,只有细微的差异,如Rust在需要时能够很方便地修改变量等。另一点有意思的是,我们选择采用递归下降分析器和手工编写词法分析器给我们带来了回报。虽然这有点风险,因为教授并没有推荐这一点,我是自学来的,但我发现它很易于使用,是个正确的决定。

我认为,这个团队可能并没有开发出Haskell的全部潜力。如果他们能更善于使用Haskell,他们的代码应该行数更少。我相信,像Edward Kmeet之类的人可以使用更少的Haskell代码就能编写出同样的编译器,从这一点上来说,我朋友的团队并没有使用太多超高级的抽象,而且他们也不允许使用更好的组合库,如lens等。但是,这样做的代价就是理解编译器的难度。团队的成员都是有经验的程序员,他们知道Haskell可以做非常漂亮的事情,但还是决定不这样做,因为他们认为,这样做花费的时间会超过节省的时间,而且会让代码变得难以理解。在我看来这的确是个正确的选择,用“魔法”的方式使用Haskell编写编译器,会产生“Haskell写编译器的门槛非常高,如果你不考虑对于不太了解Haskell的人的可维护性的话”的结果,而这种结果并不是我们想要的。

另一个有趣的发现是,教授在开始时说过,学生可以选择任何能够在学校服务器上运行的语言,但同时针对Haskell提出了警告,说过去使用Haskell的团队的分数的方差是最高的,因为许多选择Haskell的团队都高估了他们的Haskell能力,导致他们的得分比选择其他语言的团队低得多,也有另一部分Haskell团队像我朋友那样做得非常完美。

C++

接下来我与另一个在团队中使用了C++的朋友进行了交谈。那个团队中我只认识这一个人,但由于滑铁卢大学中使用C++的课程非常普遍,所以估计团队中的每个人都有C++经验。

他们的项目代码行数为8733,字节数为280kb,这些数字不包括测试代码,但包括大约500行的额外功能。与我们不含测试的代码(也包含500行的额外功能)相比,他们的代码行数为1.4倍。他们通过了100%的公开测试,但仅通过了90%的秘密测试,很可能是因为它们没有实现项目要求的数组vtable,这个功能需要大约50-100行代码实现。

我并没有深入挖掘代码差异的原因,我感觉最有可能的解释为:

他们使用了LR解析器和树重写,而没有采用递归下降分析器;C++缺乏汇总类型和模式匹配这两个非常常用的功能;他们需要重复头文件中所有的函数签名,而Rust不需要这样做。

我们比较的另一件事是编译时间。在我的笔记本上,我们的编译器的调试版完整编译需要9.7秒,调试版增量编译需要3.5秒。我的朋友并没有给出他们的C++编译器的构建时间(采用并行make),但说我提供的数字与他们的非常接近,而且说他们把一些常用的小函数的签名放到了头文件中,以增加编译时间为代价来减少函数签名的重复(也正是由于这个原因,我没有办法比较单纯的头文件代码行数)。

Python

我的一位朋友是非常优秀的程序员,她选择使用Python独立完成项目。她还比其他团队多实现了好几个额外功能,包括带有寄存器分配的SSA立即表示,还有其他优化。另一方面,由于她是独立完成的,而且实现了许多额外的功能,因此她在代码质量上只花费了最小限度的经历,例如所有错误都会抛出统一的异常(所以调试时需要进行栈跟踪),而不是像我们一样每种错误都给出特定的错误类型和错误信息。

她的编译器只有4581行,并且通过了所有公开测试和秘密测试。她实现的功能比所有其他团队都多得多,但很难确定那些功能占了多少行代码,因为许多额外功能与每个人都在做的功能都相同,比如常量折叠、代码生成等,但功能却更强大。额外的功能估计至少占用了1000~2000行,所以我很确信她的代码的表达性要比我们至少高两倍。

造成这种差异的最大原因很可能是动态类型。我们的ast.rs中类型定义就占了500行,编译器的其他部分还有更多的类型定义。我们还通过类型系统做了各种类型限制。例如,我们需要基础设施,才能在分析代码过程中向AST中添加信息供以后使用,而Python中只需要给AST结点添加新的域即可。

强大的元编程也是造成差异的原因之一。例如,尽管她用的是LR分析器而不是递归下降分析器,但她的项目代码量更小,因为她不需要进行树重写的过程,而是在LR语法中加入了Python代码片段来构建AST,而生成器可以直接利用eval变成Python函数。我们没有采用LR分析器的部分原因是,不使用树重写来构建AST需要大量的代码(生成的Rust文件或过程式的宏)将语法绑定到Rust代码片段上。

元编程和动态类型的强大之处的另一个例子是,我们有个名为visit.rs的文件有400行,里面大部分是重复性的样板代码,仅为了实现在各种AST结构上的访问。在Python中只需要一个大约10行的函数即可递归地访问AST结点的各个域(通过__dict__属性)。

作为Rust和静态类型语言的爱好者,我需要指出,类型系统非常有助于避免bug和提高性能。强大的元编程同时会让代码更难理解,但是,这个比较结果依然让我非常惊讶,我没想到代码的差异能有如此之大。如果差异真的导致需要写两倍的代码,那我依然认为Rust的付出是值得的,但两倍的差异的确不可忽视,我以后会考虑在独立完成某项工作中的一次性代码时使用Ruby或Python。

关注,转发,私信小编“01”免费领取Python学习资料!


推荐阅读
  • Android源码深入理解JNI技术的概述和应用
    本文介绍了Android源码中的JNI技术,包括概述和应用。JNI是Java Native Interface的缩写,是一种技术,可以实现Java程序调用Native语言写的函数,以及Native程序调用Java层的函数。在Android平台上,JNI充当了连接Java世界和Native世界的桥梁。本文通过分析Android源码中的相关文件和位置,深入探讨了JNI技术在Android开发中的重要性和应用场景。 ... [详细]
  • 微软头条实习生分享深度学习自学指南
    本文介绍了一位微软头条实习生自学深度学习的经验分享,包括学习资源推荐、重要基础知识的学习要点等。作者强调了学好Python和数学基础的重要性,并提供了一些建议。 ... [详细]
  • Nginx使用(server参数配置)
    本文介绍了Nginx的使用,重点讲解了server参数配置,包括端口号、主机名、根目录等内容。同时,还介绍了Nginx的反向代理功能。 ... [详细]
  • Android中高级面试必知必会,积累总结
    本文介绍了Android中高级面试的必知必会内容,并总结了相关经验。文章指出,如今的Android市场对开发人员的要求更高,需要更专业的人才。同时,文章还给出了针对Android岗位的职责和要求,并提供了简历突出的建议。 ... [详细]
  • 本文介绍了OC学习笔记中的@property和@synthesize,包括属性的定义和合成的使用方法。通过示例代码详细讲解了@property和@synthesize的作用和用法。 ... [详细]
  • 本文介绍了RPC框架Thrift的安装环境变量配置与第一个实例,讲解了RPC的概念以及如何解决跨语言、c++客户端、web服务端、远程调用等需求。Thrift开发方便上手快,性能和稳定性也不错,适合初学者学习和使用。 ... [详细]
  • 拥抱Android Design Support Library新变化(导航视图、悬浮ActionBar)
    转载请注明明桑AndroidAndroid5.0Loollipop作为Android最重要的版本之一,为我们带来了全新的界面风格和设计语言。看起来很受欢迎࿰ ... [详细]
  • 本文介绍了作者在开发过程中遇到的问题,即播放框架内容安全策略设置不起作用的错误。作者通过使用编译时依赖注入的方式解决了这个问题,并分享了解决方案。文章详细描述了问题的出现情况、错误输出内容以及解决方案的具体步骤。如果你也遇到了类似的问题,本文可能对你有一定的参考价值。 ... [详细]
  • 闭包一直是Java社区中争论不断的话题,很多语言都支持闭包这个语言特性,闭包定义了一个依赖于外部环境的自由变量的函数,这个函数能够访问外部环境的变量。本文以JavaScript的一个闭包为例,介绍了闭包的定义和特性。 ... [详细]
  • 海马s5近光灯能否直接更换为H7?
    本文主要介绍了海马s5车型的近光灯是否可以直接更换为H7灯泡,并提供了完整的教程下载地址。此外,还详细讲解了DSP功能函数中的数据拷贝、数据填充和浮点数转换为定点数的相关内容。 ... [详细]
  • 本文介绍了lua语言中闭包的特性及其在模式匹配、日期处理、编译和模块化等方面的应用。lua中的闭包是严格遵循词法定界的第一类值,函数可以作为变量自由传递,也可以作为参数传递给其他函数。这些特性使得lua语言具有极大的灵活性,为程序开发带来了便利。 ... [详细]
  • 搭建Windows Server 2012 R2 IIS8.5+PHP(FastCGI)+MySQL环境的详细步骤
    本文详细介绍了搭建Windows Server 2012 R2 IIS8.5+PHP(FastCGI)+MySQL环境的步骤,包括环境说明、相关软件下载的地址以及所需的插件下载地址。 ... [详细]
  • 本文介绍了在Linux下安装Perl的步骤,并提供了一个简单的Perl程序示例。同时,还展示了运行该程序的结果。 ... [详细]
  • Java在运行已编译完成的类时,是通过java虚拟机来装载和执行的,java虚拟机通过操作系统命令JAVA_HOMEbinjava–option来启 ... [详细]
  • 如何搭建Java开发环境并开发WinCE项目
    本文介绍了如何搭建Java开发环境并开发WinCE项目,包括搭建开发环境的步骤和获取SDK的几种方式。同时还解答了一些关于WinCE开发的常见问题。通过阅读本文,您将了解如何使用Java进行嵌入式开发,并能够顺利开发WinCE应用程序。 ... [详细]
author-avatar
幽雅闲居xl
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有