Linux 有個好用偵錯工具 - strace,它可以追蹤及統計應用程式調用系統呼叫 (System Call) 的狀況。在作業系統裡,舉凡開關檔、讀檔、程序管理、通訊... 等核心等級的作業系統動作都必須透過 System Call 完成。因此,應用程式完成相同動作所呼叫 System Call 次數可當成衡量執行效率的參考指標,不全然精準(例如:程式沒呼叫 System Call 但拼命跑迴圈的極端案例),但在大部分情境呼叫次數愈少通常代表程式愈輕巧,效率愈好。

舉例來說,同樣要在終端機顯示 Hello Word!,用 C 寫一定比組合語言複雜,用 .NET 寫則又比 C 更複雜,但編譯成 Native AOT 應會有所改善。所以這篇來做個有趣實驗,從 strace 角度比較 Console.WriteLine("Hello, World!") .NET 程式有無編譯成 AOT 的差異,各需要動用多少 System Call 完成這個超簡單任務。另外,既然動手了就順便連 C++、C 及組合語言都拿來比一比,見識一下中低階語言編譯的程式碼的精簡程度。

我的測試方法很簡單,在 WSL2 用 dotnet new console -o hello-cs 產生空白 Console Appliation 專案,Program.cs 原本就只有一行 Console.WriteLine("Hello, World!");,執行 dotnet publish --no-self-contained -p:PublishSingleFile=true 得到執行檔 bin/Release/net8.0/linux-x64/publish/hello-cs

接著修改 hello-cs.csproj 加入 <PublishAot>true</PublishAot> 參考,重新 dotnet publish,得到執行檔 bin/Release/net8.0/linux-x64/publish/hello-cs,這裡將它更名為 hello-cs-aot,方便稍後比較。

再來我分別寫了 C/C++ 跟組合語言的版本:

C 語言,hello-c.c:

#include <stdio.h>
// gcc hello-c.c -o hello-c
int main(void){
        printf("Hello, World!\n");
        return 0;
}

C++,hello-cpp.cpp:

#include <iostream>
// g++ hello-cpp.cpp -o hello-cpp
int main() {
    std::cout << "Hello World!";
    return 0;
}

最後是組合語言:

section .data
    text db "Hello, World!",10
 
section .text
    global _start
 
_start:
    mov rax, 1
    mov rdi, 1
    mov rsi, text
    mov rdx, 14
    syscall
 
    mov rax, 60
    mov rdi, 0
    syscall

編譯執行:

sudo apt install -y nasm
nasm -f elf64 hello.asm
ld hello.o -o hello-asm
strace ./hello-asm 2> asm.txt

五隻程式一字排開:

一如預期,組合語言最小,只有 8,488 bytes,C 比 C++ 小一點,16,696 vs 17,176。C# 標準執行檔大小為 77,941 bytes,體積不大,原因是有一大部分工作由 Runtime 完成;編譯成 AOT 版本時,原本由 Runtime 執行的作業包進執行檔,大小增加到 1.5MB。

使用 strace -c <exec-file-name> 2> <result-txt> 記錄各語言版本的 Hello World! System Call 統計如下。

C# 標準執行檔最多,共 1539 次,耗時 0.014398s:

% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
 22.78    0.003280           8       375           mprotect
 12.61    0.001816           8       222           mmap
 11.47    0.001652           7       216       131 openat
  9.61    0.001383          14        93           read
  5.35    0.000771           7       108         2 lstat
  4.32    0.000622          44        14           futex
  4.07    0.000586           7        75           close
  3.83    0.000551           8        62           munmap
  3.71    0.000534          66         8           write
  3.59    0.000517           6        85           fstat
  2.36    0.000340           6        49        36 stat
  2.06    0.000296          11        25           madvise
  1.63    0.000235           7        30           brk
  1.53    0.000221          44         5         2 unlink
  1.42    0.000204          29         7           clone
  1.38    0.000198           7        26           rt_sigaction
  1.11    0.000160           5        27           pread64
  0.92    0.000133           8        15         8 access
  0.71    0.000102          10        10           lseek
  0.65    0.000093           4        22           fcntl
  0.48    0.000069           9         7           prlimit64
  0.38    0.000054          13         4           statfs
  0.36    0.000052           8         6           getpid
  0.34    0.000049          24         2           mknod
  0.34    0.000049          12         4           sched_getaffinity
  0.32    0.000046          11         4           getdents64
  0.30    0.000043           8         5           readlink
  0.29    0.000042          10         4           ioctl
  0.28    0.000041          13         3           sysinfo
  0.25    0.000036          12         3           pipe2
  0.24    0.000035          35         1           bind
  0.23    0.000033           8         4           membarrier
  0.13    0.000019           9         2           getsid
  0.13    0.000018           9         2           sigaltstack
  0.12    0.000017           8         2           gettid
  0.10    0.000015          15         1           socket
  0.09    0.000013          13         1           memfd_create
  0.08    0.000012          12         1           ftruncate
  0.08    0.000012          12         1           fchmod
  0.08    0.000011          11         1           listen
  0.06    0.000009           9         1           rt_sigprocmask
  0.06    0.000009           4         2         1 arch_prctl
  0.06    0.000009           9         1           set_tid_address
  0.06    0.000009           9         1           set_robust_list
  0.01    0.000002           2         1         1 get_mempolicy
  0.00    0.000000           0         1           execve
