我曾考虑过如何将指令一个接一个地转换为C ++代码,然后尝试运行该代码。我知道我想将RISC-V转换为一些简单的低级代码,进行编译,然后将其作为共享对象加载。希望它可以提供显着的加速,消除一些仿真开销。 libriscv已经是相当快的仿真器,可以将每个指令解码为单个执行段的函数指针,但是自然要远远落后于本机性能。使用二进制翻译,希望这将使我的母语翻译能力降低到1/4之内,尽管不是第一个版本。
这一切都是从我读著名的Popek& Goldberg谈到了虚拟机的需求,我意识到我至少必须尝试为程序中最重要的部分创建某种有效的代码。
在第一个版本中,我开始将RISC-V指令一个接一个地转换为C ++,这可以直接转换RISC-V机器的状态。对于那些很小的可执行文件,这不是问题。我很快发现,代码块检测确实是一门艺术。我最终停止了每种分支和系统指令。最后,由于许多因素,这将提供很少的加速。一个人还没有支持足够的指令,因为我越来越多地支持。另一个问题是,翻译后的块最终变得太短,增加了输入和离开此代码的开销。最糟糕的是,在较大的程序上,编译时间太可怕了,我依赖于RISC-V模拟器自己项目的标头!
通过仅在分支实际命中时退出已翻译的本机代码,我可以继续翻译更长的块,从而提供更大的加速。我还生成了一个自定义API标头,将其嵌入共享对象中,并释放了不需要的依赖关系。编译时间大大减少,但是C ++解析仍然很复杂。我检查了-ftime-report,除了考虑继续进行C代码生成之外,我无能为力。实际上,我实际上认真地研究了如何减少C和C ++的编译时间,但我发现很多未知的东西:在tmpfs上进行编译,减少头文件,避免使用模板等等。
通过重写整个东西以生成标准C,现在的编译时间已经足够短,可以使该东西对更大的可执行文件更有用。我还完成了所有正常的说明,与未翻译的基准测试相比,我看到了明显的加速。
但是,最大的变化是检测循环!由于无法在循环位置返回本机代码,我无法在最长的时间内比LuaJIT的fib(40)更快。通过扫描负偏移量的分支指令,然后回头为它们生成代码,我可以通过LuaJIT进行正确的飞行。如果正确完成,那当然也就不足为奇了,因为我正在翻译一个优化的可执行文件,而LuaJIT却独自完成了所有事情-很快!实话实说,我之所以与Lua进行比较,仅仅是因为它很容易集成。它是与业余项目进行比较的一个很好的目标!
由于编译可能要花费一些时间,具体取决于程序的大小,因此,如果我们可以稍后将其保存在临时位置,那将是很好的。因此,如果在库中启用该功能,它将对代码和编译器参数进行CRC32校验,并将校验和用作文件名的一部分。然后,我们可以稍后再次找到相同的文件。我们不希望出现一些不起眼的错误,因为一些微小的更改会导致缓存的共享库出现错误,因此,这是避免这种情况的好方法。我将其称为翻译缓存,它是从翻译后的RISC-V编译的共享库。代码生成非常快,只要有广泛的使用,就可以使用C ++ 20的std :: format再次使其更快。由于SSE4.2可用时,我确保使用CRC32-C指令,因此校验和也非常快。
如果您更改的只是一些变量的名称和一些注释,那么看到翻译器忽略您的新二进制文件肯定会很有趣。有时代码是相同的,校验和将反映出这一点。
因此,让我们谈谈仍然存在的,可能永远无法解决的一些障碍。首先,当您扫描要翻译的代码块时,必须使用阈值以最小化块大小。如果您不这样做,则可以坐在那里永久地翻译并增加编译时间,这几乎没有什么好处。
另一个是您愿意翻译的代码块和指令(单独计算)的最大数量。出乎意料的是,该方法效果很好,因为您关心的代码在可执行文件布局中更高!我尝试了多种代码块和指令计数的组合,经过一定数量的尝试后,您一无所有。也许您在启动时就将其恢复了,但是对于我的项目,我并不在乎,特别是如果我在编译阶段使用二进制翻译的时候。
每个寄存器更改不可避免地必须保存在仿真器寄存器文件中。这意味着需要额外的指令,额外的存储,尽管可以通过优化器对其进行改进,尤其是在紧密循环中,但最后只必须提交这些存储,之后大部分存储都将被忽略。
编译器之间的编译时间有很大差异。到目前为止,最快的是-fuse-ld = lld。减少编译翻译代码所花费的时间是目前最大的障碍。现在,小型可执行文件几乎立即完成,但是大型可执行文件的编译时间呈指数级增长。例如,一个带有例外且启用了所有功能的C ++示例程序将花费几秒钟的时间进行编译,并且启用了优化(-O2)。禁用优化(-O0)则更少。我这样做是为了使您可以将CC和CFLAGS传递给托管libriscv的进程来控制这些事情。
另一方面,您可以将其关闭并立即执行。这取决于您的需求。
我敢肯定,你们中的许多人都在想为什么我不只是直接生成程序集。那完全是不可能的。它不是便携式的,要花费一切,可能要花几个月甚至几年的时间。它确实具有几乎即时的链接时间。但是,我会一直怀念全局。如果不重新发明优化轮,我将无法生成最佳装配。优化装配的最常见方法是窥孔优化。取而代之的是,现在有一个可移植的二进制转换解决方案,您可以调整其优化级别。它可以在您的ARM服务器,台式机和手机上使用!对于iOS和Nintendo Switch,您只需关闭二进制翻译即可。
如果二进制翻译的任何阶段失败,则仿真器将清理临时文件并退回常规仿真。我不确定这样做是否正确,但是Machine中有一个布尔型getter来告诉您当前正在运行的程序是否正在使用本机编译的共享库。
实际上,除了坐在该功能分支上一周以查看它是否稳定以及是否可以改善编译时间外,我没有任何计划。我当时正在考虑尝试libtcc,这样我就可以将amd64编译并直接编译到内存中。我已经尝试过使用它来手动编译二进制翻译和加载的输出,但是由于来宾在我测试的每个程序上均失败,因此显然存在一些误编译。不知道我是否有一些不清楚的C代码或TCC是否只是个小虫,但是它编译时没有警告,并且所有公共符号都存在。所以我现在把它放在架子上。我要检查的第一件事是它是否错误编译符号扩展名强制转换,例如:(int64_t)(int8_t)value。或者,当我使用联合来修改浮点值时,如果行为不当。
我仍然可以做很多改进来简化生成的C代码。我尝试了缓存函数序言/结尾,但是我没有看到实际运行时的很多改进,只是重申了C编译器确实很好,或者它的硬件也是如此。我很高兴最终选择了C,因为它是一种您可以根据需要严重减少代码大小的语言,因为这是一开始它的主要目标之一。这就是为什么要使用三元运算符,++和单字符运算符(^)而不是单词(xor)的原因。它非常适合我的用例,并且可以通过减少整体代码大小来改善编译时间。