/static/codemoomoo.png

OCaml 5 สำหรับการพัฒนาระบบ IT: Memory Safety, Concurrency และ Multicore

Part 3 — Memory Safety in OCaml 5

ในส่วนนี้เราจะลงลึกถึงกลไก Memory Safety (ความปลอดภัยหน่วยความจำ) ของ OCaml 5 ตั้งแต่ระดับ Garbage Collector, Immutability, การจัดการ Mutable State, การทำ Interop กับ C ไปจนถึงเครื่องมือสำหรับ Memory Profiling ซึ่งเป็นหัวใจสำคัญที่ทำให้ OCaml เหมาะกับการพัฒนา IT Systems ที่ต้องการทั้งความเร็วและความน่าเชื่อถือ (correctness) โดยไม่ต้องเขียน ownership ให้ปวดหัวแบบ Rust


7. Memory Model และ Garbage Collector

OCaml 5 ใช้ Generational Incremental GC ที่ออกแบบมาให้ทำงานได้ดีทั้งในโหมด single-threaded และ multicore โดยไม่ต้องเปลี่ยน API ของนักพัฒนา เข้าใจ memory model ก่อนจะช่วยให้เรา tune ระบบได้ตรงจุด

7.1 ภาพรวมของ Heap Architecture

OCaml แบ่ง heap ออกเป็นสองส่วนหลัก:

  1. Minor Heap (heap รุ่นเยาว์) — เป็น bump-pointer allocator ที่จองเร็วมาก ใช้สำหรับ short-lived objects (วัตถุที่อายุสั้น) ตามหลัก Generational Hypothesis คือ "วัตถุส่วนใหญ่ตายเร็ว" ใน OCaml 5 แต่ละ Domain มี minor heap ของตัวเอง (ขนาดเริ่มต้น ~ 256KB)
  2. Major Heap (heap รุ่นโต) — เป็น free-list allocator ใช้สำหรับ long-lived objects วัตถุที่รอดจาก minor GC จะถูก promote มาที่นี่ major heap เป็น shared ข้าม Domain ทั้งหมดใน OCaml 5
%%{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

7.2 Generational GC: การจัดสรรและเก็บกวาด

Allocation (การจัดสรร) ใน minor heap ทำงานง่ายและเร็วมาก เพียงแค่ลด pointer ลงตามขนาดที่ต้องการ:

pnew = pcurrent - ( size + 1 ) × word

เมื่อ:

Minor GC ใช้อัลกอริทึม Cheney's copying collector ซึ่งคัดลอกเฉพาะ live objects ออกไปยัง major heap ค่าใช้จ่ายแปรผันกับจำนวนวัตถุที่ยังมีชีวิตอยู่ (proportional to live data) ไม่ใช่ขนาด heap ทั้งหมด:

Tminor Nlive

เมื่อ Nlive คือจำนวน word ของวัตถุที่ยังมีชีวิตเมื่อเกิด minor collection นี่คือเหตุผลที่ allocation ที่ตายเร็วถือว่า "ฟรี" ใน OCaml

Major GC เป็น incremental mark-and-sweep ทำงานเป็น slices เล็กๆ ที่แทรกกับงานปกติ จึงไม่หยุด (stop-the-world) นาน ใน OCaml 5 มีการเพิ่ม parallel marking ให้ Domain ช่วยกัน mark ได้

7.3 GC Tuning Parameters

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 ()

7.4 GC Pause กับ Latency-Sensitive Systems

แม้ OCaml GC จะ incremental แต่ก็ยังมี pause บางช่วง โดยเฉพาะตอน minor collection (stop-the-Domain) ซึ่งยาวประมาณ:

tpause Slive Bcopy

เมื่อ Slive คือขนาดของ live data และ Bcopy คือ bandwidth ของการ copy (ปกติ ~ 1-2 GB/s)

เทคนิคลด GC pause:

(* ตัวอย่าง: ฟังก์ชันรับประกัน 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 *)

7.5 Gc.compact และ Gc.stat: ใช้อย่างมีสติ

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 () = ()

8. Immutability และ Persistent Data Structures

Immutability (ความไม่เปลี่ยนแปลง) เป็นหัวใจของ OCaml ที่ทำให้โค้ดปลอดภัยจาก memory bugs ส่วนใหญ่โดยอัตโนมัติ

8.1 ทำไม Immutability ถึง Memory-Safe โดยธรรมชาติ

ใน C++ หรือ Go ถ้า thread สองตัวถือ pointer ไปยัง object เดียวกันและตัวหนึ่ง mutate จะเกิด data race แต่ใน OCaml ถ้าข้อมูลเป็น immutable (ปกติคือ default) จะไม่มีใคร mutate ได้เลย นี่คือสิ่งที่ทำให้ sharing ระหว่าง Domain ใน OCaml 5 ปลอดภัย ถ้าเป็น immutable data

Properties ที่ได้มาฟรี:

8.2 Persistent Data Structures และ Structural Sharing

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) คือ:

