Brainfly: 用 C# 类型系统构建 Brainfuck 编译器
<h2 id="brainfuck-简介">Brainfuck 简介</h2><p>Brainfuck 是由 Urban Müller 在 1993 年创造的一门非常精简的图灵完备的编程语言。</p>
<p>正所谓大道至简,这门编程语言简单到语法只有 8 个字符,每一个字符对应一个指令,用 C 语言来描述的话就是:</p>
<table>
<thead>
<tr>
<th>字符</th>
<th>含义</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>></code></td>
<td><code>++ptr</code></td>
</tr>
<tr>
<td><code><</code></td>
<td><code>--ptr</code></td>
</tr>
<tr>
<td><code>+</code></td>
<td><code>++*ptr</code></td>
</tr>
<tr>
<td><code>-</code></td>
<td><code>--*ptr</code></td>
</tr>
<tr>
<td><code>.</code></td>
<td><code>putchar(*ptr)</code></td>
</tr>
<tr>
<td><code>,</code></td>
<td><code>*ptr = getchar()</code></td>
</tr>
<tr>
<td><code>[</code></td>
<td><code>while (*ptr) {</code></td>
</tr>
<tr>
<td><code>]</code></td>
<td><code>}</code></td>
</tr>
</tbody>
</table>
<p>然后只需要提供一个已经初始化为 0 的字节数组作为内存、一个指向数组的指针、以及用于输入输出的两个字节流就能够让程序运行了。</p>
<p>比如 Hello World! 程序就可以写成:</p>
<pre><code class="language-bf">++++++++++[>+++++++>++++++++++>+++>+<<<<-]
>++.>+.+++++++..+++.>++.<<+++++++++++++++.
>.+++.------.--------.>+.>.
</code></pre>
<h2 id="c-类型系统入门">C# 类型系统入门</h2>
<p>既然要用 C# 类型系统来构建 Brainfuck 的编译器,我们需要首先对 C# 类型系统有一些认知。</p>
<h3 id="泛型系统">泛型系统</h3>
<p>C# 的类型系统构建在 .NET 的类型系统之上,而众所周知 .NET 是一个有具现化泛型的类型系统的平台,意味着泛型参数不仅不会被擦除,还会根据泛型参数来分发甚至特化代码。</p>
<p>例如:</p>
<pre><code class="language-cs">class Foo<T>
{
public void Print() => Console.WriteLine(default(T)?.ToString() ?? "null");
}
</code></pre>
<p>对于上面的代码,调用 <code>new Foo<int>().Print()</code> 会输出 <code>0</code>,调用 <code>new Foo<DateTime>().Print()</code> 会输出 <code>0001-01-01T00:00:00</code>,而调用 <code>new Foo<string>().Print()</code> 则会输出 <code>null</code>。</p>
<p>更进一步,因为 .NET 泛型在运行时会根据类型参数对代码进行特化,比如:</p>
<pre><code class="language-cs">class Calculator<T> where T : IAdditionOperators<T, T, T>
{
public T Add(T left, T right)
{
return left + right;
}
}
</code></pre>
<p>我们可以前往 godbolt 看看 .NET 的编译器对上述代码产生了什么机器代码:</p>
<pre><code class="language-asm">Calculator`1:Add(int,int):int:this (FullOpts):
lea eax,
ret
Calculator`1:Add(long,long):long:this (FullOpts):
lea rax,
ret
Calculator`1:Add(ubyte,ubyte):ubyte:this (FullOpts):
add edx, esi
movzx rax, dl
ret
Calculator`1:Add(float,float):float:this (FullOpts):
vaddss xmm0, xmm0, xmm1
ret
Calculator`1:Add(double,double):double:this (FullOpts):
vaddsd xmm0, xmm0, xmm1
ret
</code></pre>
<p>可以看到我代入不同的类型参数进去,会得到各自特化后的代码。</p>
<h3 id="接口的虚静态成员">接口的虚静态成员</h3>
<p>你可能好奇为什么上面的 <code>Calculator<T></code> 里 <code>left</code> 和 <code>right</code> 可以直接加,这是因为 .NET 支持接口的虚静态成员。上面的 <code>IAdditionOperators</code> 接口其实定义长这个样子:</p>
<pre><code class="language-cs">interface IAdditionOperators<TSelf, TOther, TResult>
{
abstract static TResult operator+(TSelf self, TOther other);
}
</code></pre>
<p>我们对 <code>T</code> 进行泛型约束 <code>where T : IAdditionOperators<T, T, T></code> 之后,就使得泛型代码中可以通过类型 <code>T</code> 直接调用接口中的静态抽象方法 <code>operator+</code>。</p>
<h3 id="性能">性能?</h3>
<p>有了上面的知识,我想知道在这套类型系统之上,.NET 的编译器到底能生成多优化的代码,那接下来我们进行一些小的测试。</p>
<p>首先让我们用类型表达一下具有 <code>int</code> 范围的数字,毕竟之后构建 Brainfuck 编译器的时候肯定会用到。众所周知 <code>int</code> 有 32 位,用 16 进制表示那就是 8 位。我们可以给 16 进制的每一个数位设计一个类型,然后将 8 位十六进制数位组合起来就是数字。</p>
<p>首先我们起手一个 <code>interface IHex</code>,然后让每一个数位都实现这个接口。</p>
<pre><code class="language-cs">interface IHex
{
abstract static int Value { get; }
}
</code></pre>
<p>比如十六进制数位 0、6、C 可以分别表示为:</p>
<pre><code class="language-cs">struct Hex0 : IHex
{
public static int Value => 0;
}
struct Hex6 : IHex
{
public static int Value => 6;
}
struct HexC : IHex
{
public static int Value => 12;
}
</code></pre>
<p>这里我们想把数字和数位区分开,因此我们定义一个跟 <code>IHex</code> 长得差不多但是泛型的接口 <code>INum<T></code> 用来给数字 <code>Int</code> 实现,之所以是泛型的是因为给万一没准以后想要扩展点浮点数之类的做考虑:</p>
<pre><code class="language-cs">interface INum<T>
{
abstract static T Value { get; }
}
struct Int<H7, H6, H5, H4, H3, H2, H1, H0> : INum<int>
where H7 : IHex
where H6 : IHex
where H5 : IHex
where H4 : IHex
where H3 : IHex
where H2 : IHex
where H1 : IHex
where H0 : IHex
{
public static int Value
{
get => H7.Value << 28 | H6.Value << 24 | H5.Value << 20 | H4.Value << 16 | H3.Value << 12 | H2.Value << 8 | H1.Value << 4 | H0.Value;
}
}
</code></pre>
<p>这里我们给 <code>Value</code> 加了 <code></code> 确保这个方法会被编译器 inline。</p>
<p>如此一来,如果我们想表达一个 <code>0x1234abcd</code>,我们就可以用 <code>Int<Hex1, Hex2, Hex3, Hex4, HexA, HexB, HexC, HexD></code> 来表达。</p>
<p>这里我们同样去 godbolt 看看 .NET 编译器给我们生成了怎样的代码:</p>
<pre><code class="language-asm">Int`8:get_Value():int (FullOpts):
push rbp
mov rbp, rsp
mov eax, 0x1234ABCD
pop rbp
ret
</code></pre>
<p>可以看到直接被编译器折叠成 <code>0x1234ABCD</code> 了,没有比这更优的代码,属于是真正的零开销抽象。</p>
<p>那么性能方面放心了之后,我们就可以开始搞 Brainfuck 编译器了。</p>
<h2 id="brainfuck-编译器">Brainfuck 编译器</h2>
<p>Brainfuck 编译分为两个步骤,一个是解析 Brainfuck 源代码,一个是产生编译结果。</p>
<p>对于 Brainfuck 源代码的解析,可以说是非常的简单,从左到右扫描一遍源代码就可以,这里就不详细说了。问题是怎么产生编译结果呢?</p>
<p>这里我们选择使用类型来表达一个程序,因此编译结果自然也就是类型。</p>
<p>我们需要用类型来表达程序的结构。</p>
<h3 id="基本操作">基本操作</h3>
<p>Brainfuck 程序离不开 4 个基本操作:</p>
<ul>
<li>移动指针</li>
<li>操作内存</li>
<li>输入</li>
<li>输出</li>
</ul>
<p>因此我们对此抽象出一套操作接口:</p>
<pre><code class="language-cs">interface IOp
{
abstract static int Run(int address, Span<byte> memory, Stream input, Stream output);
}
</code></pre>
<p>然后我们就可以定义各种操作了。</p>
<p>首先是移动指针,我们用两个泛型参数分别表达移动指针的偏移量和下一个操作:</p>
<pre><code class="language-cs">struct AddPointer<Offset, Next> : IOp
where Offset : INum<int>
where Next : IOp
{
public static int Run(int address, Span<byte> memory, Stream input, Stream output)
{
return Next.Run(address + Offset.Value, memory, input, output);
}
}
</code></pre>
<p>然后是操作内存:</p>
<pre><code class="language-cs">struct AddData<Data, Next> : IOp
where Data : INum<int>
where Next : IOp
{
public static int Run(int address, Span<byte> memory, Stream input, Stream output)
{
memory.UnsafeAt(address) += (byte)Data.Value;
return Next.Run(address, memory, input, output);
}
}
</code></pre>
<p>我们 Brainfuck 不需要什么内存边界检查,因此这里我用了一个 <code>UnsafeAt</code> 扩展方法跳过边界检查:</p>
<pre><code class="language-cs">internal static ref T UnsafeAt<T>(this Span<T> span, int address)
{
return ref Unsafe.Add(ref MemoryMarshal.GetReference(span), address);
}
</code></pre>
<p>接下来就是输入和输出了,这个比较简单,直接操作 <code>input</code> 和 <code>output</code> 就行了:</p>
<pre><code class="language-cs">struct OutputData<Next> : IOp
where Next : IOp
{
public static int Run(int address, Span<byte> memory, Stream input, Stream output)
{
output.WriteByte(memory.UnsafeAt(address));
return Next.Run(address, memory, input, output);
}
}
struct InputData<Next> : IOp
where Next : IOp
{
public static int Run(int address, Span<byte> memory, Stream input, Stream output)
{
var data = input.ReadByte();
if (data == -1)
{
return address;
}
memory.UnsafeAt(address) = (byte)data;
return Next.Run(address, memory, input, output);
}
}
</code></pre>
<h3 id="控制流">控制流</h3>
<p>有了上面的 4 种基本操作之后,我们就需要考虑程序控制流了。</p>
<p>首先,我们的程序最终毕竟是要停下来的,因此我们定义一个什么也不干的操作:</p>
<pre><code class="language-cs">struct Stop : IOp
{
public static int Run(int address, Span<byte> memory, Stream input, Stream output)
{
return address;
}
}
</code></pre>
<p>然后,Brainfuck 是支持循环的,这要怎么处理呢?其实也很简单,模拟 <code>while (*ptr) {</code> 这个操作就行了,也就是反复执行当前操作更新指针,直到指针指向的数据变成 0,然后跳到下一个操作去。</p>
<pre><code class="language-cs">struct Loop<Body, Next> : IOp
where Body : IOp
where Next : IOp
{
public static int Run(int address, Span<byte> memory, Stream input, Stream output)
{
while (memory.UnsafeAt(address) != 0)
{
address = Body.Run(address, memory, input, output);
}
return Next.Run(address, memory, input, output);
}
}
</code></pre>
<h3 id="hello-world">Hello World!</h3>
<p>有了上面的东西,我们就可以用类型表达 Brainfuck 程序了。</p>
<p>我们来看看最基础的程序:Hello World!。</p>
<pre><code class="language-bf">++++++++++[>+++++++>++++++++++>+++>+<<<<-]
>++.>+.+++++++..+++.>++.<<+++++++++++++++.
>.+++.------.--------.>+.>.
</code></pre>
<p>上面这个实现可能不是很直观,那我们换一种非常直观的实现:</p>
<pre><code class="language-bf">++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++>
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++>
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++>
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++>
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++>
++++++++++++++++++++++++++++++++>
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++>
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++>
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++>
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++>
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++>
+++++++++++++++++++++++++++++++++
<<<<<<<<<<<
.>.>.>.>.>.>.>.>.>.>.>.
</code></pre>
<p>这段程序很粗暴的分别把内存从左到右写成 <code>Hello World!</code> 的每一位,然后把指针移回到开头后逐位输出。</p>
<p>不过这么看 Hello World! 还是太长了,不适合用来一上来就展示,我们换个简单点的输出 <code>123</code>:</p>
<pre><code class="language-bf">+++++++++++++++++++++++++++++++++++++++++++++++++
>
++++++++++++++++++++++++++++++++++++++++++++++++++
>
+++++++++++++++++++++++++++++++++++++++++++++++++++
<<
.>.>.
</code></pre>
<p>表达这个程序的类型自然就是:</p>
<pre><code class="language-cs">AddData<49, AddPointer<1, AddData<50, AddPointer<1, AddData<51, // 分别设置 1 2 3
AddPointer<-2, // 指针移回开头
OutputData<AddPointer<1, OutputData<AddPointer<1, OutputData< // 输出
Stop>>>>>>>>>>> // 停止
</code></pre>
<p>这里为了简洁,我把数字全都带入了数字类型,不然会变得很长。例如实际上 <code>49</code> 应该表达为 <code>Int<Hex0, Hex0, Hex0, Hex0, Hex0, Hex0, Hex3, Hex1></code>。</p>
<p>那怎么运行呢?很简单:</p>
<pre><code class="language-cs">AddData<49, AddPointer<1, AddData<50, AddPointer<1, AddData<51, AddPointer<-2, OutputData<AddPointer<1, OutputData<AddPointer<1, OutputData<Stop>>>>>>>>>>>
.Run(0, stackalloc byte, Console.OpenStandardInput(), Console.OpenStandardOutput());
</code></pre>
<p>即可。</p>
<p>我们可以借助 C# 的 Type Alias,这样我们就不需要每次运行都打那么一大长串的类型:</p>
<pre><code class="language-cs">using Print123 = AddData<49, AddPointer<1, AddData<50, AddPointer<1, AddData<51, AddPointer<-2, OutputData<AddPointer<1, OutputData<AddPointer<1, OutputData<Stop>>>>>>>>>>>;
Print123.Run(0, stackalloc byte, Console.OpenStandardInput(), Console.OpenStandardOutput());
</code></pre>
<p>那我们上 godbolt 看看 .NET 给我们的 Brainfuck 程序产生了怎样的机器代码?</p>
<pre><code class="language-asm">push rbp
push r15
push r14
push r13
push rbx
lea rbp,
mov rbx, rsi
mov r15, r8
movsxd rsi, edi
add rsi, rbx
add byteptr , 49 ; '1'
inc edi
movsxd rsi, edi
add rsi, rbx
add byteptr , 50 ; '2'
inc edi
movsxd rsi, edi
add rsi, rbx
add byteptr , 51 ; '3'
lea r14d,
movsxd rsi, r14d
movzx rsi, byteptr
mov rdi, r15
mov rax, qword ptr
mov r13, qword ptr
call System.IO.Stream:WriteByte(ubyte):this
inc r14d
movsxd rsi, r14d
movzx rsi, byteptr
mov rdi, r15
call System.IO.Stream:WriteByte(ubyte):this
inc r14d
movsxd rsi, r14d
movzx rsi, byteptr
mov rdi, r15
call System.IO.Stream:WriteByte(ubyte):this
mov eax, r14d
pop rbx
pop r13
pop r14
pop r15
pop rbp
ret
</code></pre>
<p>这不就是</p>
<pre><code class="language-c">*(ptr++) = '1';
*(ptr++) = '2';
*ptr = '3';
ptr -= 2;
WriteByte(*(ptr++));
WriteByte(*(ptr++));
WriteByte(*ptr);
</code></pre>
<p>吗?可以看到我们代码里的抽象全都被 .NET 给优化干净了。</p>
<p>而前面那个不怎么直观的 Hello World! 代码则编译出:</p>
<pre><code class="language-cs">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>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
</code></pre>
<h3 id="jit-编译">JIT 编译</h3>
<p>如果我们想以 JIT 的形式运行 Brainfuck 代码,那如何在运行时生成类型然后运行代码呢?我们在 .NET 中有完善的反射支持,因此完全可以做到运行时创建类型。</p>
<p>比如根据数字来生成数字类型:</p>
<pre><code class="language-cs">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));
}
</code></pre>
<p>同理也可以用于生成各种程序结构上。</p>
<p>最后我们只需要对构建好的类型进行反射然后调用 <code>Run</code> 方法即可:</p>
<pre><code class="language-cs">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);
</code></pre>
<h3 id="aot-编译">AOT 编译</h3>
<p>那如果我不想 JIT,而是想 AOT 编译出来一个可执行文件呢?</p>
<p>你会发现,因为编译出的东西是类型,因此我们不仅可以在 JIT 环境下跑,还能直接把类型当作程序 AOT 编译出可执行文件!只需要编写一个入口点方法调用 <code>Run</code> 即可:</p>
<pre><code class="language-cs">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, Console.OpenStandardInput(), Console.OpenStandardOutput());
}
</code></pre>
<p>然后调用 AOT 编译:</p>
<pre><code class="language-bash">dotnet publish -c Release -r linux-x64 /p:PublishAot=true /p:IlcInstructionSet=native /p:OptimizationPreference=Speed
</code></pre>
<p>上面的 <code>/p:IlcInstructionSet=native</code> 即 C++ 世界里的 <code>-march=native</code>,<code>OptimizationPreference=Speed</code> 则是 <code>-O2</code>。</p>
<p>运行编译后的程序就能直接输出 <code>Hello World!</code>。</p>
<h2 id="性能测试">性能测试</h2>
<p>这里我们采用一段用 Brainfuck 编写的 Mandelbrot 程序进行性能测试,代码见 Pastebin。</p>
<p>它运行之后会在屏幕上输出:</p>
<p><img src="https://img2024.cnblogs.com/blog/1590449/202501/1590449-20250131232430695-1318554704.png" alt="mandelbrot_brainfuck" loading="lazy"></p>
<p>这段程序编译出来的类型也是非常的壮观:</p>
<p><img src="https://img2024.cnblogs.com/blog/1590449/202501/1590449-20250131232519063-475825482.png" alt="mandelbrot_brainfuck_type" loading="lazy"></p>
<p>去掉所有空格之后类型名称足足有 165,425 个字符!</p>
<p>这里我们采用 5 种方案来跑这段代码:</p>
<ul>
<li>C 解释器:C 语言编写的 Brainfuck 解释器直接运行</li>
<li>GCC:用 Brainfuck 翻译器把 Brainfuck 代码翻译到 C 语言后,用 <code>gcc -O3 -march=native</code> 编译出可执行程序后运行</li>
<li>Clang:用 Brainfuck 翻译器把 Brainfuck 代码翻译到 C 语言后,用 <code>clang -O3 -march=native</code> 编译出可执行程序后运行</li>
<li>.NET JIT:通过 JIT 现场生成类型后运行,统计之前会跑几轮循环预热</li>
<li>.NET AOT:通过 .NET NativeAOT 编译出可执行程序后运行</li>
</ul>
<p>测试环境:</p>
<ul>
<li>系统:Debian GNU/Linux 12 (bookworm)</li>
<li>CPU:13th Gen Intel(R) Core(TM) i7-13700K</li>
<li>RAM:CORSAIR DDR5-6800MHz 32Gx2</li>
</ul>
<p>运行 10 次取最优成绩,为了避免输出影响性能,所有输出重定向到 <code>/dev/null</code>。</p>
<p>得出的性能测试结果如下:</p>
<table>
<thead>
<tr>
<th>项目</th>
<th>运行时间(毫秒)</th>
<th>排名</th>
<th>比例</th>
</tr>
</thead>
<tbody>
<tr>
<td>C 解释器</td>
<td>4874.6587</td>
<td>5</td>
<td>5.59</td>
</tr>
<tr>
<td>GCC</td>
<td>901.0225</td>
<td>3</td>
<td>1.03</td>
</tr>
<tr>
<td>Clang</td>
<td>881.7177</td>
<td>2</td>
<td>1.01</td>
</tr>
<tr>
<td>.NET JIT</td>
<td>925.1596</td>
<td>4</td>
<td>1.06</td>
</tr>
<tr>
<td>.NET AOT</td>
<td>872.2287</td>
<td>1</td>
<td>1.00</td>
</tr>
</tbody>
</table>
<p>最后 .NET AOT 在这个项目里取得了最好的成绩,当然,这离不开 .NET 类型系统层面的零开销抽象。</p>
<h2 id="项目地址">项目地址</h2>
<p>该项目以 MIT 协议开源,欢迎 star。</p>
<p>项目开源地址:https://github.com/hez2010/Brainfly</p><br><br>
来源:https://www.cnblogs.com/hez2010/p/18696074/brainfly-brainfuck-compiler-built-with-csharp
頁:
[1]