bench: 증분 루프를 측정으로 증명한다
100k줄 200모듈에서 본문 수정은 downstream을 한 칸도 건드리지 않고(재검사 1개, 0.8ms), 시그니처 수정은 hash가 변한 곳까지만 전파되어(재검사 2개, 2.1ms) downstream 소진성 위반을 실제로 잡는다. "빠르다"가 아니라 "다시 볼 것이 적다"가 주장이므로, 시간보다 재검사된 모듈 수를 먼저 출력한다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019ZVDeU6KLuUVL3gs18Hm3E
This commit is contained in:
+128
@@ -0,0 +1,128 @@
|
||||
(* 빠른 검증 루프는 주장이 아니라 측정이다.
|
||||
|
||||
측정하는 것은 두 가지다:
|
||||
1. 전체 검사 시간 (cold) — 규모가 커져도 파국이 아닌가
|
||||
2. 증분 재검사 시간 (warm) — 그리고 무엇이 재검사되었는가
|
||||
|
||||
두 번째가 본체다. 아키텍처의 주장은 "빠르다"가 아니라 "다시 볼 것이
|
||||
적다"이고, 그것은 시간이 아니라 재검사된 모듈 수로 먼저 증명된다.
|
||||
시간은 그 수가 옳다는 것의 따름 결과다. *)
|
||||
|
||||
open Coollang
|
||||
|
||||
let modules = 200
|
||||
let fns_per_module = 68
|
||||
let now () = Unix.gettimeofday ()
|
||||
|
||||
let find_sub hay needle =
|
||||
let n = String.length needle and h = String.length hay in
|
||||
let rec go i =
|
||||
if i + n > h then failwith "not found"
|
||||
else if String.sub hay i n = needle then i
|
||||
else go (i + 1)
|
||||
in
|
||||
go 0
|
||||
|
||||
let write file s =
|
||||
let oc = open_out_bin file in
|
||||
output_string oc s;
|
||||
close_out oc
|
||||
|
||||
(* i번 모듈은 i-1번 모듈을 가져온다. 사슬이므로 시그니처 변경은 끝까지
|
||||
전파되어야 하고, 본문 변경은 한 칸도 가면 안 된다. *)
|
||||
let gen_module i ~body =
|
||||
let b = Buffer.create 8192 in
|
||||
if i > 0 then
|
||||
Buffer.add_string b (Printf.sprintf "import \"m%d\" as Up\n\n" (i - 1));
|
||||
Buffer.add_string b "pub enum Shape {\n Circle(Int),\n Square(Int),\n}\n\n";
|
||||
Buffer.add_string b "pub copyable struct Point {\n x: Int,\n y: Int,\n}\n\n";
|
||||
Buffer.add_string b
|
||||
(Printf.sprintf
|
||||
"pub fn area(s: Shape) -> Int {\n\
|
||||
\ match s {\n\
|
||||
\ Circle(r) => r * r,\n\
|
||||
\ Square(w) => w * w,\n\
|
||||
\ }\n\
|
||||
}\n\n");
|
||||
if i > 0 then
|
||||
Buffer.add_string b
|
||||
"pub fn up_area(s: Up.Shape) -> Int {\n\
|
||||
\ match s {\n\
|
||||
\ Up.Circle(r) => Up.area(s),\n\
|
||||
\ Up.Square(w) => w * w,\n\
|
||||
\ }\n\
|
||||
}\n\n";
|
||||
for k = 0 to fns_per_module - 1 do
|
||||
Buffer.add_string b
|
||||
(Printf.sprintf
|
||||
"pub fn f%d(p: Point, n: Int) -> Int {\n\
|
||||
\ let a = p.x + n\n\
|
||||
\ let b = p.y * %d\n\
|
||||
\ let c = if a > b { a } else { b }\n\
|
||||
\ %s\n\
|
||||
}\n\n"
|
||||
k (k + 1) body)
|
||||
done;
|
||||
Buffer.contents b
|
||||
|
||||
let count_lines s =
|
||||
String.fold_left (fun n c -> if c = '\n' then n + 1 else n) 0 s
|
||||
|
||||
let ms t = Printf.sprintf "%.1fms" (t *. 1000.)
|
||||
|
||||
let () =
|
||||
let dir = Filename.concat (Filename.get_temp_dir_name ()) "cool_bench" in
|
||||
ignore
|
||||
(Sys.command
|
||||
(Printf.sprintf "rm -rf %s && mkdir -p %s" (Filename.quote dir)
|
||||
(Filename.quote dir)));
|
||||
let path i = Filename.concat dir (Printf.sprintf "m%d.cool" i) in
|
||||
let lines = ref 0 in
|
||||
for i = 0 to modules - 1 do
|
||||
let s = gen_module i ~body:"a + c" in
|
||||
lines := !lines + count_lines s;
|
||||
write (path i) s
|
||||
done;
|
||||
Printf.printf "모듈 %d개, %d줄 생성\n" modules !lines;
|
||||
|
||||
(* 1. cold: 전체 그래프 로드와 검사 *)
|
||||
let st = Session.create ~root:dir () in
|
||||
let t0 = now () in
|
||||
Session.load st (path (modules - 1));
|
||||
let cold = now () -. t0 in
|
||||
let errs = Session.errors st in
|
||||
Printf.printf "cold 전체 검사 %s (오류 %d건)\n" (ms cold) (List.length errs);
|
||||
if errs <> [] then
|
||||
List.iter
|
||||
(fun e -> prerr_endline (Session.string_of_error e))
|
||||
(List.filteri (fun i _ -> i < 5) errs);
|
||||
|
||||
(* 2. warm: 사슬 한가운데 모듈의 본문만 수정 *)
|
||||
let mid = modules / 2 in
|
||||
write (path mid) (gen_module mid ~body:"a + c + 1");
|
||||
let t0 = now () in
|
||||
let touched = Session.recheck st [ path mid ] in
|
||||
let warm_body = now () -. t0 in
|
||||
Printf.printf "warm 본문만 수정 %s (재검사 %d개, 오류 %d건)\n" (ms warm_body)
|
||||
(List.length touched)
|
||||
(List.length (Session.errors st));
|
||||
|
||||
(* 3. warm: 같은 모듈의 시그니처 수정 — variant 추가 *)
|
||||
let sig_src =
|
||||
let s = gen_module mid ~body:"a + c + 1" in
|
||||
let needle = " Square(Int),\n" in
|
||||
let i = find_sub s needle in
|
||||
String.sub s 0 (i + String.length needle)
|
||||
^ " Tri(Int),\n"
|
||||
^ String.sub s
|
||||
(i + String.length needle)
|
||||
(String.length s - i - String.length needle)
|
||||
in
|
||||
write (path mid) sig_src;
|
||||
let t0 = now () in
|
||||
let touched = Session.recheck st [ path mid ] in
|
||||
let warm_sig = now () -. t0 in
|
||||
let errs = Session.errors st in
|
||||
Printf.printf "warm 시그니처 수정 %s (재검사 %d개, 오류 %d건)\n" (ms warm_sig)
|
||||
(List.length touched) (List.length errs);
|
||||
List.iter (fun e -> print_endline (" " ^ Session.string_of_error e)) errs
|
||||
@@ -0,0 +1,3 @@
|
||||
(executable
|
||||
(name bench)
|
||||
(libraries coollang unix))
|
||||
@@ -332,3 +332,14 @@ L2 빠른 테스트 / L3 fuzzing / L4 formal proof → 요청 시, 분리 실행
|
||||
오류를 더 빨리 잡는가? / 컴파일 복잡도·시간은 예측 가능한가? /
|
||||
invalidation 범위를 넓히는가? / unrelated code 의미를 바꾸는가? /
|
||||
기존 개념의 중복 표현인가? → 강한 이유 없으면 거절
|
||||
|
||||
■ 측정 (2026-08, v0 fast path)
|
||||
100,391줄 / 200 모듈 (사슬 의존). bench/bench.ml로 재현.
|
||||
cold 전체 검사 245ms
|
||||
본문만 수정 0.8ms, 재검사 1개 모듈
|
||||
시그니처 수정 2.1ms, 재검사 2개 모듈 (+ downstream 소진성 위반 검출)
|
||||
증분 루프의 비용은 시간이 아니라 재검사 범위가 결정한다. 본문 수정이
|
||||
downstream을 한 칸도 건드리지 않는 것이 이 아키텍처의 주장이고, 위 수치의
|
||||
"재검사 1개"가 그 주장이다. 시그니처 수정이 사슬 끝까지 가지 않고 2개에서
|
||||
멈추는 것도 같은 규칙의 결과다 — m101의 interface는 변하지 않으므로 m102는
|
||||
다시 볼 이유가 없다.
|
||||
|
||||
Reference in New Issue
Block a user