C# Compiler dan Just-in-Time (JIT) Compilation

Ketika kita menulis kode C#, kode tidak langsung di-compile menjadi machine instruction untuk prosesor atau operating system spesifik. Kode C# tersebut harus masuk ke pipeline compilation yang didesain untuk mendukung…

Ketika kita menulis kode C#, kode tidak langsung di-compile menjadi machine instruction untuk prosesor atau operating system spesifik. Kode C# tersebut harus masuk ke pipeline compilation yang didesain untuk mendukung portabilitas, keamanan, dan optimisasi.

1. Roslyn, C# Compiler

Langkah pertama dalam pipeline adalah C# compiler, yang dikenal sebagai Roslyn. Roslyn menganalisis source code untuk ketepatan, mengecek syntax, type safety, dan language rules sebelum menghasilkan compiled output.

Roslyn juga memberdayai code analysis features pada tools seperti Visual Studio dan VS Code, termasuk IntelliSense, code navigation, dan real-time error detection. Selain itu, Roslyn memberdayai static code analysis tools yang mengecek kualitas kode, style issues, dan potensi masalah keamanan.

Jika kodenya valid, compiler (Roslyn) menerjemahkannya ke Intermediate Language (IL).

Contoh: kode C# sederhana ini…

public class Calculator
{
    public int Add(int a, int b)
    {
        return a + b;
    }
}

…akan gagal di-compile oleh Roslyn kalau ada type mismatch, misalnya:

public int Add(int a, int b)
{
    return a + "b"; // Compile-time error: CS0029
                    // Cannot implicitly convert type 'string' to 'int'
}

Error ini ditangkap sebelum kode sempat jadi IL sama sekali, ini bagian dari “ketepatan” yang dicek Roslyn.

Roslyn juga dapat dipakai secara programatik (Roslyn API) untuk analisis kode, contohnya untuk menghitung jumlah method dalam sebuah class:

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

var code = "public class Foo { void A() {} void B() {} }";
var tree = CSharpSyntaxTree.ParseText(code);
var root = tree.GetRoot();

var methodCount = root.DescendantNodes()
                       .OfType<MethodDeclarationSyntax>()
                       .Count();

Console.WriteLine($"Jumlah method: {methodCount}"); // 2

Ini contoh konkret dari “code analysis features” yang disebut di atas — analyzer di VS Code/Visual Studio pada dasarnya melakukan hal serupa di background.

2. Intermediate Language (IL)

IL adalah representasi low-level platform independent dari sebuah program. Dan karena IL tidak terikat pada spesifik OS atau arsitektur prossesor, aplikasi yang telah di-compiled bisa berjalan pada envrionment yang berbeda tanpa harus direcompiled dari source code.

Kalau Calculator.cs di atas di-compile dan IL-nya dilihat (misalnya lewat ildasm atau sharplab.io), method Add akan terlihat kira-kira seperti ini:

.method public hidebysig instance int32 Add (
    int32 a,
    int32 b
) cil managed
{
    .maxstack 8
    IL_0000: ldarg.1   // load a
    IL_0001: ldarg.2   // load b
    IL_0002: add       // a + b
    IL_0003: ret
}

Perhatikan: instruksi seperti ldarg dan add ini bukan instruksi native CPU — ini bytecode CIL (Common Intermediate Language) yang masih perlu diterjemahkan lagi ke native code oleh runtime. Inilah yang membuat file .dll/.exe hasil build C# bisa jalan di Windows, Linux, maupun macOS selama ada .NET runtime yang sesuai.

3. .NET Runtime (CLR), Eksekusi

Setelah aplikasi mulai berjalan, .NET runtime Common Language Runtime (CLR) bertanggung jawab untuk eksekusi. Runtime menerjemahkan IL ke native machine code yang cocok dengan platform (Windows, Linux, macOS). Ada dua cara:

Just-in-Time (JIT) compilation

  1. Paling umum; kompilasi terjadi saat runtime (di mesin milik end-user, bukan di mesin developer).
  2. Method di-compile ke native code ketika pertama kali dieksekusi, bukan semuanya di awal. Setelah itu, compiled code (native code) disimpan di memory dan digunakan kembali selama lifetime aplikasi, tidak di-compile ulang setiap dipanggil.
  3. Compiler bisa mengoptimasi kode generasi berdasarkan informasi yang hanya tersedia saat runtime (misalnya CPU spesifik yang dipakai, atau pola pemakaian method, ini disebut tiered compilation, di mana JIT awalnya menghasilkan kode “cepat tapi kurang optimal”, lalu method yang sering dipanggil (hot path) di-compile ulang dengan optimisasi lebih agresif).
dotnet run
# Secara default project .NET biasa (framework-dependent) pakai JIT:
# method di-compile on-the-fly saat pertama kali dipanggil.

Konsekuensinya: ada sedikit startup delay (warm-up) karena method-method perlu di-JIT dulu saat pertama dipanggil, makanya request pertama ke sebuah endpoint ASP.NET Core biasanya lebih lambat dibanding request-request berikutnya.

Penting: JIT bukan sesuatu yang terikat pada perintah dotnet run saja. dotnet run hanya perintah development yang melakukan build lalu langsung menjalankan hasilnya, itu cuma salah satu cara untuk memulai proses. JIT adalah perilaku level CLR (runtime), dan akan tetap terjadi di skenario mana pun selama assembly IL dieksekusi tanpa precompilation (AOT/ReadyToRun):

# Semua skenario ini tetap memakai JIT di baliknya:
dotnet run
dotnet bin/Release/net8.0/MyApp.dll                       # hasil publish framework-dependent
dotnet test                                                # saat test runner mengeksekusi kode
# + aplikasi ASP.NET Core yang di-hosting lewat IIS, Kestrel, atau container

Contoh publish self-contained (tanpa flag AOT/R2R), sama-sama tetap pakai JIT, hanya beda target platform:

# Linux
dotnet publish -c Release --self-contained -r linux-x64

# Windows
dotnet publish -c Release --self-contained -r win-x64

Untuk kedua publish self-contained di atas, hasil outputnya tidak membutuhkan .NET runtime terinstal di mesin target, karena seluruh runtime CLR (termasuk JIT compiler di dalamnya) ikut di-bundle ke folder output publish. Cara menjalankannya beda tergantung OS:

# Linux — jalankan executable langsung
./MyApp

# Windows (cmd/PowerShell) — jalankan .exe langsung
MyApp.exe

Ahead-Of-Time (AOT) Compilation

  1. IL dikompilasi ke native machine code sebelum aplikasi berjalan, yaitu pada saat proses build/publish, bukan saat runtime.
  2. Mengurangi startup time karena tidak perlu ada proses JIT-ing saat aplikasi pertama kali dijalankan, kode sudah 100% native code dari awal.
  3. Meningkatkan predictability, karena tidak ada variasi performa akibat warm-up JIT atau tiered compilation; setiap eksekusi konsisten sejak awal.
  4. Cocok untuk skenario seperti container/serverless (cold start harus cepat), CLI tools, atau device dengan resource terbatas.
  5. Trade-off: ukuran binary biasanya lebih besar (karena runtime ikut di-bundle atau sebagian di-trim), dan beberapa fitur berbasis reflection dinamis bisa butuh penyesuaian karena AOT butuh tahu semua kode yang mungkin dieksekusi di waktu compile.
# Publish sebagai Native AOT (tersedia mulai .NET 7+)

# Linux
dotnet publish -c Release -r linux-x64 -p:PublishAot=true

# Windows
dotnet publish -c Release -r win-x64 -p:PublishAot=true

# Hasilnya: satu file executable native (MyApp di Linux, MyApp.exe di Windows),
# tanpa perlu .NET runtime terinstal di mesin target, dan tanpa proses JIT saat dijalankan.

Proses AOT ini tidak cross-compile secara default — publish untuk win-x64 idealnya dilakukan di mesin Windows, dan publish untuk linux-x64 di mesin Linux, karena Native AOT butuh native toolchain platform tersebut (di Windows: Visual Studio Build Tools/C++ toolset; di Linux: clang).

Ada juga mode ketiga yang sering disandingkan dengan JIT/AOT, yaitu ReadyToRun (R2R) ini semacam hybrid: assembly di-precompile sebagian ke native code saat publish (untuk mempercepat startup), tapi JIT tetap jalan di background untuk optimisasi lanjutan. Ini beda dari Native AOT yang benar-benar menghilangkan JIT sepenuhnya.

# Linux
dotnet publish -c Release -r linux-x64 --self-contained -p:PublishReadyToRun=true

# Windows
dotnet publish -c Release -r win-x64 --self-contained -p:PublishReadyToRun=true

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *