ludic build on every core: the IR split with llvm-split and compiled by a clang per part at once
Most of a build was one clang -O2 on one .ll (the game: 32 s of a 41 s headless build, one core). Both paths that assemble - the CLI's (build.ludic: ludicc --emit-llvm, then clang) and ludicc's own (-o, which ludic bundle and the examples use) - now cut the program's IR into N parts with llvm-split (externalizing what the parts share), compile them with one clang each in parallel (-x ir -O<opt> -mmacosx-version-min=11.0, the link's own clang taking the objects where it took the .ll), and remove the parts and objects after. N is $LUDIC_JOBS, else min(cores, free GB / 1.5). It needs an llvm-split and a clang of the same LLVM (Homebrew's LLVM 22 writes attributes Apple's clang 17 cannot read): $LUDIC_LLVM, else /opt/homebrew/opt/llvm/bin. With either missing, on Windows (its shell cannot run the parts at once yet), with LUDIC_SPLIT=0, or when a part fails, it compiles the .ll whole as before. $LUDIC_OPT=1 is a developer's faster build; ludic bundle sets LUDIC_OPT=2 for its compile whatever the shell says. Measured before the compile-only rule (this Mac, 12 cores, one build at a time): - the game headless: 37-41 s -> 13-15.5 s (8 parts / by free memory), peak 2.1 GB -> 1.0-1.1 GB; - the lab headless: 43.1 s -> 12.7 s, peak 2.4 GB -> 1.0 GB; - the game at LUDIC_OPT=1, split: 11.8 s (fps cost not measured). Both built and linked clean; the goldens and a headless shot of the result are not run here. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
9f94be7cca
commit
9cc3f24c56
10 changed files with 4119 additions and 2288 deletions
87
selfhost/split_build.ludic
Normal file
87
selfhost/split_build.ludic
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
# split_build.ludic - the program's IR compiled on every core: llvm-split cuts it into N parts, one
|
||||
# clang each runs at once, and the link takes their objects where it took the one .ll. Most of a
|
||||
# build was one clang -O2 on one file (the game: 32 s of 41). Needs an llvm-split and a clang of
|
||||
# the same LLVM (a newer LLVM's parts do not read in an older clang): $LUDIC_LLVM (a bin directory),
|
||||
# else Homebrew's. Anything missing, or a part that does not compile, falls back to the one clang.
|
||||
|
||||
function split_there(p: pointer) -> bool {
|
||||
let f = file_open(p, "rb")
|
||||
if f == null { return false }
|
||||
file_close(f)
|
||||
return true
|
||||
}
|
||||
|
||||
# the LLVM bin directory to split with, or null (Windows' shell cannot run the parts at once yet)
|
||||
function split_dir() -> pointer {
|
||||
if host_is_windows() { return null }
|
||||
if getenv_or("LUDIC_SPLIT", "1") == "0" { return null }
|
||||
let d = getenv_or("LUDIC_LLVM", "/opt/homebrew/opt/llvm/bin")
|
||||
if not split_there(`{d}/llvm-split`) or not split_there(`{d}/clang`) { return null }
|
||||
return d
|
||||
}
|
||||
|
||||
# how many parts: $LUDIC_JOBS, else the cores, no more than the free memory allows at 1.5 GB a clang
|
||||
function split_jobs(tmp: pointer) -> int {
|
||||
let j = getenv("LUDIC_JOBS")
|
||||
if j != null { return split_int(j) }
|
||||
let probe = `{tmp}.cores`
|
||||
let aw = "awk '/Pages free/{f=$3} /Pages inactive/{i=$3} END{print int((f+i)*p/1073741824)}'"
|
||||
run(`echo $(sysctl -n hw.ncpu) $(vm_stat | {aw} p=$(sysctl -n hw.pagesize)) > "{probe}" 2>/dev/null`)
|
||||
let f = file_open(probe, "rb")
|
||||
if f == null { return 1 }
|
||||
let buf = bytes(64)
|
||||
let n = file_read(f, buf, 63)
|
||||
file_close(f)
|
||||
remove_file(`"{probe}"`)
|
||||
if n <= 0 { return 1 }
|
||||
buf[n] = 0
|
||||
var cores = 0
|
||||
var i = 0
|
||||
while i < n and buf[i] >= '0' and buf[i] <= '9' { cores = cores * 10 + (buf[i] - '0'); i += 1 }
|
||||
while i < n and buf[i] == ' ' { i += 1 }
|
||||
var gb = 0
|
||||
while i < n and buf[i] >= '0' and buf[i] <= '9' { gb = gb * 10 + (buf[i] - '0'); i += 1 }
|
||||
let by_mem = (gb * 2) / 3
|
||||
if by_mem < cores { return by_mem }
|
||||
return cores
|
||||
}
|
||||
function split_int(s: pointer) -> int {
|
||||
var v = 0
|
||||
var i = 0
|
||||
while i < len(s) and s[i] >= '0' and s[i] <= '9' { v = v * 10 + (s[i] - '0'); i += 1 }
|
||||
return v
|
||||
}
|
||||
|
||||
# the IR at `ll` compiled in parts at -O`opt`: their objects, quoted and space-separated, for the
|
||||
# link to take in place of the .ll; null when it was not split (the caller compiles the .ll whole)
|
||||
function split_compile(ll: pointer, out: pointer, opt: pointer) -> pointer {
|
||||
let d = split_dir()
|
||||
if d == null { return null }
|
||||
let n = split_jobs(out)
|
||||
if n < 2 { return null }
|
||||
let pre = `{out}.part`
|
||||
if run(`"{d}/llvm-split" -j {n} -o "{pre}" "{ll}"`) != 0 { return null }
|
||||
var sh = ""
|
||||
var i = 0
|
||||
while i < n {
|
||||
sh = sh + `"{d}/clang" -x ir -O{opt} -mmacosx-version-min=11.0 -Wno-override-module -Wno-missing-sysroot -c "{pre}{itoa(i)}" -o "{pre}{itoa(i)}.o" & `
|
||||
i += 1
|
||||
}
|
||||
run(`/bin/sh -c '{sh}wait'`)
|
||||
var objs = ""
|
||||
var ok = true
|
||||
i = 0
|
||||
while i < n {
|
||||
remove_file(`"{pre}{itoa(i)}"`)
|
||||
if not split_there(`{pre}{itoa(i)}.o`) { ok = false }
|
||||
objs = objs + ` "{pre}{itoa(i)}.o"`
|
||||
i += 1
|
||||
}
|
||||
if not ok {
|
||||
split_clean(out)
|
||||
return null
|
||||
}
|
||||
return objs
|
||||
}
|
||||
# the parts' objects, once linked (or when a part failed)
|
||||
function split_clean(out: pointer) -> void { run(`rm -f "{out}".part*.o`) }
|
||||
Loading…
Add table
Add a link
Reference in a new issue