------ ----------- ----------- --------- --------- ----------------
100.00    0.014398                  1539       181 total

AOT 版只有 1/3,574 次,速度也變快,耗時縮短到 0.000521s:

% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
 16.89    0.000088           4        19           rt_sigaction
 15.74    0.000082           0       185       141 openat
 11.71    0.000061           0        78           mmap
  8.25    0.000043          10         4           ioctl
  7.49    0.000039          19         2           write
  7.10    0.000037           1        34           mprotect
  5.95    0.000031          15         2           clone
  5.57    0.000029           0        38           read
  3.45    0.000018           0        43           close
  3.45    0.000018           0        42           fstat
  2.88    0.000015           0        27           munmap
  2.50    0.000013          13         1           pipe2
  1.92    0.000010           2         4           brk
  1.92    0.000010           1         9           pread64
  1.73    0.000009           9         1           lseek
  1.73    0.000009           3         3           getpid
  1.73    0.000009           4         2           futex
  0.00    0.000000           0        40        35 stat
  0.00    0.000000           0         1           rt_sigprocmask
  0.00    0.000000           0         1         1 access
  0.00    0.000000           0        18           madvise
  0.00    0.000000           0         1           execve
  0.00    0.000000           0         1           fcntl
  0.00    0.000000           0         1           readlink
  0.00    0.000000           0         2           sysinfo
  0.00    0.000000           0         2           statfs
  0.00    0.000000           0         2         1 arch_prctl
  0.00    0.000000           0         1           gettid
  0.00    0.000000           0         3           sched_getaffinity
  0.00    0.000000           0         1           set_tid_address
  0.00    0.000000           0         1         1 get_mempolicy
  0.00    0.000000           0         1           set_robust_list
  0.00    0.000000           0         2           prlimit64
  0.00    0.000000           0         2           membarrier
------ ----------- ----------- --------- --------- ----------------
100.00    0.000521                   574       179 total

C++ 版本 64 次,耗時 0.000926s。

% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
 26.13    0.000242          11        22           mmap
 19.87    0.000184         184         1           execve
  9.18    0.000085          12         7           mprotect
  9.07    0.000084          16         5           openat
  6.91    0.000064          10         6           fstat
  5.94    0.000055           9         6           pread64
  5.29    0.000049           9         5           close
  5.18    0.000048          12         4           read
  4.00    0.000037          12         3           brk
  2.59    0.000024          24         1           write
  2.27    0.000021          10         2         1 arch_prctl
  1.84    0.000017          17         1           munmap
  1.73    0.000016          16         1         1 access
------ ----------- ----------- --------- --------- ----------------
100.00    0.000926                    64         2 total

C 語言版本 33 次,時間短到測不到。

% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
  0.00    0.000000           0         1           read
  0.00    0.000000           0         1           write
  0.00    0.000000           0         2           close
  0.00    0.000000           0         3           fstat
  0.00    0.000000           0         7           mmap
  0.00    0.000000           0         3           mprotect
  0.00    0.000000           0         1           munmap
  0.00    0.000000           0         3           brk
  0.00    0.000000           0         6           pread64
  0.00    0.000000           0         1         1 access
  0.00    0.000000           0         1           execve
  0.00    0.000000           0         2         1 arch_prctl
  0.00    0.000000           0         2           openat
------ ----------- ----------- --------- --------- ----------------
100.00    0.000000                    33         2 total

最後來看組合語言,各位觀眾,2 次!

% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
  0.00    0.000000           0         1           write
  0.00    0.000000           0         1           execve
------ ----------- ----------- --------- --------- ----------------
100.00    0.000000                     2           total

【結論】

在以上實驗,用呼叫 System Call 頻率作為系統複雜度及執行效能指標,一如預期得到 C# > C++ > C > 組合語言 的相對關係,愈高階的程式語言在完成相同任務的前題下,編譯產生的程式體積愈大,執行時動用的 System Call 愈多。

不過,在大部分情境下,高階語言開發速度快、易於維護修改擴充的優勢遠大於這部分的效能損失,而且生態系統的程式庫及文件資源較中低階豐富 N 倍。故除非是極度追求速度的應用,高階語言還是多數人的選擇。(若被要求用組合語言寫 MVC 網站,開發人員會選擇自掛東南枝吧?XD)

而 .NET AOT 在 Hello, World! 案例差異明顯,執行過程減少了近 2/3 的 System Call 次數,速度加快 27 倍。改善源自不用帶入整個 Runtime 及省掉 JIT 編譯,故對啟動速度敏感的應用,可考慮啟用 AOT 加速。

【延伸閱讀】

This article uses Linux strace to observe the performance improvements of .NET AOT, and also compares the differences between C#, C/C++, and assembly language.


Comments

# by Name

DUH …

Post a comment