feat(jobs): real OS threads - Job.parallel_for, fn name, thread-safe Sync

- `fn name` names a top-level function as a value (E_FNREF, lowers to @fn_<name>); the worker
  entry point for Job.parallel_for, which checks it takes (int, pointer-like) and returns void.
- runtime/native/threads.ll (pthreads) and threads_win.ll (Win32 SRWLOCK/CONDITION_VARIABLE): a
  pool of one worker per core but one, parked between batches; every thread claims chunks by
  compare-and-swap. Linked only into programs that use Job/Promise/Sync, by `ludicc -o`,
  `ludic build` and the test suite's build helper.
- Sync.* is real: native mutexes, atomics as cmpxchg retry loops (neither clang takes atomicrw,
  the PC's rejects seq_consistent), mutex-guarded channels, Sync.cpu_count from the OS.
- spawn/despawn on a pool thread stop the program with a located panic.
- examples/library/threads.ludic and its test; docs for fn, Job.parallel_for, Job.is_worker.
- Reseeded (bootstrap-cfree: out.ll == seed.ll). 141/141 on macOS; jobs, threads and the guard
  pass on Windows from the reseeded Windows seed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-15 13:52:22 +03:00
parent f5d1a62ccf
commit c10abd9f9f
22 changed files with 57464 additions and 55794 deletions

View file

@ -386,35 +386,56 @@ const SYNC_ATOMIC: int = 64
const SYNC_CHAN: int = 32
const CHAN_CAP: int = 64 # capacity of each channel's ring buffer
# The OS side (threads.ll / threads_win.ll, linked with this file). Sync handles stay small ints;
# behind each is a real mutex, or an int read and written with atomic instructions, so the calls
# are safe from Job.parallel_for workers. Make the objects on the main thread before starting work.
extern function thr_cpu_count() -> int = "thr_cpu_count"
extern function thr_is_worker() -> int = "thr_is_worker"
extern function thr_parallel_for(count: int, work: pointer, ctx: pointer) = "thr_parallel_for"
extern function thr_mutex_new() -> pointer = "thr_mutex_new"
extern function thr_lock(m: pointer) = "thr_lock"
extern function thr_unlock(m: pointer) = "thr_unlock"
extern function thr_trylock(m: pointer) -> int = "thr_trylock"
extern function thr_atomic_add(p: pointer, delta: int) -> int = "thr_atomic_add"
extern function thr_cas(p: pointer, expect: int, next: int) -> int = "thr_cas"
extern function thr_load(p: pointer) -> int = "thr_load"
extern function thr_store(p: pointer, v: int) = "thr_store"
var sy_ready: bool = false
var mx_used: words = null
var mx_held: words = null
var mx_obj: pointers = null # the native mutex behind each handle
var at_used: words = null
var at_val: words = null
var at_cell: pointers = null # the int each atomic handle names (a words(1) of its own)
var ch_used: words = null
var ch_head: words = null
var ch_count: words = null
var ch_buf: words = null # flat [SYNC_CHAN * CHAN_CAP]
var ch_lock: pointers = null # a mutex per channel
function sy_init() -> void {
if sy_ready { return }
mx_used = words(SYNC_MUTEX); fill(mx_used, 0, SYNC_MUTEX * 4)
mx_held = words(SYNC_MUTEX); fill(mx_held, 0, SYNC_MUTEX * 4)
mx_obj = bytes(SYNC_MUTEX * 8); fill(mx_obj, 0, SYNC_MUTEX * 8)
at_used = words(SYNC_ATOMIC); fill(at_used, 0, SYNC_ATOMIC * 4)
at_val = words(SYNC_ATOMIC); fill(at_val, 0, SYNC_ATOMIC * 4)
at_cell = bytes(SYNC_ATOMIC * 8); fill(at_cell, 0, SYNC_ATOMIC * 8)
ch_used = words(SYNC_CHAN); fill(ch_used, 0, SYNC_CHAN * 4)
ch_head = words(SYNC_CHAN); fill(ch_head, 0, SYNC_CHAN * 4)
ch_count = words(SYNC_CHAN); fill(ch_count, 0, SYNC_CHAN * 4)
ch_buf = words(SYNC_CHAN * CHAN_CAP); fill(ch_buf, 0, SYNC_CHAN * CHAN_CAP * 4)
ch_lock = bytes(SYNC_CHAN * 8); fill(ch_lock, 0, SYNC_CHAN * 8)
sy_ready = true
}
# ---- mutex (a cooperative lock) --------------------------------------------
# ---- mutex -----------------------------------------------------------------
function sync_mutex() -> int {
sy_init()
var i = 0
while i < SYNC_MUTEX {
if mx_used[i] == 0 { mx_used[i] = 1; mx_held[i] = 0; return i + 1 }
if mx_used[i] == 0 {
mx_used[i] = 1
if mx_obj[i] == null { mx_obj[i] = thr_mutex_new() }
return i + 1
}
i += 1
}
return 0
@ -423,22 +444,20 @@ function sync_mutex() -> int {
function sync_lock(m: int) -> void {
sy_init()
if (m < 1) or (m > SYNC_MUTEX) { return }
mx_held[m - 1] = 1
thr_lock(mx_obj[m - 1])
}
function sync_unlock(m: int) -> void {
sy_init()
if (m < 1) or (m > SYNC_MUTEX) { return }
mx_held[m - 1] = 0
thr_unlock(mx_obj[m - 1])
}
# take the lock only if it is free; returns whether it was taken.
function sync_try_lock(m: int) -> bool {
sy_init()
if (m < 1) or (m > SYNC_MUTEX) { return false }
if mx_held[m - 1] != 0 { return false }
mx_held[m - 1] = 1
return true
return thr_trylock(mx_obj[m - 1]) == 1
}
# ---- atomic counter --------------------------------------------------------
@ -446,7 +465,12 @@ function sync_atomic() -> int {
sy_init()
var i = 0
while i < SYNC_ATOMIC {
if at_used[i] == 0 { at_used[i] = 1; at_val[i] = 0; return i + 1 }
if at_used[i] == 0 {
at_used[i] = 1
if at_cell[i] == null { at_cell[i] = words(1) }
thr_store(at_cell[i], 0)
return i + 1
}
i += 1
}
return 0
@ -455,38 +479,39 @@ function sync_atomic() -> int {
function sync_get(a: int) -> int {
sy_init()
if (a < 1) or (a > SYNC_ATOMIC) { return 0 }
return at_val[a - 1]
return thr_load(at_cell[a - 1])
}
function sync_set(a: int, v: int) -> void {
sy_init()
if (a < 1) or (a > SYNC_ATOMIC) { return }
at_val[a - 1] = v
thr_store(at_cell[a - 1], v)
}
# add `delta` and return the new value.
function sync_add(a: int, delta: int) -> int {
sy_init()
if (a < 1) or (a > SYNC_ATOMIC) { return 0 }
at_val[a - 1] += delta
return at_val[a - 1]
return thr_atomic_add(at_cell[a - 1], delta)
}
# compare-and-set: if the value equals `expect`, store `next` and return true.
function sync_cas(a: int, expect: int, next: int) -> bool {
sy_init()
if (a < 1) or (a > SYNC_ATOMIC) { return false }
if at_val[a - 1] != expect { return false }
at_val[a - 1] = next
return true
return thr_cas(at_cell[a - 1], expect, next) == 1
}
# ---- channel (a bounded int FIFO) ------------------------------------------
# ---- channel (a bounded int FIFO, behind its own mutex) ---------------------
function sync_channel() -> int {
sy_init()
var i = 0
while i < SYNC_CHAN {
if ch_used[i] == 0 { ch_used[i] = 1; ch_head[i] = 0; ch_count[i] = 0; return i + 1 }
if ch_used[i] == 0 {
ch_used[i] = 1; ch_head[i] = 0; ch_count[i] = 0
if ch_lock[i] == null { ch_lock[i] = thr_mutex_new() }
return i + 1
}
i += 1
}
return 0
@ -497,12 +522,14 @@ function sync_send(c: int, v: int) -> bool {
sy_init()
if (c < 1) or (c > SYNC_CHAN) { return false }
let s = c - 1
if ch_count[s] >= CHAN_CAP { return false }
thr_lock(ch_lock[s])
if ch_count[s] >= CHAN_CAP { thr_unlock(ch_lock[s]); return false }
let pos = ch_head[s] + ch_count[s]
var idx = pos
if idx >= CHAN_CAP { idx -= CHAN_CAP }
ch_buf[s * CHAN_CAP + idx] = v
ch_count[s] += 1
thr_unlock(ch_lock[s])
return true
}
@ -511,27 +538,43 @@ function sync_recv(c: int) -> int {
sy_init()
if (c < 1) or (c > SYNC_CHAN) { return 0 }
let s = c - 1
if ch_count[s] == 0 { return 0 }
thr_lock(ch_lock[s])
if ch_count[s] == 0 { thr_unlock(ch_lock[s]); return 0 }
let v = ch_buf[s * CHAN_CAP + ch_head[s]]
var nh = ch_head[s] + 1
if nh >= CHAN_CAP { nh = 0 }
ch_head[s] = nh
ch_count[s] -= 1
thr_unlock(ch_lock[s])
return v
}
function sync_can_recv(c: int) -> bool {
sy_init()
if (c < 1) or (c > SYNC_CHAN) { return false }
return ch_count[c - 1] > 0
thr_lock(ch_lock[c - 1])
let has = ch_count[c - 1] > 0
thr_unlock(ch_lock[c - 1])
return has
}
function sync_len(c: int) -> int {
sy_init()
if (c < 1) or (c > SYNC_CHAN) { return 0 }
return ch_count[c - 1]
thr_lock(ch_lock[c - 1])
let n = ch_count[c - 1]
thr_unlock(ch_lock[c - 1])
return n
}
# worker lanes available to the scheduler. One today (the deterministic main
# thread); a future OS-thread backend would report the real core count here.
function sync_cpu_count() -> int { return 1 }
# the machine's logical cores: how many threads Job.parallel_for spreads work across
function sync_cpu_count() -> int { return thr_cpu_count() }
# ---- Job.parallel_for (real threads) -----------------------------------------
# `work(i, ctx)` for every i in [0, count), across one worker per core but one and the calling
# thread; returns when every call has returned. `work` is a function reference (`fn name`) taking
# (int, pointer). The rule: a worker computes on what `ctx` points at and writes its results there
# - it never spawns, despawns, pushes onto a list another thread can see, or touches the world.
function job_parallel_for(count: int, work: pointer, ctx: pointer) -> void { thr_parallel_for(count, work, ctx) }
# true on a Job.parallel_for worker thread
function job_is_worker() -> bool { return thr_is_worker() == 1 }

300
runtime/native/threads.ll Normal file
View file

@ -0,0 +1,300 @@
; ============================================================================
; threads.ll — OS threads behind Job.parallel_for and Sync.* (macOS / POSIX, pthreads).
;
; Linked into any program that uses Job.*, Promise.* or Sync.* (selfhost/main.ludic,
; tools/ludic-cli/build.ludic). threads_win.ll is the same interface over Win32.
;
; thr_cpu_count() -> int logical cores, 1 .. 64
; thr_is_worker() -> int 1 on a pool thread, 0 elsewhere (the debug guard)
; thr_parallel_for(count, fn, ctx) fn(i, ctx) for every i in [0, count); returns when done
; thr_mutex_new() -> ptr a real mutex; thr_lock / thr_unlock / thr_trylock
; thr_atomic_add(p, d) -> int *p += d atomically; the new value
; thr_cas(p, expect, next) -> int compare-and-swap on *p; 1 when it swapped
; thr_load(p) / thr_store(p, v) an atomic read / write of *p
;
; The pool: one worker per core but one, started on the first parallel_for and parked on a
; condition variable between batches. A batch is (fn, ctx, count); every thread, the caller
; included, claims the next chunk of indices with one atomic add and runs it, so no index is run
; twice or skipped. The caller then waits until every worker has reported the batch finished.
; A parallel_for from inside a worker, or with no workers, runs inline. Only one thread outside
; the pool (the main thread) may start batches.
; ============================================================================
declare i32 @pthread_create(ptr, ptr, ptr, ptr)
declare i32 @pthread_detach(ptr)
declare i32 @pthread_mutex_init(ptr, ptr)
declare i32 @pthread_mutex_lock(ptr)
declare i32 @pthread_mutex_unlock(ptr)
declare i32 @pthread_mutex_trylock(ptr)
declare i32 @pthread_cond_init(ptr, ptr)
declare i32 @pthread_cond_wait(ptr, ptr)
declare i32 @pthread_cond_broadcast(ptr)
declare i64 @sysconf(i32)
declare ptr @malloc(i64)
@T_worker = thread_local global i32 0
@T_ready = internal global i32 0
@T_n = internal global i32 0 ; worker threads started
@T_lock = internal global ptr null
@T_work = internal global ptr null ; signalled when a batch is ready
@T_done = internal global ptr null ; signalled when the last worker finishes a batch
@T_gen = internal global i32 0 ; batch number, under @T_lock
@T_active = internal global i32 0 ; workers still on the current batch, under @T_lock
@T_fn = internal global ptr null
@T_ctx = internal global ptr null
@T_count = internal global i32 0
@T_chunk = internal global i32 1
@T_next = internal global i32 0 ; the next unclaimed index, claimed by compare-and-swap
define i32 @thr_cpu_count() {
entry:
; _SC_NPROCESSORS_ONLN is 58 on macOS
%n = call i64 @sysconf(i32 58)
%n32 = trunc i64 %n to i32
%lo = icmp slt i32 %n32, 1
%a = select i1 %lo, i32 1, i32 %n32
%hi = icmp sgt i32 %a, 64
%b = select i1 %hi, i32 64, i32 %a
ret i32 %b
}
define i32 @thr_is_worker() {
entry:
%w = load i32, ptr @T_worker
ret i32 %w
}
; *p += d atomically, as a compare-and-swap retry loop (the toolchains' clang has no atomicrw);
; the new value
define internal i32 @t_add(ptr %p, i32 %d) {
entry:
br label %retry
retry:
%old = load atomic i32, ptr %p acquire, align 4
%new = add i32 %old, %d
%r = cmpxchg ptr %p, i32 %old, i32 %new release acquire
%ok = extractvalue { i32, i1 } %r, 1
br i1 %ok, label %done, label %retry
done:
ret i32 %new
}
; claim chunks until the batch has none left, running each index
define internal void @t_run_chunks() {
entry:
br label %claim
claim:
%chunk = load i32, ptr @T_chunk
%count = load i32, ptr @T_count
%claimed = call i32 @t_add(ptr @T_next, i32 %chunk)
%start = sub i32 %claimed, %chunk
%past = icmp sge i32 %start, %count
br i1 %past, label %done, label %body
body:
%end0 = add i32 %start, %chunk
%over = icmp sgt i32 %end0, %count
%end = select i1 %over, i32 %count, i32 %end0
%fn = load ptr, ptr @T_fn
%ctx = load ptr, ptr @T_ctx
br label %loop
loop:
%i = phi i32 [ %start, %body ], [ %i1, %run ]
%more = icmp slt i32 %i, %end
br i1 %more, label %run, label %claim
run:
call void %fn(i32 %i, ptr %ctx)
%i1 = add i32 %i, 1
br label %loop
done:
ret void
}
define internal ptr @t_worker(ptr %arg) {
entry:
store i32 1, ptr @T_worker
%lk = load ptr, ptr @T_lock
%wk = load ptr, ptr @T_work
%dn = load ptr, ptr @T_done
%seen = alloca i32
store i32 0, ptr %seen
br label %park
park:
%l0 = call i32 @pthread_mutex_lock(ptr %lk)
br label %check
check:
%g = load i32, ptr @T_gen
%s = load i32, ptr %seen
%same = icmp eq i32 %g, %s
br i1 %same, label %sleep, label %go
sleep:
%w0 = call i32 @pthread_cond_wait(ptr %wk, ptr %lk)
br label %check
go:
store i32 %g, ptr %seen
%u0 = call i32 @pthread_mutex_unlock(ptr %lk)
call void @t_run_chunks()
%l1 = call i32 @pthread_mutex_lock(ptr %lk)
%a = load i32, ptr @T_active
%a1 = sub i32 %a, 1
store i32 %a1, ptr @T_active
%last = icmp eq i32 %a1, 0
br i1 %last, label %signal, label %release
signal:
%b0 = call i32 @pthread_cond_broadcast(ptr %dn)
br label %release
release:
%u1 = call i32 @pthread_mutex_unlock(ptr %lk)
br label %park
}
define internal void @t_init() {
entry:
%r = load i32, ptr @T_ready
%have = icmp ne i32 %r, 0
br i1 %have, label %out, label %make
make:
; generous sizes: pthread_mutex_t is 64 bytes and pthread_cond_t 48 on macOS
%lk = call ptr @malloc(i64 128)
%wk = call ptr @malloc(i64 128)
%dn = call ptr @malloc(i64 128)
%i0 = call i32 @pthread_mutex_init(ptr %lk, ptr null)
%i1 = call i32 @pthread_cond_init(ptr %wk, ptr null)
%i2 = call i32 @pthread_cond_init(ptr %dn, ptr null)
store ptr %lk, ptr @T_lock
store ptr %wk, ptr @T_work
store ptr %dn, ptr @T_done
%cores = call i32 @thr_cpu_count()
%n = sub i32 %cores, 1
%tid = alloca i64
br label %spawn
spawn:
%k = phi i32 [ 0, %make ], [ %k1, %started ]
%more = icmp slt i32 %k, %n
br i1 %more, label %start, label %ready
start:
%rc = call i32 @pthread_create(ptr %tid, ptr null, ptr @t_worker, ptr null)
%ok = icmp eq i32 %rc, 0
br i1 %ok, label %detach, label %ready
detach:
%t = load i64, ptr %tid
%tp = inttoptr i64 %t to ptr
%d = call i32 @pthread_detach(ptr %tp)
br label %started
started:
%k1 = add i32 %k, 1
store i32 %k1, ptr @T_n
br label %spawn
ready:
store i32 1, ptr @T_ready
br label %out
out:
ret void
}
define void @thr_parallel_for(i32 %count, ptr %fn, ptr %ctx) {
entry:
%none = icmp sle i32 %count, 0
br i1 %none, label %out, label %init
init:
call void @t_init()
%n = load i32, ptr @T_n
%w = load i32, ptr @T_worker
%nowork = icmp eq i32 %n, 0
%small = icmp slt i32 %count, 2
%inworker = icmp ne i32 %w, 0
%a = or i1 %nowork, %small
%inline = or i1 %a, %inworker
br i1 %inline, label %serial, label %batch
serial:
%si = phi i32 [ 0, %init ], [ %si1, %srun ]
%smore = icmp slt i32 %si, %count
br i1 %smore, label %srun, label %out
srun:
call void %fn(i32 %si, ptr %ctx)
%si1 = add i32 %si, 1
br label %serial
batch:
%lk = load ptr, ptr @T_lock
%wk = load ptr, ptr @T_work
%dn = load ptr, ptr @T_done
%l0 = call i32 @pthread_mutex_lock(ptr %lk)
store ptr %fn, ptr @T_fn
store ptr %ctx, ptr @T_ctx
store i32 %count, ptr @T_count
; about eight chunks a thread: small enough to share the work, large enough that claiming is cheap
%threads = add i32 %n, 1
%per = mul i32 %threads, 8
%c0 = sdiv i32 %count, %per
%tiny = icmp slt i32 %c0, 1
%chunk = select i1 %tiny, i32 1, i32 %c0
store i32 %chunk, ptr @T_chunk
store atomic i32 0, ptr @T_next release, align 4
store i32 %n, ptr @T_active
%g = load i32, ptr @T_gen
%g1 = add i32 %g, 1
store i32 %g1, ptr @T_gen
%b0 = call i32 @pthread_cond_broadcast(ptr %wk)
%u0 = call i32 @pthread_mutex_unlock(ptr %lk)
call void @t_run_chunks()
%l1 = call i32 @pthread_mutex_lock(ptr %lk)
br label %wait
wait:
%act = load i32, ptr @T_active
%busy = icmp sgt i32 %act, 0
br i1 %busy, label %sleep, label %finished
sleep:
%w0 = call i32 @pthread_cond_wait(ptr %dn, ptr %lk)
br label %wait
finished:
%u1 = call i32 @pthread_mutex_unlock(ptr %lk)
br label %out
out:
ret void
}
; ---- Sync.* --------------------------------------------------------------------------------------
define ptr @thr_mutex_new() {
entry:
%m = call ptr @malloc(i64 128)
%r = call i32 @pthread_mutex_init(ptr %m, ptr null)
ret ptr %m
}
define void @thr_lock(ptr %m) {
entry:
%r = call i32 @pthread_mutex_lock(ptr %m)
ret void
}
define void @thr_unlock(ptr %m) {
entry:
%r = call i32 @pthread_mutex_unlock(ptr %m)
ret void
}
define i32 @thr_trylock(ptr %m) {
entry:
%r = call i32 @pthread_mutex_trylock(ptr %m)
%got = icmp eq i32 %r, 0
%v = zext i1 %got to i32
ret i32 %v
}
define i32 @thr_atomic_add(ptr %p, i32 %d) {
entry:
%new = call i32 @t_add(ptr %p, i32 %d)
ret i32 %new
}
define i32 @thr_cas(ptr %p, i32 %expect, i32 %next) {
entry:
%r = cmpxchg ptr %p, i32 %expect, i32 %next release acquire
%ok = extractvalue { i32, i1 } %r, 1
%v = zext i1 %ok to i32
ret i32 %v
}
define i32 @thr_load(ptr %p) {
entry:
%v = load atomic i32, ptr %p acquire, align 4
ret i32 %v
}
define void @thr_store(ptr %p, i32 %v) {
entry:
store atomic i32 %v, ptr %p release, align 4
ret void
}

View file

@ -0,0 +1,283 @@
; ============================================================================
; threads_win.ll — OS threads behind Job.parallel_for and Sync.* (Windows).
;
; The interface of threads.ll over Win32: CreateThread, SRWLOCK and CONDITION_VARIABLE (both
; zero-initialised, pointer-sized), GetActiveProcessorCount. See threads.ll for how the pool and a
; batch work; the two files differ only in the primitives underneath.
; ============================================================================
declare ptr @CreateThread(ptr, i64, ptr, ptr, i32, ptr)
declare i32 @CloseHandle(ptr)
declare void @AcquireSRWLockExclusive(ptr)
declare void @ReleaseSRWLockExclusive(ptr)
declare i8 @TryAcquireSRWLockExclusive(ptr)
declare i32 @SleepConditionVariableSRW(ptr, ptr, i32, i32)
declare void @WakeAllConditionVariable(ptr)
declare i32 @GetActiveProcessorCount(i16)
declare ptr @malloc(i64)
@T_worker = thread_local global i32 0
@T_ready = internal global i32 0
@T_n = internal global i32 0
@T_lock = internal global ptr null
@T_work = internal global ptr null
@T_done = internal global ptr null
@T_gen = internal global i32 0
@T_active = internal global i32 0
@T_fn = internal global ptr null
@T_ctx = internal global ptr null
@T_count = internal global i32 0
@T_chunk = internal global i32 1
@T_next = internal global i32 0
; a zeroed pointer-sized object: an SRWLOCK or a CONDITION_VARIABLE
define internal ptr @t_zeroed() {
entry:
%p = call ptr @malloc(i64 16)
store i64 0, ptr %p
%p8 = getelementptr i8, ptr %p, i64 8
store i64 0, ptr %p8
ret ptr %p
}
define i32 @thr_cpu_count() {
entry:
; ALL_PROCESSOR_GROUPS
%n = call i32 @GetActiveProcessorCount(i16 -1)
%lo = icmp slt i32 %n, 1
%a = select i1 %lo, i32 1, i32 %n
%hi = icmp sgt i32 %a, 64
%b = select i1 %hi, i32 64, i32 %a
ret i32 %b
}
define i32 @thr_is_worker() {
entry:
%w = load i32, ptr @T_worker
ret i32 %w
}
; *p += d atomically, as a compare-and-swap retry loop (the toolchains' clang has no atomicrw);
; the new value
define internal i32 @t_add(ptr %p, i32 %d) {
entry:
br label %retry
retry:
%old = load atomic i32, ptr %p acquire, align 4
%new = add i32 %old, %d
%r = cmpxchg ptr %p, i32 %old, i32 %new release acquire
%ok = extractvalue { i32, i1 } %r, 1
br i1 %ok, label %done, label %retry
done:
ret i32 %new
}
define internal void @t_run_chunks() {
entry:
br label %claim
claim:
%chunk = load i32, ptr @T_chunk
%count = load i32, ptr @T_count
%claimed = call i32 @t_add(ptr @T_next, i32 %chunk)
%start = sub i32 %claimed, %chunk
%past = icmp sge i32 %start, %count
br i1 %past, label %done, label %body
body:
%end0 = add i32 %start, %chunk
%over = icmp sgt i32 %end0, %count
%end = select i1 %over, i32 %count, i32 %end0
%fn = load ptr, ptr @T_fn
%ctx = load ptr, ptr @T_ctx
br label %loop
loop:
%i = phi i32 [ %start, %body ], [ %i1, %run ]
%more = icmp slt i32 %i, %end
br i1 %more, label %run, label %claim
run:
call void %fn(i32 %i, ptr %ctx)
%i1 = add i32 %i, 1
br label %loop
done:
ret void
}
define internal i32 @t_worker(ptr %arg) {
entry:
store i32 1, ptr @T_worker
%lk = load ptr, ptr @T_lock
%wk = load ptr, ptr @T_work
%dn = load ptr, ptr @T_done
%seen = alloca i32
store i32 0, ptr %seen
br label %park
park:
call void @AcquireSRWLockExclusive(ptr %lk)
br label %check
check:
%g = load i32, ptr @T_gen
%s = load i32, ptr %seen
%same = icmp eq i32 %g, %s
br i1 %same, label %sleep, label %go
sleep:
%w0 = call i32 @SleepConditionVariableSRW(ptr %wk, ptr %lk, i32 -1, i32 0)
br label %check
go:
store i32 %g, ptr %seen
call void @ReleaseSRWLockExclusive(ptr %lk)
call void @t_run_chunks()
call void @AcquireSRWLockExclusive(ptr %lk)
%a = load i32, ptr @T_active
%a1 = sub i32 %a, 1
store i32 %a1, ptr @T_active
%last = icmp eq i32 %a1, 0
br i1 %last, label %signal, label %release
signal:
call void @WakeAllConditionVariable(ptr %dn)
br label %release
release:
call void @ReleaseSRWLockExclusive(ptr %lk)
br label %park
}
define internal void @t_init() {
entry:
%r = load i32, ptr @T_ready
%have = icmp ne i32 %r, 0
br i1 %have, label %out, label %make
make:
%lk = call ptr @t_zeroed()
%wk = call ptr @t_zeroed()
%dn = call ptr @t_zeroed()
store ptr %lk, ptr @T_lock
store ptr %wk, ptr @T_work
store ptr %dn, ptr @T_done
%cores = call i32 @thr_cpu_count()
%n = sub i32 %cores, 1
br label %spawn
spawn:
%k = phi i32 [ 0, %make ], [ %k1, %started ]
%more = icmp slt i32 %k, %n
br i1 %more, label %start, label %ready
start:
%t = call ptr @CreateThread(ptr null, i64 0, ptr @t_worker, ptr null, i32 0, ptr null)
%bad = icmp eq ptr %t, null
br i1 %bad, label %ready, label %close
close:
%c = call i32 @CloseHandle(ptr %t)
br label %started
started:
%k1 = add i32 %k, 1
store i32 %k1, ptr @T_n
br label %spawn
ready:
store i32 1, ptr @T_ready
br label %out
out:
ret void
}
define void @thr_parallel_for(i32 %count, ptr %fn, ptr %ctx) {
entry:
%none = icmp sle i32 %count, 0
br i1 %none, label %out, label %init
init:
call void @t_init()
%n = load i32, ptr @T_n
%w = load i32, ptr @T_worker
%nowork = icmp eq i32 %n, 0
%small = icmp slt i32 %count, 2
%inworker = icmp ne i32 %w, 0
%a = or i1 %nowork, %small
%inline = or i1 %a, %inworker
br i1 %inline, label %serial, label %batch
serial:
%si = phi i32 [ 0, %init ], [ %si1, %srun ]
%smore = icmp slt i32 %si, %count
br i1 %smore, label %srun, label %out
srun:
call void %fn(i32 %si, ptr %ctx)
%si1 = add i32 %si, 1
br label %serial
batch:
%lk = load ptr, ptr @T_lock
%wk = load ptr, ptr @T_work
%dn = load ptr, ptr @T_done
call void @AcquireSRWLockExclusive(ptr %lk)
store ptr %fn, ptr @T_fn
store ptr %ctx, ptr @T_ctx
store i32 %count, ptr @T_count
%threads = add i32 %n, 1
%per = mul i32 %threads, 8
%c0 = sdiv i32 %count, %per
%tiny = icmp slt i32 %c0, 1
%chunk = select i1 %tiny, i32 1, i32 %c0
store i32 %chunk, ptr @T_chunk
store atomic i32 0, ptr @T_next release, align 4
store i32 %n, ptr @T_active
%g = load i32, ptr @T_gen
%g1 = add i32 %g, 1
store i32 %g1, ptr @T_gen
call void @WakeAllConditionVariable(ptr %wk)
call void @ReleaseSRWLockExclusive(ptr %lk)
call void @t_run_chunks()
call void @AcquireSRWLockExclusive(ptr %lk)
br label %wait
wait:
%act = load i32, ptr @T_active
%busy = icmp sgt i32 %act, 0
br i1 %busy, label %sleep, label %finished
sleep:
%w0 = call i32 @SleepConditionVariableSRW(ptr %dn, ptr %lk, i32 -1, i32 0)
br label %wait
finished:
call void @ReleaseSRWLockExclusive(ptr %lk)
br label %out
out:
ret void
}
; ---- Sync.* --------------------------------------------------------------------------------------
define ptr @thr_mutex_new() {
entry:
%m = call ptr @t_zeroed()
ret ptr %m
}
define void @thr_lock(ptr %m) {
entry:
call void @AcquireSRWLockExclusive(ptr %m)
ret void
}
define void @thr_unlock(ptr %m) {
entry:
call void @ReleaseSRWLockExclusive(ptr %m)
ret void
}
define i32 @thr_trylock(ptr %m) {
entry:
%r = call i8 @TryAcquireSRWLockExclusive(ptr %m)
%got = icmp ne i8 %r, 0
%v = zext i1 %got to i32
ret i32 %v
}
define i32 @thr_atomic_add(ptr %p, i32 %d) {
entry:
%new = call i32 @t_add(ptr %p, i32 %d)
ret i32 %new
}
define i32 @thr_cas(ptr %p, i32 %expect, i32 %next) {
entry:
%r = cmpxchg ptr %p, i32 %expect, i32 %next release acquire
%ok = extractvalue { i32, i1 } %r, 1
%v = zext i1 %ok to i32
ret i32 %v
}
define i32 @thr_load(ptr %p) {
entry:
%v = load atomic i32, ptr %p acquire, align 4
ret i32 %v
}
define void @thr_store(ptr %p, i32 %v) {
entry:
store atomic i32 %v, ptr %p release, align 4
ret void
}