
ในส่วนนี้เราจะลงลึกถึงกลไก Memory Safety (ความปลอดภัยหน่วยความจำ) ของ OCaml 5 ตั้งแต่ระดับ Garbage Collector, Immutability, การจัดการ Mutable State, การทำ Interop กับ C ไปจนถึงเครื่องมือสำหรับ Memory Profiling ซึ่งเป็นหัวใจสำคัญที่ทำให้ OCaml เหมาะกับการพัฒนา IT Systems ที่ต้องการทั้งความเร็วและความน่าเชื่อถือ (correctness) โดยไม่ต้องเขียน ownership ให้ปวดหัวแบบ Rust
OCaml 5 ใช้ Generational Incremental GC ที่ออกแบบมาให้ทำงานได้ดีทั้งในโหมด single-threaded และ multicore โดยไม่ต้องเปลี่ยน API ของนักพัฒนา เข้าใจ memory model ก่อนจะช่วยให้เรา tune ระบบได้ตรงจุด
OCaml แบ่ง heap ออกเป็นสองส่วนหลัก:
%%{init: {'theme':'base', 'themeVariables': {
'background':'#282828',
'primaryColor':'#3c3836',
'primaryTextColor':'#ebdbb2',
'primaryBorderColor':'#fabd2f',
'lineColor':'#83a598',
'secondaryColor':'#504945',
'tertiaryColor':'#665c54'
}}}%%
flowchart TB
subgraph Domain0 ["🧵 Domain 0 (หลัก)"]
MH0["Minor Heap
(bump pointer)
เร็ว ~256KB"]
end
subgraph Domain1 ["🧵 Domain 1"]
MH1["Minor Heap
(bump pointer)
เร็ว ~256KB"]
end
subgraph Domain2 ["🧵 Domain 2"]
MH2["Minor Heap
(bump pointer)
เร็ว ~256KB"]
end
subgraph Shared ["🌐 Major Heap (Shared / ใช้ร่วมกัน)"]
MAJ["Free-list Allocator
Incremental Mark & Sweep
Parallel Collection"]
end
MH0 -->|"promote (เลื่อนชั้น)"| MAJ
MH1 -->|"promote (เลื่อนชั้น)"| MAJ
MH2 -->|"promote (เลื่อนชั้น)"| MAJ
style Domain0 fill:#3c3836,stroke:#b8bb26,color:#ebdbb2
style Domain1 fill:#3c3836,stroke:#b8bb26,color:#ebdbb2
style Domain2 fill:#3c3836,stroke:#b8bb26,color:#ebdbb2
style Shared fill:#3c3836,stroke:#fe8019,color:#ebdbb2
style MH0 fill:#504945,stroke:#fabd2f,color:#ebdbb2
style MH1 fill:#504945,stroke:#fabd2f,color:#ebdbb2
style MH2 fill:#504945,stroke:#fabd2f,color:#ebdbb2
style MAJ fill:#504945,stroke:#fb4934,color:#ebdbb2
Allocation (การจัดสรร) ใน minor heap ทำงานง่ายและเร็วมาก เพียงแค่ลด pointer ลงตามขนาดที่ต้องการ:
เมื่อ:
Minor GC ใช้อัลกอริทึม Cheney's copying collector ซึ่งคัดลอกเฉพาะ live objects ออกไปยัง major heap ค่าใช้จ่ายแปรผันกับจำนวนวัตถุที่ยังมีชีวิตอยู่ (proportional to live data) ไม่ใช่ขนาด heap ทั้งหมด:
เมื่อ คือจำนวน word ของวัตถุที่ยังมีชีวิตเมื่อเกิด minor collection นี่คือเหตุผลที่ allocation ที่ตายเร็วถือว่า "ฟรี" ใน OCaml
Major GC เป็น incremental mark-and-sweep ทำงานเป็น slices เล็กๆ ที่แทรกกับงานปกติ จึงไม่หยุด (stop-the-world) นาน ใน OCaml 5 มีการเพิ่ม parallel marking ให้ Domain ช่วยกัน mark ได้
OCaml เปิดให้ปรับพารามิเตอร์ของ GC ผ่าน Gc.set พารามิเตอร์หลักที่ควรรู้:
| Parameter | ค่าเริ่มต้น | ความหมาย | เมื่อไหร่ควรปรับ |
|---|---|---|---|
minor_heap_size |
262144 words (2MB) | ขนาด minor heap ต่อ Domain | เพิ่มเมื่อมี allocation churn สูง, ลดเมื่อ cache-sensitive |
space_overhead |
120 | % overhead ที่ยอมให้ก่อนเริ่ม major GC | ลด → GC บ่อย, ใช้ RAM น้อย / เพิ่ม → GC น้อย, ใช้ RAM มาก |
max_overhead |
500 | threshold ที่จะ compact | เพิ่มถ้าไม่อยากให้ compact เลย |
allocation_policy |
2 (best-fit) | อัลกอริทึม free-list | 2 = best-fit (default), 1 = first-fit, 0 = next-fit |
verbose |
0 | bitmask สำหรับ log | 0x01 สำหรับ start of major, 0x02 สำหรับ minor |
ตัวอย่าง: ปรับ GC สำหรับ low-latency server
(* File: gc_tuning.ml *)
(* ตัวอย่างการปรับ GC ให้เหมาะกับงาน server ที่ต้องการ latency ต่ำ *)
let configure_low_latency_gc () =
let current = Gc.get () in
Gc.set {
current with
(* เพิ่ม minor heap เป็น 8MB เพื่อลดความถี่ของ minor GC *)
minor_heap_size = 8 * 1024 * 1024 / 8; (* 8MB → words *)
(* ลด space_overhead ให้ major GC ทำงานถี่ขึ้น แต่ละรอบสั้น *)
space_overhead = 80;
(* ตั้ง allocation policy เป็น best-fit เพื่อลด fragmentation *)
allocation_policy = 2;
(* log เมื่อเกิด major GC (bit 0x01) เพื่อ monitoring *)
verbose = 0x01;
}
(* ฟังก์ชันสำหรับดูสถิติ GC ปัจจุบัน *)
let print_gc_stats () =
let stat = Gc.stat () in
Printf.printf "=== GC Statistics ===\n";
Printf.printf "Minor collections: %d\n" stat.minor_collections;
Printf.printf "Major collections: %d\n" stat.major_collections;
Printf.printf "Heap words: %d (%.2f MB)\n"
stat.heap_words
(float_of_int (stat.heap_words * 8) /. 1024.0 /. 1024.0);
Printf.printf "Live words: %d\n" stat.live_words;
Printf.printf "Free words: %d\n" stat.free_words;
Printf.printf "Top heap words: %d\n" stat.top_heap_words;
Printf.printf "Compactions: %d\n" stat.compactions
(* ตัวอย่างการใช้งาน *)
let () =
configure_low_latency_gc ();
(* ทำงานจริงของ application... *)
let _big_list = List.init 1_000_000 (fun i -> i * i) in
print_gc_stats ()
แม้ OCaml GC จะ incremental แต่ก็ยังมี pause บางช่วง โดยเฉพาะตอน minor collection (stop-the-Domain) ซึ่งยาวประมาณ:
เมื่อ คือขนาดของ live data และ คือ bandwidth ของการ copy (ปกติ ~ 1-2 GB/s)
เทคนิคลด GC pause:
Bytes.unsafe_set แทน ^ (string concat), ใช้ Buffer สำหรับ string buildingBigarray สำหรับข้อมูลใหญ่ที่ไม่จำเป็นต้องให้ GC ดูแลzero_alloc attribute (OCaml 5.1+) — คอมไพเลอร์จะแจ้ง error ถ้าฟังก์ชัน allocate ในพาธนั้น(* ตัวอย่าง: ฟังก์ชันรับประกัน zero allocation *)
let[@zero_alloc] sum_array (arr : int array) =
let n = Array.length arr in
let acc = ref 0 in
for i = 0 to n - 1 do
acc := !acc + Array.unsafe_get arr i
done;
!acc
(* ถ้าเผลอเพิ่ม List.map ข้างใน compiler จะ error เพราะ allocate *)
Gc.compact () บังคับให้เกิด major GC + defragmentation เต็มรูปแบบ ซึ่ง ช้ามาก และหยุดระบบไป อย่าเรียกบ่อย เหมาะกับ:
(* ตัวอย่าง: warmup แล้ว compact ก่อนรับ request จริง *)
let warmup_then_serve () =
(* จำลอง workload เพื่อ warmup JIT-ish paths และ fill caches *)
for _ = 1 to 10_000 do
let _ = process_sample_request () in ()
done;
(* บังคับ compact เพื่อเคลียร์ heap ก่อนเริ่ม production traffic *)
Gc.compact ();
Printf.printf "Warmup complete. Starting server...\n";
start_server ()
and process_sample_request () = 42
and start_server () = ()
Immutability (ความไม่เปลี่ยนแปลง) เป็นหัวใจของ OCaml ที่ทำให้โค้ดปลอดภัยจาก memory bugs ส่วนใหญ่โดยอัตโนมัติ
ใน C++ หรือ Go ถ้า thread สองตัวถือ pointer ไปยัง object เดียวกันและตัวหนึ่ง mutate จะเกิด data race แต่ใน OCaml ถ้าข้อมูลเป็น immutable (ปกติคือ default) จะไม่มีใคร mutate ได้เลย นี่คือสิ่งที่ทำให้ sharing ระหว่าง Domain ใน OCaml 5 ปลอดภัย ถ้าเป็น immutable data
Properties ที่ได้มาฟรี:
Persistent data structures (โครงสร้างข้อมูลถาวร) คือโครงสร้างที่เมื่อ "แก้ไข" จะคืนค่าใหม่โดยไม่ทำลายของเก่า ใช้เทคนิค Structural Sharing (การแชร์โครงสร้าง) เพื่อไม่ต้อง copy ทั้งหมด
%%{init: {'theme':'base', 'themeVariables': {
'background':'#282828',
'primaryColor':'#3c3836',
'primaryTextColor':'#ebdbb2',
'primaryBorderColor':'#fabd2f',
'lineColor':'#83a598',
'secondaryColor':'#504945'
}}}%%
flowchart TB
subgraph Before ["🔵 Before: tree เดิม (root = A)"]
A1["A (root เก่า)"]
B1["B"]
C1["C"]
D1["D (leaf)"]
E1["E (leaf)"]
A1 --> B1
A1 --> C1
B1 --> D1
B1 --> E1
end
subgraph After ["🟡 After: update leaf D → D' (root = A')"]
A2["A' (root ใหม่)"]
B2["B' (copy)"]
D2["D' (leaf ใหม่)"]
A2 --> B2
A2 -.->|"SHARED ⚡"| C1
B2 --> D2
B2 -.->|"SHARED ⚡"| E1
end
style A1 fill:#458588,stroke:#83a598,color:#ebdbb2
style B1 fill:#458588,stroke:#83a598,color:#ebdbb2
style C1 fill:#98971a,stroke:#b8bb26,color:#282828
style D1 fill:#458588,stroke:#83a598,color:#ebdbb2
style E1 fill:#98971a,stroke:#b8bb26,color:#282828
style A2 fill:#d79921,stroke:#fabd2f,color:#282828
style B2 fill:#d79921,stroke:#fabd2f,color:#282828
style D2 fill:#cc241d,stroke:#fb4934,color:#ebdbb2
style Before fill:#3c3836,stroke:#83a598
style After fill:#3c3836,stroke:#fabd2f
Cost ของการ update สำหรับ persistent map (balanced tree) คือ:
คือใช้เวลาและจัดสรร memory ใหม่เพียง nodes ขณะที่ nodes ที่เหลือถูก share กับโครงสร้างเดิม
(* File: persistent_map_demo.ml *)
(* ตัวอย่างการใช้ Map ของ OCaml ซึ่งเป็น persistent balanced binary tree *)
(* สร้าง module ของ Map ที่ key เป็น string *)
module StringMap = Map.Make(String)
(* สร้าง map เริ่มต้นจำลองฐานข้อมูลพนักงาน *)
let employees_v1 =
StringMap.empty
|> StringMap.add "E001" ("สมชาย", 35000)
|> StringMap.add "E002" ("สมหญิง", 42000)
|> StringMap.add "E003" ("วิชัย", 38000)
(* update salary ของ E002 → ได้ map ใหม่โดยของเดิมไม่เปลี่ยน *)
let employees_v2 =
StringMap.update "E002" (function
| Some (name, _salary) -> Some (name, 48000) (* ขึ้นเงินเดือน *)
| None -> None
) employees_v1
(* เพิ่มพนักงานใหม่ → ได้ map ใหม่อีกตัว *)
let employees_v3 =
StringMap.add "E004" ("ประภา", 40000) employees_v2
(* ฟังก์ชันแสดงข้อมูลทั้ง map *)
let print_snapshot label m =
Printf.printf "\n=== %s ===\n" label;
StringMap.iter (fun id (name, salary) ->
Printf.printf " %s | %-15s | %d บาท\n" id name salary
) m
(* ทดสอบ: ทั้งสาม version มีอยู่พร้อมกัน ไม่มีอันไหนถูกทำลาย
นี่คือคุณสมบัติ "persistent" — สามารถ time-travel ได้ *)
let () =
print_snapshot "Snapshot v1 (เริ่มต้น)" employees_v1;
print_snapshot "Snapshot v2 (หลังขึ้นเงินเดือน E002)" employees_v2;
print_snapshot "Snapshot v3 (หลังรับ E004)" employees_v3;
Printf.printf "\nv1 ยังมี E002 เงินเดือนเดิม: %s\n"
(match StringMap.find_opt "E002" employees_v1 with
| Some (_, s) -> string_of_int s
| None -> "-")
Output ที่คาดหวัง:
=== Snapshot v1 (เริ่มต้น) ===
E001 | สมชาย | 35000 บาท
E002 | สมหญิง | 42000 บาท
E003 | วิชัย | 38000 บาท
=== Snapshot v2 (หลังขึ้นเงินเดือน E002) ===
E001 | สมชาย | 35000 บาท
E002 | สมหญิง | 48000 บาท
E003 | วิชัย | 38000 บาท
=== Snapshot v3 (หลังรับ E004) ===
E001 | สมชาย | 35000 บาท
E002 | สมหญิง | 48000 บาท
E003 | วิชัย | 38000 บาท
E004 | ประภา | 40000 บาท
v1 ยังมี E002 เงินเดือนเดิม: 42000
Persistent structure ไม่ใช่ของฟรี — มี overhead จาก allocation
| คุณสมบัติ | Mutable (Hashtbl) | Persistent (Map) |
|---|---|---|
| Lookup | O(1) เฉลี่ย | O(log n) |
| Insert/Update | O(1) เฉลี่ย | O(log n) + allocate log n nodes |
| Copy/Snapshot | O(n) ต้อง deep copy | O(1) แค่ share root pointer |
| Concurrent read (หลาย Domain) | ต้อง lock | ไม่ต้อง lock |
| GC pressure | ต่ำ | สูงกว่า (allocate ทุก update) |
| Use case | hot loop, single-thread cache | shared config, undo/redo, time-travel |
หลักการเลือกใช้:
(* ตัวอย่าง: immutable record + functional update syntax *)
type server_config = {
host : string;
port : int;
max_connections : int;
tls_enabled : bool;
worker_domains : int;
}
let default_config = {
host = "0.0.0.0";
port = 8080;
max_connections = 1000;
tls_enabled = false;
worker_domains = 4;
}
(* Functional update: สร้าง record ใหม่จากอันเก่า เปลี่ยนเฉพาะ field ที่ระบุ
(ฟิลด์อื่นจะถูก "share" แบบไม่ต้อง copy ค่าที่เป็น immutable) *)
let production_config =
{ default_config with
port = 443;
tls_enabled = true;
worker_domains = 16 }
let () =
Printf.printf "Default: %s:%d (workers=%d)\n"
default_config.host default_config.port default_config.worker_domains;
Printf.printf "Production: %s:%d (workers=%d, TLS=%b)\n"
production_config.host production_config.port
production_config.worker_domains production_config.tls_enabled
แม้ immutability จะเป็น default แต่ systems code บางครั้งต้องการ mutable state จริงๆ OCaml ให้เครื่องมือที่ชัดเจนและ explicit เพื่อให้นักพัฒนา เห็น ว่าที่ไหนมี mutation
OCaml มีสองกลไกหลักสำหรับ mutable state:
ref cell — kernel เล็กๆ ที่เป็น record มี field contents แก้ได้: let counter = ref 0 in counter := !counter + 1mutable record fields — field ใน record ที่ประกาศด้วย keyword mutable แก้ได้ด้วย <-(* File: mutable_demo.ml *)
(* ตัวอย่าง: mutable state แบบต่างๆ *)
(* แบบที่ 1: ref cell สำหรับค่าเดียว *)
let global_request_count = ref 0
let handle_request () =
incr global_request_count; (* syntactic sugar ของ := !global_request_count + 1 *)
Printf.printf "Request #%d\n" !global_request_count
(* แบบที่ 2: mutable record fields สำหรับ stateful object *)
type connection_pool = {
max_size : int; (* immutable: config *)
mutable active : int; (* mutable: running state *)
mutable total_served : int; (* mutable: counter *)
mutable last_error : string option; (* mutable: last event *)
}
let create_pool ~max_size = {
max_size;
active = 0;
total_served = 0;
last_error = None;
}
let acquire pool =
if pool.active >= pool.max_size then
(pool.last_error <- Some "pool exhausted"; Error "pool full")
else begin
pool.active <- pool.active + 1;
pool.total_served <- pool.total_served + 1;
Ok pool.active
end
let release pool =
if pool.active > 0 then pool.active <- pool.active - 1
(* ตัวอย่างการใช้งาน *)
let () =
let pool = create_pool ~max_size:3 in
for _ = 1 to 5 do handle_request () done;
let _ = acquire pool in
let _ = acquire pool in
let _ = acquire pool in
(match acquire pool with
| Ok n -> Printf.printf "Got connection #%d\n" n
| Error e -> Printf.printf "Error: %s\n" e);
release pool;
Printf.printf "Pool: active=%d, served=%d\n" pool.active pool.total_served
ใช้ mutable state เมื่อ:
array เป็น mutable โดย designหลีกเลี่ยง mutable เมื่อ:
Atomic)Pattern ที่สำคัญที่สุด: ซ่อน mutation ไว้หลัง module signature เพื่อให้ภายนอกเห็นเป็น pure interface
(* File: counter.mli — signature บริสุทธิ์ ไม่เห็น mutation *)
type t
val create : unit -> t
val increment : t -> unit
val value : t -> int
val reset : t -> unit
(* File: counter.ml — ภายในมี mutable แต่ซ่อนไว้ *)
type t = { mutable n : int }
let create () = { n = 0 }
let increment c = c.n <- c.n + 1
let value c = c.n
let reset c = c.n <- 0
(* คนที่ใช้ counter.mli จะเห็นแค่ interface abstract
ไม่สามารถเข้าไปแอบแก้ .n ได้ *)
Aliasing bug คือเมื่อ pointer สองตัวชี้ไปที่ข้อมูลเดียวกันและตัวหนึ่ง mutate ทำให้อีกตัวเห็นการเปลี่ยนแปลงที่ไม่คาดคิด OCaml ป้องกันด้วยเทคนิค:
ตัวอย่าง Phantom type สำหรับ read/write permission:
(* phantom type: 'perm จะเป็น `read หรือ `write โดย compile-time ตรวจสอบ *)
module Buffer : sig
type 'perm t
val create : int -> [`read | `write] t
val to_readonly : [`read | `write] t -> [`read] t
val read : [> `read] t -> int -> char
val write : [`write] t -> int -> char -> unit (* เฉพาะ write permission เท่านั้น *)
end = struct
type 'perm t = bytes
let create n = Bytes.create n
let to_readonly b = b
let read b i = Bytes.get b i
let write b i c = Bytes.set b i c
end
(* ทดสอบ: ถ้า demote แล้วพยายาม write จะ compile error *)
let () =
let buf = Buffer.create 10 in
Buffer.write buf 0 'A'; (* OK: write permission *)
let ro = Buffer.to_readonly buf in
Printf.printf "%c\n" (Buffer.read ro 0)
(* ถ้าเขียน: Buffer.write ro 0 'X' → compile error! *)
นี่คือ memory safety ที่บังคับโดย type system — คล้ายกับ ownership ใน Rust แต่เบากว่าและยืดหยุ่นกว่า
เมื่อเห็น mutable code ให้ถามตัวเอง 5 ข้อ:
active <= max_size ต้องจริงเสมอOCaml มี FFI (Foreign Function Interface) ที่แข็งแกร่งเพื่อเรียกใช้ไลบรารี C การเชื่อมต่อกับโลก unmanaged เป็นจุดที่ต้องระวังที่สุดเรื่อง memory safety
OCaml FFI ทำงานผ่าน 2 ระดับ:
CAMLparam, CAMLreturn, CAMLlocal เพื่อลงทะเบียน GC rootctypes ประกาศ type ของ C function แล้ว marshal อัตโนมัติ
%%{init: {'theme':'base', 'themeVariables': {
'background':'#282828',
'primaryColor':'#3c3836',
'primaryTextColor':'#ebdbb2',
'primaryBorderColor':'#fabd2f',
'lineColor':'#83a598'
}}}%%
flowchart LR
subgraph OCaml ["🐫 OCaml World"]
OC["OCaml code
(GC-managed heap)"]
STUB["C stubs
(CAMLparam/CAMLreturn)"]
end
subgraph C_World ["⚙️ C World"]
CAPI["C library
(malloc/free)"]
end
OC -->|"external keyword"| STUB
STUB -->|"normal C call"| CAPI
CAPI -.->|"return ptr/value"| STUB
STUB -.->|"Val_int / caml_copy_string"| OC
style OCaml fill:#3c3836,stroke:#b8bb26,color:#ebdbb2
style C_World fill:#3c3836,stroke:#fe8019,color:#ebdbb2
style OC fill:#458588,stroke:#83a598,color:#ebdbb2
style STUB fill:#d79921,stroke:#fabd2f,color:#282828
style CAPI fill:#cc241d,stroke:#fb4934,color:#ebdbb2
ไฟล์ stubs.c (C side):
/* File: stubs.c — C stub สำหรับเรียกใช้จาก OCaml */
#define CAML_NAME_SPACE
#include <caml/mlvalues.h>
#include <caml/alloc.h>
#include <caml/memory.h>
#include <caml/fail.h>
#include <string.h>
/* ฟังก์ชัน: รับ string OCaml แล้วคืน length (ตัวอย่างง่าย) */
CAMLprim value caml_fast_strlen(value v_str) {
CAMLparam1(v_str); /* ลงทะเบียน v_str เป็น GC root */
const char *s = String_val(v_str); /* ดึง pointer ของ OCaml string */
size_t len = strlen(s);
CAMLreturn(Val_int(len)); /* แปลง int → OCaml value */
}
/* ฟังก์ชัน: allocate buffer นอก heap OCaml (ต้อง free เองใน OCaml!) */
CAMLprim value caml_alloc_external(value v_size) {
CAMLparam1(v_size);
CAMLlocal1(result); /* ลงทะเบียน local variable ไว้ */
intnat size = Long_val(v_size);
void *ptr = malloc(size);
if (!ptr) caml_failwith("alloc_external: out of memory");
/* wrap pointer เป็น Int64 ให้ OCaml จัดการ (ต้อง free เอง!) */
result = caml_copy_int64((int64_t)(intptr_t)ptr);
CAMLreturn(result);
}
ไฟล์ fast_lib.ml (OCaml side):
(* File: fast_lib.ml — OCaml binding ของ stub *)
(* ประกาศฟังก์ชันที่ implement ใน C *)
external fast_strlen : string -> int = "caml_fast_strlen"
external alloc_external : int -> int64 = "caml_alloc_external"
(* ใช้งาน *)
let () =
let s = "สวัสดี Hello 🌍" in
Printf.printf "Byte length of %S = %d\n" s (fast_strlen s);
let ptr = alloc_external 1024 in
Printf.printf "Allocated at 0x%Lx (remember to free!)\n" ptr
ไฟล์ dune (build config):
(library
(name fast_lib)
(public_name fast_lib)
(c_library_flags (-lc))
(foreign_stubs (language c) (names stubs)))
ctypes ช่วยประกาศ C signature เป็น OCaml expression โดย compile-time ไม่ต้องเขียน C stub เอง:
(* File: libm_bindings.ml *)
(* ใช้ ctypes ผูกกับ libm (math library) โดยไม่ต้องเขียน .c เอง *)
open Ctypes
open Foreign
(* ประกาศ binding ของฟังก์ชัน C: double sqrt(double x) *)
let c_sqrt = foreign "sqrt" (double @-> returning double)
(* ฟังก์ชัน C: double pow(double base, double exp) *)
let c_pow = foreign "pow" (double @-> double @-> returning double)
(* ห่อด้วย interface ที่ใช้สบายใน OCaml *)
let sqrt_fast x = c_sqrt x
let pow_fast base exp = c_pow base exp
let () =
Printf.printf "sqrt(2.0) = %f\n" (sqrt_fast 2.0);
Printf.printf "2^10 = %f\n" (pow_fast 2.0 10.0)
ข้อดีของ ctypes:
Bigarray คือ array ที่จัดสรรนอก OCaml heap ใช้สำหรับ:
(* File: bigarray_demo.ml *)
(* ตัวอย่าง: ใช้ Bigarray เก็บ image pixels แบบ off-heap *)
open Bigarray
(* สร้าง 2D array ขนาด 1920×1080 ของ uint8 (image buffer)
- Array2: 2 มิติ
- Int8_unsigned: uint8_t ใน C
- C_layout: row-major (เหมือน C) *)
let create_image_buffer ~width ~height =
Array2.create Int8_unsigned C_layout height width
(* ฟังก์ชันเติมค่า gradient (ตัวอย่าง computation) *)
let fill_gradient buf =
let h = Array2.dim1 buf in
let w = Array2.dim2 buf in
for y = 0 to h - 1 do
for x = 0 to w - 1 do
(* Array2.unsafe_set เลี่ยง bounds check — เร็วขึ้นแต่ระวังด้วย *)
Array2.unsafe_set buf y x ((x * 255 / w) land 0xFF)
done
done
(* คำนวณขนาด memory ที่ใช้ *)
let bytes_used buf =
Array2.dim1 buf * Array2.dim2 buf * (kind_size_in_bytes (Array2.kind buf))
(* ทดสอบ *)
let () =
let img = create_image_buffer ~width:1920 ~height:1080 in
fill_gradient img;
Printf.printf "Image: %dx%d, off-heap bytes: %d (%.2f MB)\n"
(Array2.dim2 img) (Array2.dim1 img)
(bytes_used img)
(float_of_int (bytes_used img) /. 1024.0 /. 1024.0);
(* ข้อมูลนี้อยู่ **นอก** OCaml heap → ไม่เพิ่ม GC pressure *)
let stat = Gc.stat () in
Printf.printf "OCaml heap words: %d (ไม่รวม Bigarray)\n" stat.heap_words
อันตรายที่สุดตอนข้าม FFI boundary คือ:
กฎป้องกัน use-after-free:
caml_register_global_root / caml_remove_global_root สำหรับ persistent rootCAMLparam ทุก argument ที่รับมาตัวอย่าง pinning แบบถูกต้อง:
/* C stub: register OCaml callback ไว้เป็น persistent root */
static value stored_callback = Val_unit;
CAMLprim value caml_register_callback(value cb) {
CAMLparam1(cb);
if (stored_callback != Val_unit) {
caml_remove_global_root(&stored_callback);
}
stored_callback = cb;
caml_register_global_root(&stored_callback); /* pin ไว้ ไม่ให้ GC เก็บ */
CAMLreturn(Val_unit);
}
CAMLprim value caml_trigger_callback(value arg) {
CAMLparam1(arg);
CAMLlocal1(result);
if (stored_callback == Val_unit) caml_failwith("no callback registered");
result = caml_callback(stored_callback, arg);
CAMLreturn(result);
}
สำหรับ resource ภายนอก (file handle, C pointer, socket) ใช้ Gc.finalise เพื่อผูก cleanup function เข้ากับ value:
(* File: external_resource.ml *)
(* ตัวอย่าง: wrap C pointer ให้ auto-free เมื่อ GC เก็บ *)
type c_buffer = { ptr : int64; size : int }
external c_alloc : int -> int64 = "caml_alloc_external"
external c_free : int64 -> unit = "caml_free_external"
let create_buffer size =
let buf = { ptr = c_alloc size; size } in
(* ผูก finaliser: เมื่อ GC เก็บ buf จะเรียก c_free ก่อน *)
Gc.finalise (fun b ->
Printf.eprintf "[GC] freeing C buffer at 0x%Lx\n" b.ptr;
c_free b.ptr
) buf;
buf
(* ⚠️ ข้อควรระวัง:
1. Finaliser อาจไม่ถูกเรียกถ้าโปรแกรมออกก่อน GC เก็บ → ใช้ at_exit ด้วย
2. เรียงลำดับ finaliser ไม่แน่นอน → ห้ามพึ่งพากัน
3. ห้าม allocate ใน finaliser มาก — อยู่ใน GC context *)
let () =
let _b = create_buffer 1024 in
Gc.compact (); (* บังคับ GC เพื่อเห็น finaliser ทำงาน *)
Printf.printf "Done\n"
การ profile memory ใน OCaml มีเครื่องมือที่ทรงพลังและ integrate ดีกับ GC
memtrace (โดย Jane Street) เป็น profiler ที่ sample allocation โดยมี overhead ต่ำมาก (~5%) จึงใช้ใน production ได้ หลักการคือ:
เมื่อ คือ sampling rate (words) allocation ขนาด size จะถูก sample ตามการแจกแจง Poisson ทำให้ผลลัพธ์ไม่ bias ต่อ allocation ขนาดเล็กหรือใหญ่
การใช้งาน:
(* File: memtrace_example.ml *)
(* ใช้งาน memtrace ในโปรแกรมจริง *)
let process_batch () =
(* จำลอง allocation pattern ที่ต้องหา leak *)
let cache = Hashtbl.create 1000 in
for i = 0 to 100_000 do
let key = Printf.sprintf "key_%d" i in
let value = String.init 100 (fun _ -> Char.chr (i mod 256)) in
Hashtbl.add cache key value (* ตั้งใจใช้ add แทน replace → leak! *)
done;
Hashtbl.length cache
let () =
(* เริ่ม tracing เขียน log ไปไฟล์ *)
let tracer = Memtrace.start_tracing
~context:None
~sampling_rate:1e-4 (* sample 1 ใน 10000 words *)
~filename:"trace.ctf" in
(* workload จริง *)
let n = process_batch () in
Printf.printf "Cache size: %d\n" n;
(* หยุด tracing *)
Memtrace.stop_tracing tracer;
Printf.printf "Trace saved to trace.ctf\n"
วิเคราะห์ผลลัพธ์ ด้วย memtrace-viewer:
# ติดตั้ง (ครั้งแรก)
opam install memtrace_viewer
# เปิดดูผล trace ผ่าน browser
memtrace-viewer trace.ctf
# เปิด http://localhost:8080
ใน viewer จะเห็น flame graph ของ allocation แยกตาม call stack ช่วยหาจุดที่ allocate เยอะผิดปกติ
"Leak" ใน OCaml ต่างจาก C — ไม่ใช่ memory ที่ลืม free แต่เป็น reference ที่ลืมลบทิ้ง ทำให้ GC เก็บไม่ได้ Pattern ที่เจอบ่อย:
เทคนิคหา leak:
(* File: leak_detector.ml *)
(* เทคนิค: ถ่าย snapshot ก่อน-หลัง แล้วหา diff *)
let snapshot_heap label =
Gc.compact (); (* บังคับ GC ก่อน snapshot *)
let s = Gc.stat () in
Printf.printf "[%s] heap_words=%d live_words=%d live_blocks=%d\n"
label s.heap_words s.live_words s.live_blocks;
s
let detect_leak_in f =
let before = snapshot_heap "BEFORE" in
f ();
Gc.compact ();
let after = snapshot_heap "AFTER" in
let delta = after.live_words - before.live_words in
if delta > 1000 then
Printf.printf "⚠️ POSSIBLE LEAK: +%d live words\n" delta
else
Printf.printf "✅ No significant leak (+%d words)\n" delta
(* ทดสอบ: function มี leak (ไม่เคล้า global cache) *)
let global_cache : (int, string) Hashtbl.t = Hashtbl.create 16
let leaky_function () =
for i = 0 to 10_000 do
Hashtbl.add global_cache i (Printf.sprintf "entry_%d" i)
done
let clean_function () =
let local = Hashtbl.create 16 in
for i = 0 to 10_000 do
Hashtbl.add local i (Printf.sprintf "entry_%d" i)
done
(* local ถูก GC เก็บเมื่อ function คืนค่า → ไม่ leak *)
let () =
Printf.printf "\n=== Test 1: leaky ===\n";
detect_leak_in leaky_function;
Printf.printf "\n=== Test 2: clean ===\n";
detect_leak_in clean_function
เมื่อใช้ C stubs หรือ Ctypes bindings memory bug ส่วนใหญ่จะมาจากฝั่ง C ใช้ valgrind ตรวจ:
# build debug version
dune build --profile dev
# รันผ่าน valgrind
valgrind --tool=memcheck \
--leak-check=full \
--show-leak-kinds=all \
--track-origins=yes \
--suppressions=ocaml.supp \
./_build/default/myapp.exe
# ocaml.supp คือ suppressions file ตัด false positives ของ OCaml runtime
# ดาวน์โหลดได้จาก OCaml repo: tools/ocamlrun-valgrind.supp
⚠️ Valgrind จะรายงาน false positive จำนวนมากจาก OCaml runtime (เช่น GC's intentional reads of uninitialized memory) ต้องใช้ suppressions file
การใช้ landmarks library ทำ manual instrumentation ได้:
(* File: landmarks_demo.ml *)
(* ต้อง build ด้วย: dune build --instrument-with landmarks *)
let[@landmark] parse_request buf =
(* ... parsing logic ... *)
String.length buf
let[@landmark] handle_request req =
let _ = parse_request req in
(* ... handling ... *)
"OK"
let[@landmark "main"] main () =
for _ = 1 to 1000 do
ignore (handle_request "GET /index.html")
done
let () =
main ();
(* พิมพ์รายงานตอนจบ: เวลา, allocation, call count ของแต่ละ landmark *)
Landmark.export ~filename:"landmarks.json" ()
รัน:
OCAML_LANDMARKS=on,format=json,output=stderr \
./_build/default/landmarks_demo.exe 2> report.json
OCaml เคยมี spacetime profiler แต่ปัจจุบันใช้ memtrace แทน snapshot หลาย timestamp เพื่อหา "ของที่โต":
(* File: heap_diff.ml *)
(* เทคนิคเปรียบเทียบ snapshot หลายช่วงเวลา *)
let profile_phase name f =
let t0 = Unix.gettimeofday () in
Gc.compact ();
let before = Gc.stat () in
let result = f () in
Gc.compact ();
let after = Gc.stat () in
let t1 = Unix.gettimeofday () in
Printf.printf "%-20s | time=%7.3fs | heap_delta=%+d words | live_delta=%+d\n"
name (t1 -. t0)
(after.heap_words - before.heap_words)
(after.live_words - before.live_words);
result
let () =
let _ = profile_phase "phase_1_parse" (fun () ->
List.init 100_000 (fun i -> Printf.sprintf "item_%d" i)
) in
let big = profile_phase "phase_2_process" (fun () ->
List.init 100_000 (fun i -> (i, i * i, Printf.sprintf "v%d" i))
) in
let _ = profile_phase "phase_3_summarize" (fun () ->
List.fold_left (fun (sa, sb) (a, b, _) -> sa + a, sb + b) (0, 0) big
) in
()
Gc.stat ของ workload ปัจจุบันก่อนปรับอะไรlive_words โตขึ้นต่อเนื่อง → หา reference ที่ค้างminor_heap_size, space_overhead ตาม workload
%%{init: {'theme':'base', 'themeVariables': {
'background':'#282828',
'primaryColor':'#3c3836',
'primaryTextColor':'#ebdbb2',
'primaryBorderColor':'#fabd2f',
'lineColor':'#83a598'
}}}%%
flowchart TD
A[📊 ตั้ง Baseline
Gc.stat] --> B[🔬 เปิด memtrace
sampling rate 1e-4]
B --> C[🏃 รัน workload จริง
1-5 นาที]
C --> D[🔥 เปิด memtrace-viewer
วิเคราะห์ flame graph]
D --> E{มี leak
หรือ hotspot?}
E -->|Yes| F[🛠️ แก้โค้ด:
ลด allocation /
clear references]
E -->|No| G[⚙️ ปรับ GC params
minor_heap, space_overhead]
F --> H[📏 Re-measure]
G --> H
H --> I{ดีขึ้น
จาก baseline?}
I -->|Yes| J[✅ เพิ่ม regression test
ใน CI]
I -->|No| D
style A fill:#458588,stroke:#83a598,color:#ebdbb2
style B fill:#458588,stroke:#83a598,color:#ebdbb2
style C fill:#458588,stroke:#83a598,color:#ebdbb2
style D fill:#d79921,stroke:#fabd2f,color:#282828
style E fill:#b16286,stroke:#d3869b,color:#ebdbb2
style F fill:#cc241d,stroke:#fb4934,color:#ebdbb2
style G fill:#689d6a,stroke:#8ec07c,color:#ebdbb2
style H fill:#458588,stroke:#83a598,color:#ebdbb2
style I fill:#b16286,stroke:#d3869b,color:#ebdbb2
style J fill:#98971a,stroke:#b8bb26,color:#282828
| เครื่องมือ | Overhead | สิ่งที่เห็น | เหมาะกับ | ข้อจำกัด |
|---|---|---|---|---|
Gc.stat |
~0% | heap stats ตอน point-in-time | quick check, CI assertion | ไม่มี call stack |
memtrace |
~5% | allocation by call stack (sampled) | production profiling | sampling bias ต่ำมาก |
landmarks |
~2-10% | manual timing + allocation by landmark | แก่น hot path | ต้อง annotate เอง |
perf (Linux) |
~3% | CPU sampling, เห็น C + OCaml frames | CPU-bound analysis | ต้อง compile ด้วย frame pointers |
valgrind memcheck |
20-50× | leak + UB ใน C stubs | หา bug ใน FFI | ช้ามาก, false positives |
eBPF via bpftrace |
~1% | syscalls, I/O, custom probes | production observability | ต้อง kernel support |
OCaml 5 มอบ memory safety ผ่านหลายชั้นที่ทำงานร่วมกัน ตั้งแต่ type system ที่ป้องกัน null และ aliasing, GC ที่จัดการ lifetime อัตโนมัติ, persistent data structures ที่ทำให้ sharing ปลอดภัยโดย default, ไปจนถึง FFI ที่ยังต้องใช้วินัยเวลาข้ามไปโลก C แม้จะไม่เข้มงวดเท่า ownership ของ Rust แต่ก็ให้ productivity ที่สูงกว่าและ correctness ที่เพียงพอสำหรับ systems work ส่วนใหญ่ เมื่อมี bug memory profiling tools อย่าง memtrace ก็ให้ข้อมูลลึกพอจะแก้ได้อย่างมั่นใจ
ในส่วนถัดไป (ส่วนที่ 4) เราจะดูว่า OCaml 5 ใช้ memory model นี้มารองรับ true parallelism ผ่าน Domains, Effects และ Eio อย่างไร — ซึ่งเป็น game changer ที่สุดของ OCaml 5