Học lập trình một lúc sẽ gặp mấy câu như này:

Học nhập môn thì nhớ như thế cũng chạy được. Nhưng đào thêm một tí sẽ bắt đầu có vấn đề: Python cũng compile ra bytecode, JavaScript engine có JIT compiler, còn TypeScript thậm chí không có runtime riêng. Vậy rốt cuộc compiled với interpreted đang nói về cái gì?

CPU không chạy source code

Lấy đoạn C++ này:

std::cout << "Hello, World!\n";

CPU không biết std::cout là cái gì. Nó cũng không hiểu if, class, dấu ngoặc nhọn hay indentation của Python. CPU chạy machine instruction của kiến trúc như x86-64 hoặc ARM.

Vậy giữa source code con người viết và instruction CPU chạy phải có một hoặc nhiều chương trình đứng giữa để dịch, biến đổi và tổ chức việc thực thi. Compiler và interpreter nằm ở đoạn này.

Source codeCompilerNative machine codeInterpreterKết quảBytecode / IRVM + interpreter / JITBa mô hình thực thi cơ bản
// Mermaid diagram

Sơ đồ trên cố tình đơn giản. Pipeline thật có thể thêm parser, optimizer, assembler, linker, virtual machine và một đống intermediate representation ở giữa.

Compile là dịch trước khi chạy

Với kiểu Ahead-of-Time compilation, compiler xử lý chương trình trước, tạo object code hoặc executable. Đến lúc chạy, hệ điều hành nạp executable và CPU thực hiện machine code đã được tạo ra.

Đường đi thường nhìn như này:

source → compiler → object file → linker → executable → CPU

C++ thường đi đường này. Go và Rust cũng vậy.

Điểm dễ thấy là lỗi syntax hoặc type có thể bị chặn ngay ở build. Kết quả thường là binary riêng cho từng OS và architecture; build cho Windows x64 không mặc nhiên chạy được trên Linux ARM.

Compiled không tự động đồng nghĩa với “nhanh”, vì tốc độ còn phụ thuộc thuật toán, compiler optimization, runtime, I/O và cách mình viết code. Nhưng native code đã được chuẩn bị trước thường có lợi về startup và hiệu năng thực thi.

Interpret là có runtime đứng ra thực thi

Interpreter nhận chương trình hoặc một dạng trung gian của chương trình rồi thực thi nó trong runtime.

Câu “interpreter đọc từng dòng source và chạy từng dòng” chỉ là hình dung nhập môn. CPython chẳng hạn, vẫn parse và compile file .py thành Python bytecode trước, sau đó evaluation loop của Python VM mới xử lý bytecode đó.

Interpreter cho phép runtime quyết định nhiều thứ lúc chương trình đang chạy, đổi lại mỗi lần thực thi có thêm một lớp ở giữa source và CPU.

Hybrid là trộn nhiều bước

Phần lớn runtime hiện đại không chịu đứng yên trong đúng một ô của bảng.

Java compile source thành JVM bytecode. JVM có thể interpret bytecode trước, sau đó JIT-compile những method được chạy nhiều thành native code. JavaScript engine cũng có thể khởi động bằng interpreter rồi tối ưu hot code bằng một hoặc nhiều compiler tier.

Vậy compiled language hay interpreted language chỉ là nhãn cho pipeline phổ biến của một implementation. Ngôn ngữ định nghĩa syntax và semantics; chương trình như CPython, V8, JVM, rustc hay Go compiler mới quyết định source đi qua đường nào.

Giờ đi từng ngôn ngữ cho dễ nhìn.

JavaScript

Chạy JavaScript bằng Node.js nghĩa là đưa source cho JavaScript engine, phổ biến là V8. Engine parse code, tạo representation nội bộ và bắt đầu thực thi. Những đoạn chạy nhiều có thể được JIT-compile thành machine code tối ưu hơn trong lúc chương trình vẫn đang chạy.

Vì thế gọi JavaScript là interpreted language không hẳn sai trong lớp nhập môn, nhưng nói “JavaScript chỉ được đọc từng dòng, không compile” thì sai.

console.log ở ví dụ dưới do môi trường chạy như browser hoặc Node.js cung cấp. Nó không phải keyword của bản thân JavaScript.

console.log("Hello, World!");

Khi chạy:

source JavaScript → engine parse → bytecode/IR → interpreter/JIT → machine code

Chi tiết bytecode và các tầng compiler là implementation detail, engine có thể đổi qua từng version. Khi phỏng vấn không cần thuộc tên tất cả tier trừ khi đang ứng tuyển vào team làm engine.

TypeScript

TypeScript hay gây hiểu nhầm vì nhìn nó giống một ngôn ngữ có runtime riêng. Thực tế type chủ yếu dùng để kiểm tra lúc phát triển và bị xoá khỏi JavaScript output.

const message: string = "Hello, World!";
console.log(message);

Sau khi compile/transpile, phần quan trọng có thể còn:

const message = "Hello, World!";
console.log(message);

Pipeline:

TypeScript → type-check + emit JavaScript → JavaScript engine → kết quả

Runtime không biết biến message từng có type string. Nếu dữ liệu từ API sai type thì type annotation không nhảy ra đỡ mình lúc chương trình đang chạy. Muốn bảo vệ runtime vẫn phải validate dữ liệu.

Python

Với implementation phổ biến là CPython, file .py được parse rồi compile thành Python bytecode. Python Virtual Machine thực thi các instruction bytecode đó.

message = "Hello, World!"
print(message)

Pipeline nôm na:

Python source → parser/compiler → Python bytecode → CPython evaluation loop → machine code của runtime

File .pyc là cache bytecode để module phù hợp không phải compile lại từ đầu. Nó không phải native executable kiểu file do C++ tạo ra.

Python có implementation khác CPython, nên đừng lấy chi tiết của CPython làm luật của toàn bộ ngôn ngữ. PyPy chẳng hạn có JIT; pipeline lại khác.

Java

Java có hai lần biến đổi dễ thấy. javac compile source .java thành JVM bytecode trong file .class. JVM load, verify rồi thực thi bytecode; trong lúc chạy, JIT compiler có thể biến hot method thành native code cho máy hiện tại.

public class Main {
    public static void main(String[] args) {
        System.out.println("Hello, World!");
    }
}
Java source → javac → JVM bytecode → JVM interpreter/JIT → machine code

Câu “write once, run anywhere” dựa trên việc nhiều nền tảng có JVM hiểu cùng format class file, không phải CPU nào cũng tự nhiên hiểu Java bytecode. Mỗi nền tảng vẫn cần JVM phù hợp.

Go

Go compiler build package và dependency thành native executable. Chạy binary không cần cài một interpreter để đọc source Go.

package main

import "fmt"

func main() {
    fmt.Println("Hello, World!")
}
Go source → compiler + linker → native executable → CPU

Nhưng native executable không có nghĩa là “không có runtime”. Binary Go vẫn mang các phần runtime cần cho garbage collector, goroutine scheduler, stack management và nhiều việc khác. Runtime ở đây được build cùng chương trình, không phải interpreter đứng ngoài đọc source.

Rust

rustc parse source, resolve tên, type-check và chạy borrow checker. Nếu ownership hoặc lifetime không hợp lệ thì dừng ở compile time. Sau đó code đi qua các intermediate representation và backend để tạo object code rồi link thành executable. Backend mặc định thường dùng LLVM.

fn main() {
    println!("Hello, World!");
}
Rust source → rustc checks + optimization → object code → linker → executable

Rust không dùng garbage collector mặc định. Borrow checker giúp kiểm tra nhiều quy tắc quản lý bộ nhớ trước khi chương trình chạy, đổi lại lúc mới học có thể bị compiler mắng khá đều.

C++

C++ có mấy bước mà IDE thường giấu đi nên người mới chỉ thấy bấm Run rồi cửa sổ hiện ra.

  1. Preprocessor xử lý #include, macro và conditional compilation.
  2. Compiler biến từng translation unit thành assembly/object code.
  3. Assembler tạo object file nếu pipeline tách riêng bước này.
  4. Linker ghép object file với library thành executable.
#include <iostream>

int main() {
    std::cout << "Hello, World!\n";
    return 0;
}

#include <iostream> không gọi một package manager lúc runtime. Preprocessor đưa declaration cần thiết vào translation unit; linker sau đó nối phần implementation từ standard library phù hợp.

C++ source → preprocess → compile/assemble → object files → link → executable

Lỗi undefined reference thường xuất hiện ở link stage: source có declaration nên compile qua, nhưng linker không tìm được definition để ghép vào executable.

Bảng ghi nhớ

Ngôn ngữĐường đi phổ biếnChạy cuối ở đâu?
JavaScriptParse → bytecode/IR → interpreter + JITJavaScript engine
TypeScriptType-check/emit → JavaScriptJavaScript engine
PythonCompile → Python bytecodePython VM trong implementation như CPython
Javajavac → JVM bytecode → interpreter/JITJVM
GoAOT compile + link → executableCPU cùng Go runtime đã build vào binary
RustCompile checks + backend + link → executableCPU
C++Preprocess + compile + assemble + linkCPU

Nếu quên bảng thì hỏi ba câu:

  1. Source được biến thành artifact gì?
  2. Lúc chạy có runtime hoặc virtual machine nào đứng giữa không?
  3. Machine code được tạo trước khi chạy hay trong lúc chạy?

Trả lời được ba câu đó có ích hơn việc cãi nhau Python “có phải compiled language hay không”. Có compile, nhưng pipeline phổ biến của CPython vẫn cần VM thực thi bytecode. Nhãn chỉ để nói nhanh; pipeline mới là thứ đang xảy ra thật.

Tài liệu tham khảo