那我们上 godbolt 看看 .NET 给我们的 Brainfuck 程序产生了怎样的机器代码?
Copy
push rbp push r15 push r14 push r13 push rbx lea rbp, [rsp+0x20] mov rbx, rsi mov r15, r8 movsxd rsi, edi add rsi, rbx add byte ptr [rsi], 49 ; '1' inc edi movsxd rsi, edi add rsi, rbx add byte ptr [rsi], 50 ; '2' inc edi movsxd rsi, edi add rsi, rbx add byte ptr [rsi], 51 ; '3' lea r14d, [rdi-0x02] movsxd rsi, r14d movzx rsi, byte ptr [rbx+rsi] mov rdi, r15 mov rax, qword ptr [r15] mov r13, qword ptr [rax+0x68] call [r13]System.IO.Stream:WriteByte(ubyte):this inc r14d movsxd rsi, r14d movzx rsi, byte ptr [rbx+rsi] mov rdi, r15 call [r13]System.IO.Stream:WriteByte(ubyte):this inc r14d movsxd rsi, r14d movzx rsi, byte ptr [rbx+rsi] mov rdi, r15 call [r13]System.IO.Stream:WriteByte(ubyte):this mov eax, r14d pop rbx pop r13 pop r14 pop r15 pop rbp ret
这不就是
Copy
*(ptr++) = '1'; *(ptr++) = '2'; *ptr = '3'; ptr -= 2; WriteByte(*(ptr++)); WriteByte(*(ptr++)); WriteByte(*ptr);
吗?可以看到我们代码里的抽象全都被 .NET 给优化干净了。
而前面那个不怎么直观的 Hello World! 代码则编译出:
Copy
AddData<8, Loop< AddPointer<1, AddData<4, Loop< AddPointer<1, AddData<2, AddPointer<1, AddData<3, AddPointer<1, AddData<3, AddPointer<1, AddData<1, AddPointer<-4, AddData<-1, Stop>>>>>>>>>>, AddPointer<1, AddData<1, AddPointer<1, AddData<1, AddPointer<1, AddData<-1, AddPointer<2, AddData<1, Loop<AddPointer<-1, Stop>, AddPointer<-1, AddData<-1, Stop>> >>>>>>>>> >>>, AddPointer<2, OutputData<AddPointer<1, AddData<-3, OutputData<AddData<7, OutputData<OutputData<AddData<3, OutputData<AddPointer<2, OutputData<AddPointer<-1, AddData<-1, OutputData<AddPointer<-1, OutputData<AddData<3, OutputData<AddData<-6, OutputData<AddData<-8, OutputData<AddPointer<2, AddData<1, OutputData<AddPointer<1, AddData<2, OutputData<Stop>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
JIT 编译#
如果我们想以 JIT 的形式运行 Brainfuck 代码,那如何在运行时生成类型然后运行代码呢?我们在 .NET 中有完善的反射支持,因此完全可以做到运行时创建类型。
比如根据数字来生成数字类型:
Copy
var type = GetNum(42); static Type GetHex(int hex) { return hex switch { 0 => typeof(Hex0), 1 => typeof(Hex1), 2 => typeof(Hex2), 3 => typeof(Hex3), 4 => typeof(Hex4), 5 => typeof(Hex5), 6 => typeof(Hex6), 7 => typeof(Hex7), 8 => typeof(Hex8), 9 => typeof(Hex9), 10 => typeof(HexA), 11 => typeof(HexB), 12 => typeof(HexC), 13 => typeof(HexD), 14 => typeof(HexE), 15 => typeof(HexF), _ => throw new ArgumentOutOfRangeException(nameof(hex)), }; } static Type GetNum(int num) { var hex0 = num & 0xF; var hex1 = (num >>> 4) & 0xF; var hex2 = (num >>> 8) & 0xF; var hex3 = (num >>> 12) & 0xF; var hex4 = (num >>> 16) & 0xF; var hex5 = (num >>> 20) & 0xF; var hex6 = (num >>> 24) & 0xF; var hex7 = (num >>> 28) & 0xF; return typeof(Int<,,,,,,,>).MakeGenericType(GetHex(hex7), GetHex(hex6), GetHex(hex5), GetHex(hex4), GetHex(hex3), GetHex(hex2), GetHex(hex1), GetHex(hex0)); }
同理也可以用于生成各种程序结构上。
最后我们只需要对构建好的类型进行反射然后调用Run方法即可:
Copy
var run = (EntryPoint)Delegate.CreateDelegate(typeof(EntryPoint), type.GetMethod("Run")!); run(0, memory, input, output); delegate int EntryPoint(int address, Span<byte> memory, Stream input, Stream output);
AOT 编译#
那如果我不想 JIT,而是想 AOT 编译出来一个可执行文件呢?
你会发现,因为编译出的东西是类型,因此我们不仅可以在 JIT 环境下跑,还能直接把类型当作程序 AOT 编译出可执行文件!只需要编写一个入口点方法调用Run即可:
Copy
using HelloWorld = AddData<8, Loop< AddPointer<1, AddData<4, Loop< AddPointer<1, AddData<2, AddPointer<1, AddData<3, AddPointer<1, AddData<3, AddPointer<1, AddData<1, AddPointer<-4, AddData<-1, Stop>>>>>>>>>>, AddPointer<1, AddData<1, AddPointer<1, AddData<1, AddPointer<1, AddData<-1, AddPointer<2, AddData<1, Loop<AddPointer<-1, Stop>, AddPointer<-1, AddData<-1, Stop>> >>>>>>>>> >>>, AddPointer<2, OutputData<AddPointer<1, AddData<-3, OutputData<AddData<7, OutputData<OutputData<AddData<3, OutputData<AddPointer<2, OutputData<AddPointer<-1, AddData<-1, OutputData<AddPointer<-1, OutputData<AddData<3, OutputData<AddData<-6, OutputData<AddData<-8, OutputData<AddPointer<2, AddData<1, OutputData<AddPointer<1, AddData<2, OutputData<Stop>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>; static void Main() { HelloWorld.Run(0, stackalloc byte[16], Console.OpenStandardInput(), Console.OpenStandardOutput()); }
然后调用 AOT 编译:
Copy
dotnet publish -c Release -r linux-x64 /p:PublishAot=true /p:IlcInstructionSet=native /p:OptimizationPreference=Speed
上面的/p:IlcInstructionSet=native即 C++ 世界里的-march=native,OptimizationPreference=Speed则是-O2。
运行编译后的程序就能直接输出Hello World!。
性能测试#
这里我们采用一段用 Brainfuck 编写的 Mandelbrot 程序进行性能测试,代码见 Pastebin。
它运行之后会在屏幕上输出:
这段程序编译出来的类型也是非常的壮观: