試試將.NET7編譯為WASM並在Docker上執行

2022-11-10 12:01:00

之前有聽到說Docker支援Wasmtime了,剛好.NET7也支援WASM,就帶大家來了解一下這個東西,順便試試它怎麼樣。

因為WASM(WebAssembly) 一開始是一個給瀏覽器的技術,比起JS解釋執行,WASM能用於提升瀏覽器的使用者體驗,因為在一些場景中它有著比JS更好的效能。

大家可以將WASM理解為C#的MSIL或者Java的位元組碼,它並不是二進位制程式碼,還是會由JIT編譯執行,JIT有很多優化,另外大多數場景也只會JIT一次,加上省略了JS載入,語法分析各種的過程,才會有著比JS更好的效能。

另外因為WASM是中間碼的格式,所以理論上任何語言C#、RUST、Java、Go都可以將程式碼編譯為WASM,然後放到瀏覽器中執行。比如C#火熱的Blazor專案,就是將C#編譯為WASM,然後使C#程式碼能在瀏覽器中執行。

另外聊一聊WASI(WebAssembly System Interface),我們知道WASM有著不錯的可移植性和安全性(目前瀏覽器執行都是沙箱執行,對於許可權管控很嚴格),那麼就有一群大佬就說,我們是不是能脫離瀏覽器單獨執行WASM程式呢?於是就產生了一個標準的系統介面,大家都按照這樣的方式來生成WASM,呼叫系統API,然後我們開發一個Runtime,讓大家的WASM程式都能在這上面執行。

舉個不嚴謹的例子說明一下WASI就是比如:

  • C# => MSIL => CLR(Mono、CoreCLR)
  • Java => 位元組碼 => JVM(HotSpot VM、ZingVM)
    而現在我們可以:
  • C# => WASM => WASI(wasmtime、wasmedge)。

各位應該就明白了,WASI其實就是個執行時的規範,大家編譯成WASM放上去就能跑。

所以現在對於它的觀點就是,覺得它在Server後端領域目前來說不是一個很價值的東西,因為可移植性好的語言比比皆是,比如C#、Java、Go等等。

拿效能來說,對於這樣的中間語言效能無關就是JIT和GC,WASI的JIT和GC能做的像C#、Java這樣的JIT、GC效能那麼好嗎?這個目前來說是存在疑問的,至少在短時間內很難追平其它平臺十多年的優化。

再說WASM的另一個優點,就是體積小和啟動快,現在C#支援NativeAOT、Java有GraalVM、Go和Rust之類的本身就是編譯型語言,啟動速度和體積都很不錯,WASM在這個方面其實不佔優勢。

.NET編譯為WASM

好了,言歸正傳,我們來試試.NET7上面的WASM。.NET7目前已經發布,我們需要使用最新的版本,如下圖所示:

然後我們建立一個簡單的控制檯專案,用於輸出斐波那契數列和執行耗時,程式碼如下所示 (這並不效能最優的實現,只是這樣子實現簡單)

using System.Diagnostics;

namespace PublishDotNetToWASM;

public static class Program
{
    public static void Main()
    {
        // warm
        ulong sum = 0;
        foreach (var i in Fibonacci().Take(1000))
        {
            sum += i;
        }

        // run
        sum = 0;
        var sw = Stopwatch.StartNew();
        foreach (var i in Fibonacci().Take(100000))
        {
            sum += i;
        }
        sw.Stop();
        Console.WriteLine($"Result:{sum}, Timespan:{sw.ElapsedTicks} Ticks");
    }

    private static IEnumerable<ulong> Fibonacci()
    {
        ulong current = 1, next = 1;

        while (true) 
        {
            yield return current;
            next = current + (current = next);
        }
    }
}

接下來為了將.NET程式釋出成WASM,我們需要安裝Wasi.Sdk預覽包,這個預覽包是Steve Sanderson大佬做的支援,可以將.NET程式編譯為WASM,截止至目前版本資訊如下所示:

<PackageReference Include="Wasi.Sdk" Version="0.1.2-preview.10061" />

執行dotnet publish -c Release命令,將我們的應用程式釋出為WASM格式,在釋出過程中,需要下載MinGW作為編譯器,網路環境不好的同學,需要使用proxy,稍微等待一會就順利的釋出成功了:

執行WASM程式

此時我們可以安裝一下Wasmtime來執行我們的程式,通過https://wasmtime.dev/下載安裝:

然後就可以直接使用wasmtime命令執行我們的程式,我分別使用wasmtimedotnet執行了我們的程式:

可見目前來說WASM的效能還是慘不忍睹的,等一等後續的優化吧。

將.NET釋出到Docker WASI

再來看看我們的Docker,對於Docker支援WASI我感到並不意外,因為Docker的容器化對於直接執行的WASM來說還是比較重,支援它是一個拓寬影響力的好事。具體的執行模型如下所示,對於WASM應用有著不同的執行方式。不再使用runc而是wasmedge


wasmedge也是一個實現了WASI標準的WASM執行時,和上文提到的wasmtime一樣。

要實現在Docker上執行WASM程式需要安裝Docker的預覽版,連結https://docs.docker.com/desktop/wasm/

然後我們整一個Dockerfile,我們直接依賴scratch映象即可,因為它不需要其它的基礎映象(暫時我沒有使用.NET7的多段構建映象,聽大佬說目前貌似有問題)。

FROM scratch
COPY ./bin/Release/net7.0/PublishDotNetToWASM.wasm /PublishDotNetToWASM.wasm
ENTRYPOINT [ "PublishDotNetToWASM.wasm" ]

再使用下面的命令構建Docker映象,由於是wasm映象,所以需要帶額外的引數。

docker buildx build --platform wasi/wasm32 -t publishdotnettowasm .

可以看到打包出來的映象是非常小的,只有3.68MB。

執行的話也很簡單,用下方的命令即可,需要指定runtime為io.containerd.wasmedge.v1,另外也需要指定paltform。

docker run --rm --name=publishdotnettowasm --runtime=io.containerd.wasmedge.v1 --platform=wasi/wasm32 publishdotnettowasm

我把dotnet原生執行、wasmtime執行、docker WASI執行都跑了一下,可以發現目前來說效能是慘不忍睹。

總結

以上就是如何將.NET7程式釋出到WASM,然後在Docker最新的WASI中執行的樣例,目前來看基本的執行都已經OK,不過正如前面提到的,現在效能還是太受影響了。

這不僅僅是在.NET平臺上,其它語言Rust、C、C++編譯為WASM上都有明顯的效能下降。

思來想去可能在一些外掛化和不需要效能很好的場景WASI會比較用。不過這些都需要時間慢慢見證,畢竟存在即合理,像JS這樣的語言不一樣好好的?

我們可以拭目以待,看看WASM/WASI會不會給我們帶來其它驚喜,期待後續Steve Sanderson大佬和WASM社群的相關優化。

原始碼連結

https://github.com/InCerryGit/PublishDotNetToWASM

參考文獻

https://www.docker.com/blog/docker-wasm-technical-preview/
https://www.zhihu.com/question/304577684/answer/1961085507
https://arghya.xyz/articles/webassembly-wasm-wasi/
https://laurentkempe.com/2022/10/31/experimenting-with-dotnet-7-wasm-and-wasi-on-docker/