- `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>
283 lines
7.6 KiB
LLVM
283 lines
7.6 KiB
LLVM
; ============================================================================
|
|
; 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
|
|
}
|