Tupdate O ( log n ) , Snew O ( log n )

คือใช้เวลาและจัดสรร memory ใหม่เพียง logn nodes ขณะที่ n-logn nodes ที่เหลือถูก share กับโครงสร้างเดิม

8.3 ตัวอย่าง: Persistent Map จาก Standard Library

(* 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

8.4 Trade-off: Allocation Overhead vs Safety

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

หลักการเลือกใช้:

8.5 Immutable Records ด้วย Functional Update

(* ตัวอย่าง: 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

9. Mutable State อย่างปลอดภัย

แม้ immutability จะเป็น default แต่ systems code บางครั้งต้องการ mutable state จริงๆ OCaml ให้เครื่องมือที่ชัดเจนและ explicit เพื่อให้นักพัฒนา เห็น ว่าที่ไหนมี mutation

9.1 ref และ mutable record fields

OCaml มีสองกลไกหลักสำหรับ mutable state:

  1. ref cell — kernel เล็กๆ ที่เป็น record มี field contents แก้ได้: let counter = ref 0 in counter := !counter + 1
  2. mutable 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

9.2 เมื่อไหร่ควรใช้ Mutable

ใช้ mutable state เมื่อ:

หลีกเลี่ยง mutable เมื่อ:

9.3 Encapsulate Mutability ด้วย Module Boundaries

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 ได้ *)

9.4 ป้องกัน Aliasing Bugs ด้วย Type System

Aliasing bug คือเมื่อ pointer สองตัวชี้ไปที่ข้อมูลเดียวกันและตัวหนึ่ง mutate ทำให้อีกตัวเห็นการเปลี่ยนแปลงที่ไม่คาดคิด OCaml ป้องกันด้วยเทคนิค:

  1. Private types — ซ่อน representation ภายใน
  2. Phantom types — ใช้ type parameter ที่ไม่มีค่าจริงเพื่อติด "label" ของสถานะ
  3. Functor boundaries — แยก state ระหว่าง instances

ตัวอย่าง 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 แต่เบากว่าและยืดหยุ่นกว่า

9.5 Checklist: Mutable Code Review

เมื่อเห็น mutable code ให้ถามตัวเอง 5 ข้อ:

  1. ใครเป็นคนเขียน (write)? — มี Domain/thread ไหนบ้างที่เขียน field นี้?
  2. ใครเป็นคนอ่าน (read)? — ต้อง synchronize กับคนเขียนไหม?
  3. มี invariant อะไรบ้าง? — เช่น active <= max_size ต้องจริงเสมอ
  4. Exception-safe ไหม? — ถ้าระหว่าง mutation เกิด exception invariant จะพังไหม?
  5. Test ครอบคลุมไหม? — โดยเฉพาะ edge case ของ state transition

10. Interop กับ C และ Memory Safety

OCaml มี FFI (Foreign Function Interface) ที่แข็งแกร่งเพื่อเรียกใช้ไลบรารี C การเชื่อมต่อกับโลก unmanaged เป็นจุดที่ต้องระวังที่สุดเรื่อง memory safety

10.1 OCaml FFI: ภาพรวม

OCaml FFI ทำงานผ่าน 2 ระดับ:

  1. Low-level stubs (C primitives) — เขียน C code ใช้ macro CAMLparam, CAMLreturn, CAMLlocal เพื่อลงทะเบียน GC root
  2. High-level bindings (Ctypes) — ใช้ library ctypes ประกาศ 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

10.2 ตัวอย่าง: Stub ระดับต่ำ

ไฟล์ 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)))

10.3 Ctypes: Type-safe C Bindings

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:

10.4 Bigarray: Off-heap Memory ที่ Share กับ C

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

10.5 ความเสี่ยง: Use-after-free และ Pinning

อันตรายที่สุดตอนข้าม FFI boundary คือ:

  1. Use-after-free — C ถือ pointer ไปยัง OCaml value, แต่ GC ย้าย value ไปที่อื่นหรือเก็บไปแล้ว
  2. Dangling pointer — OCaml ถือ int64 ชี้ไปยัง C memory ที่ถูก free แล้ว
  3. Double free — ปล่อย C resource ซ้ำเพราะ error path ซ้อนกัน

กฎป้องกัน use-after-free:

ตัวอย่าง 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);
}

10.6 Gc.finalise สำหรับ Resource Cleanup

สำหรับ 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"

11. Memory Profiling และ Leak Detection

การ profile memory ใน OCaml มีเครื่องมือที่ทรงพลังและ integrate ดีกับ GC

11.1 memtrace: Statistical Memory Profiler

memtrace (โดย Jane Street) เป็น profiler ที่ sample allocation โดยมี overhead ต่ำมาก (~5%) จึงใช้ใน production ได้ หลักการคือ:

P ( sample ) = 1 - e - size / λ

เมื่อ λ คือ 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 เยอะผิดปกติ

11.2 การหา Memory Leak ใน OCaml

"Leak" ใน OCaml ต่างจาก C — ไม่ใช่ memory ที่ลืม free แต่เป็น reference ที่ลืมลบทิ้ง ทำให้ GC เก็บไม่ได้ Pattern ที่เจอบ่อย:

  1. Unbounded cache — Hashtbl ที่ไม่เคยลบ entry
  2. Closure holding big context — closure จับ variable ใหญ่ไว้โดยไม่ตั้งใจ
  3. Long-lived list — สะสม event log ที่ไม่เคย trim
  4. Event listener/callback — subscribe แล้วไม่เคย unsubscribe
  5. Global refs — mutable ref ที่โตไปเรื่อยๆ

เทคนิคหา 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

11.3 valgrind สำหรับ C Bindings

เมื่อใช้ 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

11.4 Allocation Hotspot Analysis

การใช้ 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

11.5 Heap Snapshot ด้วย spacetime (historical) → memtrace

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
  ()

11.6 สรุป Workflow การ Profile Memory

  1. ตั้ง baseline — วัด Gc.stat ของ workload ปัจจุบันก่อนปรับอะไร
  2. เปิด memtrace — รัน workload จริงในช่วงสั้นๆ (1-5 นาที)
  3. เปิด viewer — ดู flame graph หา hottest allocation site
  4. ตรวจ leak — ถ้า live_words โตขึ้นต่อเนื่อง → หา reference ที่ค้าง
  5. ปรับ GC params — ปรับ minor_heap_size, space_overhead ตาม workload
  6. Re-measure — เปรียบเทียบกับ baseline, ต้องเห็นตัวเลขดีขึ้น
  7. เพิ่ม regression test — ถ้าเคยแก้ leak ให้ใส่ assertion heap size ใน CI
%%{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

11.7 ตัวอย่างจริง: ตารางเปรียบเทียบเครื่องมือ

เครื่องมือ 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

สรุปของส่วนที่ 3

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