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
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load diff
|
|
@ -280,7 +280,11 @@ entry {
|
|||
# -Wno-override-module: the emitted IR carries no target triple, so clang
|
||||
# substitutes the host's and warns about it on every single build. There is
|
||||
# nothing to act on — the host default is exactly what we want — so it is off.
|
||||
var cmd = `{cc} -O2 -Wno-override-module {ll}`
|
||||
# $LUDIC_OPT=1 is a developer's faster build (never a bundle's: ludic bundle sets 2)
|
||||
let opt = getenv_or("LUDIC_OPT", "2")
|
||||
let objs = split_compile(ll, out, opt) # on every core when it can (split_build.ludic)
|
||||
var cmd = `{cc} -O{opt} -Wno-override-module {ll}`
|
||||
if objs != null { cmd = `{cc} -O{opt} -Wno-override-module{objs}` }
|
||||
if g_target_win {
|
||||
# The Windows window: win32.ll (user32, gdi32 for the software present, xinput
|
||||
# for pads), and audio_win.ll when the program uses Audio.*, as on macOS.
|
||||
|
|
@ -389,6 +393,7 @@ entry {
|
|||
var line = cmd
|
||||
if host_is_windows() { line = `"{cmd}"` }
|
||||
let rc = run(line)
|
||||
if objs != null { split_clean(out) }
|
||||
if not save { remove_file(ll) }
|
||||
if rc != 0 { die("ludicc: link failed\n") }
|
||||
if natives_out != null {
|
||||
|
|
|
|||
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