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:
parent
f5d1a62ccf
commit
c10abd9f9f
22 changed files with 57464 additions and 55794 deletions
13
changes/os-threads.md
Normal file
13
changes/os-threads.md
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
bump: minor
|
||||
type: feat
|
||||
**Real OS threads behind `Job.*` and `Sync.*`** — `Job.parallel_for` runs work across every core.
|
||||
|
||||
- **`fn name`** — an expression naming a top-level function as a value, for a worker's entry point.
|
||||
No closures: the data a worker needs is passed in.
|
||||
- **`Job.parallel_for(count, fn work, ctx)`** — `work(i, ctx)` for every `i` in `[0, count)`, split
|
||||
into chunks across a worker pool (one thread per core but one, pthreads or Win32) and the calling
|
||||
thread; returns when all are done. `Job.is_worker()` says whether code is on a pool thread.
|
||||
- **`Sync.*` is thread-safe** — mutexes are real mutexes, atomics are compare-and-swap, channels are
|
||||
guarded, and `Sync.cpu_count()` reports the machine's cores.
|
||||
- **A worker must not change the world** — `spawn` and `despawn` on a pool thread stop the program with
|
||||
a located message.
|
||||
|
|
@ -4,4 +4,4 @@ title: Job
|
|||
order: 37
|
||||
---
|
||||
|
||||
Background work that stays out of the frame. A <code>Job</code> is a future — a handle to a result that lands later. Kick one off with <code>Job.run</code> (a background compute that advances a little each <code>Job.pump</code> and finishes after enough frames, so heavy work never hitches) or <code>Job.defer</code> (a future you resolve yourself with <code>Job.fulfill</code> / <code>Job.fail</code>). Poll it with <code>done</code> / <code>ok</code> / <code>failed</code> / <code>cancelled</code>, read <code>result</code> / <code>error</code>, and always collect on the main thread — a Job must never touch the ECS world directly. The scheduler is deterministic and cooperative, so the same jobs and the same budget reproduce byte-for-byte, every run and every target. Arguments are positional. Spliced in only when a program mentions <code>Job.*</code>.
|
||||
Background work that stays out of the frame. A <code>Job</code> is a future — a handle to a result that lands later. Kick one off with <code>Job.run</code> (a background compute that advances a little each <code>Job.pump</code> and finishes after enough frames, so heavy work never hitches) or <code>Job.defer</code> (a future you resolve yourself with <code>Job.fulfill</code> / <code>Job.fail</code>). Poll it with <code>done</code> / <code>ok</code> / <code>failed</code> / <code>cancelled</code>, read <code>result</code> / <code>error</code>, and always collect on the main thread — a Job must never touch the ECS world directly. The scheduler is deterministic and cooperative, so the same jobs and the same budget reproduce byte-for-byte, every run and every target. For work that should use every core now, <code>Job.parallel_for(count, fn work, ctx)</code> runs <code>work(i, ctx)</code> across a pool of real OS threads and returns when all of it is done; a worker computes on what it was handed and never changes the world. Arguments are positional. Spliced in only when a program mentions <code>Job.*</code>.
|
||||
|
|
|
|||
19
docs/language/job/job-is_worker.md
Normal file
19
docs/language/job/job-is_worker.md
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
---
|
||||
id: job-is_worker
|
||||
name: Job.is_worker
|
||||
category: job
|
||||
kind: namespace-method
|
||||
tokens: Job.is_worker
|
||||
sig: Job.is_worker() -> bool
|
||||
tip: True on a Job.parallel_for pool thread, false on the main thread.
|
||||
order: 17
|
||||
ns: Job
|
||||
member: is_worker
|
||||
---
|
||||
|
||||
True when the code is running on a `Job.parallel_for` pool thread, false on the main thread (and on
|
||||
the calling thread while it takes its own share of a batch).
|
||||
|
||||
```ludic
|
||||
if not Job.is_worker() { print("main thread") }
|
||||
```
|
||||
34
docs/language/job/job-parallel_for.md
Normal file
34
docs/language/job/job-parallel_for.md
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
---
|
||||
id: job-parallel_for
|
||||
name: Job.parallel_for
|
||||
category: job
|
||||
kind: namespace-method
|
||||
tokens: Job.parallel_for
|
||||
sig: Job.parallel_for(count, work, ctx) -> void
|
||||
tip: Run work(i, ctx) for every i in [0, count) across all cores; returns when every call is done.
|
||||
order: 16
|
||||
ns: Job
|
||||
member: parallel_for
|
||||
---
|
||||
|
||||
Run `work(i, ctx)` for every `i` in `[0, count)` on real OS threads, and return when every call has
|
||||
finished. The indices are split into chunks shared by a worker pool (one thread per core but one,
|
||||
started on first use) and the calling thread, so each index runs exactly once, in no particular order.
|
||||
|
||||
`work` is a top-level function taking `(i: int, ctx)`, named with `fn`; `ctx` is whatever it computes
|
||||
on (`words`, `bytes` or a `pointer`). A worker computes on what it was handed and writes only its own
|
||||
index's results: it must not `spawn`, `despawn`, `push` onto a list another thread can see, or use
|
||||
Http or Audio. `spawn` and `despawn` on a worker stop the program with a located message. Shared
|
||||
counters go through `Sync.add`, shared totals behind a `Sync.mutex`.
|
||||
|
||||
```ludic
|
||||
program Squares {
|
||||
function square(i: int, out: words) -> void { out[i] = i * i }
|
||||
|
||||
entry {
|
||||
let sq = words(20000)
|
||||
Job.parallel_for(20000, fn square, sq)
|
||||
print(sq[141])
|
||||
}
|
||||
}
|
||||
```
|
||||
24
docs/language/structure/kw-fn.md
Normal file
24
docs/language/structure/kw-fn.md
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
---
|
||||
id: kw-fn
|
||||
name: fn
|
||||
category: structure
|
||||
kind: keyword
|
||||
tokens: fn
|
||||
sig: fn name
|
||||
tip: A top-level function named as a value - the entry point handed to Job.parallel_for.
|
||||
order: 9
|
||||
---
|
||||
|
||||
<code>fn name</code> names a top-level function as a value, so it can be handed to something that calls it later - today, <code>Job.parallel_for</code>, which runs it on worker threads. It is a plain function reference: no closure and nothing captured, so everything the function needs is passed to it (for <code>Job.parallel_for</code>, through its <code>ctx</code> argument). A worker function takes <code>(i: int, ctx)</code> and returns <code>void</code>; the compiler rejects any other shape.
|
||||
|
||||
```ludic
|
||||
program Squares {
|
||||
function square(i: int, out: words) -> void { out[i] = i * i }
|
||||
|
||||
entry {
|
||||
let sq = words(1000)
|
||||
Job.parallel_for(1000, fn square, sq)
|
||||
print(sq[12])
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -5,13 +5,14 @@ category: sync
|
|||
kind: namespace-method
|
||||
tokens: Sync.cpu_count
|
||||
sig: Sync.cpu_count() -> int
|
||||
tip: Worker lanes available to the scheduler.
|
||||
tip: The machine's logical cores (1 to 64); Job.parallel_for uses one thread per core.
|
||||
order: 14
|
||||
ns: Sync
|
||||
member: cpu_count
|
||||
---
|
||||
|
||||
Worker lanes available to the scheduler.
|
||||
The machine's logical cores, from 1 to 64. `Job.parallel_for` runs on that many threads: a pool of
|
||||
one per core but one, plus the caller.
|
||||
|
||||
```ludic
|
||||
let lanes = Sync.cpu_count()
|
||||
|
|
|
|||
54
examples/library/threads.ludic
Normal file
54
examples/library/threads.ludic
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
# threads.ludic — Job.parallel_for on real OS threads, with Sync.* made safe to share.
|
||||
# Each assertion that holds prints its number, so a full run prints:
|
||||
# 1 2 3 4 5 6 7
|
||||
# A worker is a top-level function taking (i: int, ctx: pointer-like) that computes on what ctx
|
||||
# points at; `fn name` passes it. Workers never touch the world or a list another thread can see.
|
||||
program Threads {
|
||||
const N: int = 20000
|
||||
|
||||
var calls: int = 0 # an atomic handle, made on the main thread before any work
|
||||
var lock: int = 0 # a mutex handle
|
||||
var total: long = 0 # guarded by `lock`
|
||||
|
||||
# out[i] = i * i: every index written exactly once, by whichever thread claimed it
|
||||
function square(i: int, out: words) -> void { out[i] = i * i }
|
||||
|
||||
# count the call atomically, and add i to a shared total under the mutex
|
||||
function tally(i: int, ctx: pointer) -> void {
|
||||
Sync.add(calls, 1)
|
||||
Sync.lock(lock)
|
||||
total = total + i
|
||||
Sync.unlock(lock)
|
||||
}
|
||||
|
||||
# remember which calls ran on a pool thread
|
||||
function placed(i: int, out: words) -> void { if Job.is_worker() { out[i] = 1 } else { out[i] = 0 } }
|
||||
|
||||
entry {
|
||||
if Sync.cpu_count() >= 1 { print(1) }
|
||||
if not Job.is_worker() { print(2) }
|
||||
|
||||
let sq = words(N)
|
||||
Job.parallel_for(N, fn square, sq)
|
||||
var right = true
|
||||
for i in 0 .. N { if sq[i] != i * i { right = false } }
|
||||
if right { print(3) }
|
||||
|
||||
calls = Sync.atomic()
|
||||
lock = Sync.mutex()
|
||||
Job.parallel_for(N, fn tally, null)
|
||||
if Sync.get(calls) == N { print(4) }
|
||||
let want: long = N * (N - 1) / 2
|
||||
if total == want { print(5) }
|
||||
|
||||
let ran = words(N)
|
||||
Job.parallel_for(N, fn placed, ran)
|
||||
var pooled = 0
|
||||
for i in 0 .. N { pooled += ran[i] }
|
||||
# with more than one core some calls ran on workers; with one, all ran here
|
||||
if (Sync.cpu_count() > 1 and pooled > 0) or (Sync.cpu_count() == 1 and pooled == 0) { print(6) }
|
||||
|
||||
Job.parallel_for(0, fn square, sq) # nothing to do: returns at once
|
||||
print(7)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
300
runtime/native/threads.ll
Normal 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
|
||||
}
|
||||
283
runtime/native/threads_win.ll
Normal file
283
runtime/native/threads_win.ll
Normal 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
|
||||
}
|
||||
|
|
@ -665,6 +665,8 @@ function emit_ns_call(ns: pointer, meth: pointer, e: Node) -> Val {
|
|||
if (meth == "error") { bare = "job_error"; push(labels, "handle") }
|
||||
if (meth == "pending") { bare = "job_pending" }
|
||||
if (meth == "free") { bare = "job_free"; push(labels, "handle") }
|
||||
if (meth == "parallel_for") { bare = "job_parallel_for"; push(labels, "count"); push(labels, "work"); push(labels, "ctx") }
|
||||
if (meth == "is_worker") { bare = "job_is_worker" }
|
||||
}
|
||||
if (ns == "Promise") {
|
||||
if (meth == "all") { bare = "prom_all"; push(labels, "handles") }
|
||||
|
|
@ -1363,6 +1365,16 @@ function emit_expr(e: Node) -> Val {
|
|||
return emit_new_struct(e.s, e.a)
|
||||
}
|
||||
if e.kind == E_LIST { return emit_list(e) } # [a, b, c] -> a fresh slice
|
||||
# fn name -> the function's address, for a worker entry point. The OS-thread runtime calls it
|
||||
# as void(i32, ptr), so that is the only signature a reference may have.
|
||||
if e.kind == E_FNREF {
|
||||
let d = find_fn(e.s)
|
||||
if d == null { perr(`fn {e.s}: no function called {e.s}`) }
|
||||
var ok = len(d.kids) == 2 and llty(d.ty) == "void"
|
||||
if ok { ok = llty(d.kids[0].ty) == "i32" and llty(d.kids[1].ty) == "ptr" }
|
||||
if not ok { perr(`fn {e.s}: a worker function takes (i: int, ctx: pointer) and returns nothing`) }
|
||||
return val(`@fn_{e.s}`, "pointer")
|
||||
}
|
||||
if e.kind == E_ID {
|
||||
let li = loc_find(e.s)
|
||||
if li >= 0 { return emit_load_at(loc_reg[li], loc_ty[li]) }
|
||||
|
|
|
|||
|
|
@ -118,7 +118,27 @@ function prefab_record(name: pointer, cn: pointer) -> Node {
|
|||
return merge_records(prefab_record(p.ty, cn), spawn_record(p, cn))
|
||||
}
|
||||
|
||||
# The threads rule's debug guard: a Job.parallel_for worker computes on what it was handed and
|
||||
# never changes the world, so spawn and despawn stop the program when a pool thread reaches them.
|
||||
# Emitted only into programs that use Job/Promise/Sync, which are the only ones with a pool.
|
||||
function emit_worker_guard(what: pointer, line: int) -> void {
|
||||
if not g_uses_jobs { return }
|
||||
g_uses_panic = true
|
||||
let w = emit_bind("call i32 @thr_is_worker()")
|
||||
let on = emit_bind(`icmp ne i32 {w}, 0`)
|
||||
let lok = lbl("wgok"); let lbad = lbl("wgbad")
|
||||
emit(` br i1 {on}, label %{lbad}, label %{lok}\n`)
|
||||
emit(`{lbad}:\n`)
|
||||
let prefix = emit_str_const(`{g_src_name}:{itoa(line)}: panic: `)
|
||||
let m = emit_str_const(`{what} on a Job.parallel_for worker thread (workers must not change the world)`)
|
||||
let se = emit_bind(stdstream_rhs(2))
|
||||
emit(` call i32 (ptr, ptr, ...) @fprintf(ptr {se}, ptr @.fmt_panic, ptr {prefix}, ptr {m})\n`)
|
||||
emit(" call void @exit(i32 1)\n unreachable\n")
|
||||
emit(`{lok}:\n`)
|
||||
}
|
||||
|
||||
function emit_spawn(st: Node) -> pointer {
|
||||
emit_worker_guard("spawn", st.line)
|
||||
let e = emit_bind("call i32 @L_alloc()")
|
||||
let model = spawn_model(st.s)
|
||||
let ak = find_arch_id(model)
|
||||
|
|
@ -153,6 +173,7 @@ function emit_spawn(st: Node) -> pointer {
|
|||
}
|
||||
|
||||
function emit_despawn(st: Node) -> void {
|
||||
emit_worker_guard("despawn", st.line)
|
||||
let v = emit_expr(st.a)
|
||||
# @OnDespawn: dispatch on the entity's kind and run the matching model's hook
|
||||
if len(g_ondespawn) > 0 {
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ const E_TRY: int = 52 # try EXPR else { ... } — recover a fallible
|
|||
# a=the fallible (result-typed) expression b=else block (its
|
||||
# trailing expression is the fallback) line=source line
|
||||
const E_LIST: int = 54 # [a, b, c] — a slice literal; kids=the elements, all of one type
|
||||
const E_FNREF: int = 55 # fn name — a top-level function as a value (s=the name); a worker entry point
|
||||
# statements
|
||||
const S_LET: int = 10
|
||||
const S_ASSIGN: int = 11
|
||||
|
|
|
|||
|
|
@ -192,6 +192,8 @@ function p_primary() -> Node {
|
|||
if (t.text == "null") { pi += 1; return node(E_NULL) }
|
||||
if (t.text == "new") { pi += 1; let n = node(E_NEW); n.s = ptype(); if is_op("{") { n.a = record() }; return n }
|
||||
if (t.text == "spawn") { return parse_spawn() } # spawn as an expression: the new entity
|
||||
# fn name — a top-level function as a value, for a worker entry point (Job.parallel_for)
|
||||
if (t.text == "fn") and (toks[pi + 1].kind == TK_ID) { pi += 1; let n = node(E_FNREF); n.line = t.line; n.s = eat_id(); return n }
|
||||
# try EXPR else { ... } — evaluate a fallible (result-typed) expression; on
|
||||
# `ok` the whole expression is its payload, on `err` the else block runs (with
|
||||
# the message bound to `error`) and its trailing expression is the fallback.
|
||||
|
|
|
|||
56182
selfhost/ludicc.seed.ll
56182
selfhost/ludicc.seed.ll
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -267,6 +267,12 @@ entry {
|
|||
# reached by name). Works headless too, so it is outside the windowed block —
|
||||
# macOS-only for now, which is where the toolchain runs.
|
||||
# On Windows the transport is http_win.ll, over WinHTTP.
|
||||
# Job.* / Promise.* / Sync.* link the OS threads behind Job.parallel_for and Sync.*:
|
||||
# threads.ll (pthreads) or threads_win.ll (Win32).
|
||||
if g_uses_jobs {
|
||||
if g_target_win { cmd = `{cmd} {join_path(home, "runtime/native/threads_win.ll")}` }
|
||||
else { cmd = `{cmd} {join_path(home, "runtime/native/threads.ll")}` }
|
||||
}
|
||||
if g_uses_http {
|
||||
if g_target_win {
|
||||
let httpw = join_path(home, "runtime/native/http_win.ll")
|
||||
|
|
|
|||
|
|
@ -184,7 +184,7 @@
|
|||
{ "name": "keyword.operator.logical.ludic", "match": "\\b(and|or|not)\\b" },
|
||||
{ "name": "keyword.other.clause.ludic", "match": "\\b(phase|query|on|cancellable|public|layer|start|shows|lasts|loads|then|export|internal)\\b" },
|
||||
{ "name": "keyword.other.ludic", "match": "\\b(import|extern)\\b" },
|
||||
{ "name": "storage.type.ludic", "match": "\\b(program|property|model|prefab|namespace|enum|ui|const|var|let|function|handler|entry|event|scene|state|test)\\b" },
|
||||
{ "name": "storage.type.ludic", "match": "\\b(program|property|model|prefab|namespace|enum|ui|const|var|let|function|fn|handler|entry|event|scene|state|test)\\b" },
|
||||
{ "name": "support.type.primitive.ludic", "match": "\\b(int|long|fixed|countdown|bool|entity|string|pointer|byte|words|fixeds|pointers|Vector|IVec2|Rect|void)\\b" },
|
||||
{ "name": "constant.language.boolean.ludic", "match": "\\b(true|false|null)\\b" },
|
||||
{ "name": "constant.language.phase.ludic", "match": "\\b(Start|Input|FixedUpdate|Update|LateUpdate|Render|Overlay)\\b" }
|
||||
|
|
|
|||
|
|
@ -184,7 +184,7 @@
|
|||
{ "name": "keyword.operator.logical.ludic", "match": "\\b(and|or|not)\\b" },
|
||||
{ "name": "keyword.other.clause.ludic", "match": "\\b(phase|query|on|cancellable|public|layer|start|shows|lasts|loads|then|export|internal)\\b" },
|
||||
{ "name": "keyword.other.ludic", "match": "\\b(import|extern)\\b" },
|
||||
{ "name": "storage.type.ludic", "match": "\\b(program|property|model|prefab|namespace|enum|ui|const|var|let|function|handler|entry|event|scene|state|test)\\b" },
|
||||
{ "name": "storage.type.ludic", "match": "\\b(program|property|model|prefab|namespace|enum|ui|const|var|let|function|fn|handler|entry|event|scene|state|test)\\b" },
|
||||
{ "name": "support.type.primitive.ludic", "match": "\\b(int|long|fixed|countdown|bool|entity|string|pointer|byte|words|fixeds|pointers|Vector|IVec2|Rect|void)\\b" },
|
||||
{ "name": "constant.language.boolean.ludic", "match": "\\b(true|false|null)\\b" },
|
||||
{ "name": "constant.language.phase.ludic", "match": "\\b(Start|Input|FixedUpdate|Update|LateUpdate|Render|Overlay)\\b" }
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ function compile_app(src: pointer, out: pointer, mode: int, save: bool) -> bool
|
|||
|
||||
if mode == 2 {
|
||||
if not shq(`{ludicc()} --headless {src} --emit-llvm -o {ll}`) { return false }
|
||||
if not shq(`{cc()} -O2 {ll}{gl_link_flags(ll)}{vk_link_flags(ll)}{http_link_flags(ll)}{pbf} -o {out}`) { return false }
|
||||
if not shq(`{cc()} -O2 {ll}{gl_link_flags(ll)}{vk_link_flags(ll)}{http_link_flags(ll)}{threads_link_flags(ll)}{pbf} -o {out}`) { return false }
|
||||
if not save { shell(`rm -f {ll}`) }
|
||||
return true
|
||||
}
|
||||
|
|
@ -58,7 +58,7 @@ function compile_app(src: pointer, out: pointer, mode: int, save: bool) -> bool
|
|||
# canonical `ludicc -o` path links it only when Audio.* is used.
|
||||
let cocoa = `{home}runtime/native/cocoa.ll`
|
||||
let audio = `{home}runtime/native/audio.ll`
|
||||
if not shq(`{cc()} -O2 {ll} {cocoa} {audio} -framework Cocoa -Wl,-needed_framework,GameController -Wl,-needed_framework,AVFoundation -Wl,-rpath,@loader_path{gl_link_flags(ll)}{vk_link_flags(ll)}{http_link_flags(ll)}{pbf} -o {out}`) { return false }
|
||||
if not shq(`{cc()} -O2 {ll} {cocoa} {audio} -framework Cocoa -Wl,-needed_framework,GameController -Wl,-needed_framework,AVFoundation -Wl,-rpath,@loader_path{gl_link_flags(ll)}{vk_link_flags(ll)}{http_link_flags(ll)}{threads_link_flags(ll)}{pbf} -o {out}`) { return false }
|
||||
if not save { shell(`rm -f {ll}`) }
|
||||
return true
|
||||
}
|
||||
|
|
@ -91,6 +91,14 @@ function http_link_flags(ll: pointer) -> pointer {
|
|||
return ` {ludic_home()}runtime/native/http.ll -Wl,-needed_framework,Foundation`
|
||||
}
|
||||
|
||||
# A program that uses Job.* / Promise.* / Sync.* calls the thr_* OS-thread runtime; link
|
||||
# threads.ll only then, as `ludicc -o` does.
|
||||
function threads_link_flags(ll: pointer) -> pointer {
|
||||
if not shq(`grep -q "call .*@thr_" {ll}`) { return "" }
|
||||
if host_windows() { return ` {ludic_home()}runtime/native/threads_win.ll` }
|
||||
return ` {ludic_home()}runtime/native/threads.ll`
|
||||
}
|
||||
|
||||
# the directory part of a path, without the trailing '/' ("" when there is none)
|
||||
function dir_of_path(p: pointer) -> pointer {
|
||||
var last = -1
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@ function cmd_sh_compile(shbin: pointer, in: pointer, outbin: pointer) -> int {
|
|||
function game_build_ok(shbin: pointer, game: pointer, outbin: pointer) -> bool {
|
||||
let ll = `{outbin}.ll`
|
||||
if not shq(`{shbin} {game} > {ll} 2>{tmp_dir()}/gb.err`) { return false }
|
||||
if not shq(`{cc()} -O2 {ll} -o {outbin} 2>{tmp_dir()}/gb.err`) { return false }
|
||||
if not shq(`{cc()} -O2 {ll}{threads_link_flags(ll)} -o {outbin} 2>{tmp_dir()}/gb.err`) { return false }
|
||||
shell(`rm -f {ll}`)
|
||||
return true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -578,6 +578,7 @@ function cmd_dev_test() -> int {
|
|||
feat_case("library/containers", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14", "containers.ludic (Dict string-keyed hash map + Set string set)")
|
||||
feat_case("library/numeric", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20", "numeric.ludic (Huge idle big-numbers + Angle wrapping radians + Percent clamped [0,1])")
|
||||
feat_case("library/jobs", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31", "jobs.ludic (Job background compute/defer/fulfill/cancel + Promise all/race/progress + Sync mutex/atomic/channel; issue #14)")
|
||||
feat_case("library/threads", "", "1 2 3 4 5 6 7", "threads.ludic (Job.parallel_for on OS threads + Sync atomic/mutex shared across workers + fn name worker entry)")
|
||||
feat_case("library/optionresult", "", "1 2 3 4 5 6 7 8 9 10 11 12", "optionresult.ludic (option some/none + result ok/err/try safety types)")
|
||||
feat_case("library/regex", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18", "regex.ludic (Regex match/find/groups/classes/quantifiers/replace + linear-time safety)")
|
||||
feat_case("library/grid", "", "1 2 3 4 5 6 7 8 9 10 11 12 13", "grid.ludic (Grid line/flood/line_of_sight + A* pathfinding over the tilemap)")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue