@amritk/nish-x86_64-linux 0.18.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/INSTALL.md +58 -28
- package/bin/nish +0 -0
- package/docs/AI.md +2759 -0
- package/package.json +3 -2
- package/runtime/browser.d.ts +14 -0
- package/runtime/browser.mjs +88 -0
- package/runtime/core/arrays.d.ts +18 -0
- package/runtime/core/arrays.mjs +125 -0
- package/runtime/core/checks.mjs +20 -0
- package/runtime/core/host.d.ts +70 -0
- package/runtime/core/host.mjs +311 -0
- package/runtime/core/numbers.d.ts +53 -0
- package/runtime/core/numbers.mjs +316 -0
- package/runtime/core/result.d.ts +41 -0
- package/runtime/core/result.mjs +127 -0
- package/runtime/core/strings.d.ts +14 -0
- package/runtime/core/strings.mjs +169 -0
- package/runtime/core.d.ts +9 -0
- package/runtime/core.mjs +55 -0
- package/runtime/host.d.ts +6 -0
- package/runtime/nish.d.ts +99 -5
- package/runtime/nish.h +289 -19
- package/runtime/nish.mjs +39 -3
- package/runtime/node.mjs +383 -0
- package/runtime/runtime-aes.c +490 -0
- package/runtime/runtime-bytes.c +401 -0
- package/runtime/runtime-cross.c +366 -0
- package/runtime/runtime-host.c +8 -7
- package/runtime/runtime-net.c +93 -34
- package/runtime/runtime-os.c +214 -16
- package/runtime/runtime-parallel.c +590 -107
- package/runtime/runtime-simd.c +235 -11
- package/runtime/runtime-term.c +504 -0
- package/runtime/runtime-wasm.c +5 -14
- package/runtime/runtime.c +257 -17
- package/runtime/shim.mjs +6 -915
- package/runtime/sqlite/LICENSE.md +85 -0
- package/runtime/sqlite/README.md +45 -0
- package/runtime/sqlite/nish-sqlite.c +359 -0
- package/runtime/sqlite/sqlite3.c +271131 -0
- package/runtime/sqlite/sqlite3.h +14447 -0
- package/scripts/build.sh +437 -28
- package/std/LICENSE-fast-float +23 -0
- package/std/LICENSE-fdlibm +8 -0
- package/std/README.md +108 -58
- package/std/base64.ts +116 -0
- package/std/bytes.ts +419 -0
- package/std/cli.ts +328 -0
- package/std/collections.ts +6 -5
- package/std/crypto/LICENSE-crypt_blowfish +26 -0
- package/std/crypto/LICENSE-libsecp256k1 +19 -0
- package/std/crypto/aes.ts +238 -76
- package/std/crypto/base64url.ts +125 -0
- package/std/crypto/bcrypt.ts +760 -0
- package/std/crypto/chacha20poly1305.ts +43 -9
- package/std/crypto/ed25519.ts +840 -0
- package/std/crypto/hkdf.ts +304 -12
- package/std/crypto/hmac.ts +382 -54
- package/std/crypto/p256.ts +932 -61
- package/std/crypto/x25519.ts +55 -37
- package/std/crypto/x509-random.ts +71 -0
- package/std/crypto/x509.ts +20 -131
- package/std/json-tokenizer.ts +4237 -0
- package/std/json-typed.ts +1014 -0
- package/std/json-value.ts +1182 -0
- package/std/json.ts +1202 -34
- package/std/math.ts +608 -0
- package/std/net/buffer-pool.ts +151 -0
- package/std/net/hpack.ts +416 -112
- package/std/net/http-fields.ts +93 -0
- package/std/net/http1-server.ts +501 -62
- package/std/net/http1.ts +31 -6
- package/std/net/http2-tls.ts +112 -24
- package/std/net/http2.ts +333 -75
- package/std/net/http3-server.ts +261 -43
- package/std/net/http3.ts +187 -28
- package/std/net/postgres.ts +1875 -0
- package/std/net/prefork.ts +412 -0
- package/std/net/quic-listener.ts +108 -11
- package/std/net/quic-packet.ts +243 -82
- package/std/net/quic-recovery.ts +361 -30
- package/std/net/quic-stream.ts +424 -24
- package/std/net/quic.ts +519 -167
- package/std/net/router.ts +676 -0
- package/std/net/tls/codec.ts +426 -27
- package/std/net/tls/record-server.ts +478 -132
- package/std/net/tls/record.ts +133 -42
- package/std/net/tls/schedule.ts +464 -55
- package/std/net/tls/ticket.ts +610 -0
- package/std/net/tls-tcp.ts +196 -53
- package/std/net/tls.ts +1018 -260
- package/std/net/url.ts +571 -0
- package/std/random.ts +94 -0
- package/std/regex.ts +4274 -0
- package/std/sqlite.ts +391 -0
- package/std/term-io.ts +978 -0
- package/std/term-pixels.ts +874 -0
- package/std/term-probe.ts +459 -0
- package/std/term-rate.ts +993 -0
- package/std/term.ts +2453 -0
- package/std/text.ts +90 -7
- package/std/threads.ts +438 -14
- package/std/yaml-lines.ts +141 -0
- package/std/yaml.ts +4675 -0
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
* and raising the syscall half's ceiling to make room would move the number a
|
|
9
9
|
* reader sees for "the operating-system surface" for a reason that has nothing
|
|
10
10
|
* to do with the operating-system surface. So this file carries its own
|
|
11
|
-
* measured ceiling in tests/
|
|
12
|
-
* runs nothing in parallel pays nothing for it:
|
|
13
|
-
* -Wl,--gc-sections` drops every function no program
|
|
14
|
-
* translation unit defined it.
|
|
11
|
+
* measured ceiling in tests/runtime-budgets.js beside the other two,
|
|
12
|
+
* and a program that runs nothing in parallel pays nothing for it:
|
|
13
|
+
* `-ffunction-sections -Wl,--gc-sections` drops every function no program
|
|
14
|
+
* calls, whichever translation unit defined it.
|
|
15
15
|
*
|
|
16
16
|
* It compiles in both configurations on purpose. With `-DNISH_THREADS` it
|
|
17
17
|
* spawns; without it, and on WASI where there are no threads to spawn, the same
|
|
@@ -22,8 +22,16 @@
|
|
|
22
22
|
* arrives with the same macro.
|
|
23
23
|
*
|
|
24
24
|
* docs/wp29-thread-surface.md is the surface this is the stage under: P1's
|
|
25
|
-
* `parallelMapInto` and `parallelReduce` divide a range here,
|
|
26
|
-
* run their tasks here (`nish_scope_spawn`, `nish_scope_join`)
|
|
25
|
+
* `parallelMapInto` and `parallelReduce` divide a range here, P2's scopes
|
|
26
|
+
* run their tasks here (`nish_scope_spawn`, `nish_scope_join`), P3's lock
|
|
27
|
+
* waits here when it finds its word held (`nish_mutex_wait`), and P3's
|
|
28
|
+
* channels keep their buffers here (`nish_channel_send`, `nish_channel_count`,
|
|
29
|
+
* `nish_channel_receive`).
|
|
30
|
+
*
|
|
31
|
+
* Since the pool (docs/wp20-threads.md, section 8e) a divided region or a
|
|
32
|
+
* joined scope no longer creates a thread per chunk or task: it hands them to
|
|
33
|
+
* workers that are started once, on the first region or join that divides, and
|
|
34
|
+
* park between calls.
|
|
27
35
|
*/
|
|
28
36
|
#include "nish.h"
|
|
29
37
|
|
|
@@ -34,37 +42,51 @@
|
|
|
34
42
|
#if defined(NISH_THREADS) && !defined(__wasi__) && !defined(__wasm__)
|
|
35
43
|
#define NISH_PAR_REAL 1
|
|
36
44
|
#include <pthread.h>
|
|
37
|
-
#include <
|
|
45
|
+
#include <sched.h>
|
|
46
|
+
#ifdef __GLIBC__
|
|
47
|
+
#include <malloc.h> /* `mallopt` and `malloc_trim`, for the pool */
|
|
48
|
+
#endif
|
|
38
49
|
#endif
|
|
39
50
|
|
|
40
51
|
/* The ceiling on how many threads one region may use. A fixed bound keeps the
|
|
41
|
-
* job
|
|
42
|
-
*
|
|
43
|
-
* the
|
|
52
|
+
* job array on the stack -- 64 jobs is 3.5 KB -- so a region never allocates
|
|
53
|
+
* to divide itself, which matters because the arena it would allocate from is
|
|
54
|
+
* the very thing being made per-thread. */
|
|
44
55
|
#define NISH_PAR_MAX 64
|
|
45
56
|
|
|
46
|
-
/*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
57
|
+
/* How many pieces a divided region is cut into per thread, grain permitting
|
|
58
|
+
* (`nish_parallel_range`). Eight, measured on bench/par-heavy.ts at four
|
|
59
|
+
* threads, nine interleaved rounds: 2 pieces was slower and noisier (868 ms
|
|
60
|
+
* median, 766-1121), 4 to 32 were flat within the copy band (805-822 ms), and
|
|
61
|
+
* so was bench/par-scale.ts's memory-bound map (262-270 ms). Among the flat
|
|
62
|
+
* counts, 8 bounds the tail a preempted core leaves idle at an eighth of one
|
|
63
|
+
* thread's share while keeping the claims per region at 8n. */
|
|
64
|
+
#define NISH_PAR_PIECES 8
|
|
65
|
+
|
|
66
|
+
/* Cached, because the answer is not a cheap read: `sysconf` alone was 3.7 us
|
|
67
|
+
* measured here (glibc parses /sys/devices/system/cpu/online), and the affinity
|
|
68
|
+
* mask and the cgroup quota `nish_cpu_online` reads are more system calls on
|
|
69
|
+
* top -- which `nish_parallel_range` would otherwise pay on every region,
|
|
70
|
+
* including the ones too small to divide, where it was the entire cost. The
|
|
71
|
+
* race between two threads filling it is benign (both compute the same number)
|
|
72
|
+
* but it is still a race, so the word is read and written with relaxed atomic
|
|
73
|
+
* builtins (a plain `int64_t`, since `__atomic_*` does not take an
|
|
74
|
+
* `_Atomic`-qualified pointer): on x86-64 that is a plain load and store and
|
|
75
|
+
* costs nothing, and it is defined behaviour.
|
|
56
76
|
*
|
|
57
|
-
* A machine whose
|
|
58
|
-
*
|
|
77
|
+
* A machine whose CPU count changes under a running process therefore keeps
|
|
78
|
+
* the count it started with, which is the right trade for a number that
|
|
59
79
|
* decides how many threads a loop is divided into. */
|
|
60
80
|
static int64_t nish_cpu_cached = 0;
|
|
61
81
|
|
|
82
|
+
/* The CPUs a region may use: `nish_cpu_online` (runtime-os.c), which
|
|
83
|
+
* `availableParallelism` reads in every build, and 1 in the sequential one,
|
|
84
|
+
* which divides nothing. */
|
|
62
85
|
int64_t nish_cpu_count(void) {
|
|
63
86
|
int64_t n = __atomic_load_n(&nish_cpu_cached, __ATOMIC_RELAXED);
|
|
64
87
|
if (n != 0) return n;
|
|
65
88
|
#ifdef NISH_PAR_REAL
|
|
66
|
-
|
|
67
|
-
n = got > 0 ? (int64_t)got : 1;
|
|
89
|
+
n = nish_cpu_online();
|
|
68
90
|
#else
|
|
69
91
|
n = 1;
|
|
70
92
|
#endif
|
|
@@ -73,15 +95,6 @@ int64_t nish_cpu_count(void) {
|
|
|
73
95
|
}
|
|
74
96
|
|
|
75
97
|
#ifdef NISH_PAR_REAL
|
|
76
|
-
/* One chunk of the range, and where to run it. Passed by pointer to the
|
|
77
|
-
* worker, so it lives in the caller's frame until the join. */
|
|
78
|
-
typedef struct {
|
|
79
|
-
nish_par_body body;
|
|
80
|
-
void *ctx;
|
|
81
|
-
int64_t lo;
|
|
82
|
-
int64_t hi;
|
|
83
|
-
} nish_par_job;
|
|
84
|
-
|
|
85
98
|
/* Non-zero inside a region, so a body that itself calls `nish_parallel_range`
|
|
86
99
|
* runs its range on one thread instead of multiplying the thread count by the
|
|
87
100
|
* nesting depth. Thread-local, so the guard is per worker rather than a lock.
|
|
@@ -89,20 +102,357 @@ typedef struct {
|
|
|
89
102
|
* scheduler: work stealing is what a library would do here and it is not what
|
|
90
103
|
* this file is for. */
|
|
91
104
|
static NISH_TLS int nish_par_depth = 0;
|
|
105
|
+
/* Non-zero on a pool worker or an unpooled job's thread, whose arena holds
|
|
106
|
+
* nothing of the caller's. */
|
|
107
|
+
static NISH_TLS int nish_pool_thread = 0;
|
|
108
|
+
|
|
109
|
+
/* ---- The worker pool (docs/wp20-threads.md, section 8e) ----
|
|
110
|
+
*
|
|
111
|
+
* One unit of work for another thread: a region's chunk or a scope's task,
|
|
112
|
+
* each of which embeds one of these as its first member. `pending` is the
|
|
113
|
+
* count its caller waits on, and lives in the caller's frame until the last
|
|
114
|
+
* job is done; the unpooled path waits on the job's own thread, `th`, instead. */
|
|
115
|
+
typedef struct nish_job {
|
|
116
|
+
struct nish_job *next;
|
|
117
|
+
void (*run)(struct nish_job *);
|
|
118
|
+
union {
|
|
119
|
+
int64_t *pending; /* pooled */
|
|
120
|
+
pthread_t th; /* unpooled */
|
|
121
|
+
};
|
|
122
|
+
} nish_job;
|
|
123
|
+
|
|
124
|
+
/* The workers, started once and parked between calls, and the one queue they
|
|
125
|
+
* take jobs from. It is strictly first in, first out, and the thread that
|
|
126
|
+
* hands jobs in takes them off the same front until the queue is empty, which
|
|
127
|
+
* is what keeps channels deadlock-free with any number of workers, none
|
|
128
|
+
* included: a receiving task is spawned after every task that sends to it
|
|
129
|
+
* (wp29 C6, docs/wp29-thread-surface.md section 4.4), so by the time any
|
|
130
|
+
* thread takes the receiver off the queue, every sender has already been
|
|
131
|
+
* taken by a thread that is running it. A work-stealing deque, which takes
|
|
132
|
+
* the newest job first, would break that, and is declined for that reason.
|
|
133
|
+
*
|
|
134
|
+
* `state` is 0 until the first divided region or join starts the workers, 1
|
|
135
|
+
* once it has, and -1 when `nish_pool_disable` keeps the runtime unpooled. It
|
|
136
|
+
* changes only under the lock, and is read without it once it is not 0. */
|
|
137
|
+
static struct {
|
|
138
|
+
pthread_mutex_t mu;
|
|
139
|
+
pthread_cond_t work; /* a worker waits here for a job */
|
|
140
|
+
pthread_cond_t done; /* a caller waits here for its count to reach 0 */
|
|
141
|
+
nish_job *head, *tail;
|
|
142
|
+
int state;
|
|
143
|
+
int low; /* the trim threshold was lowered for a parked pool */
|
|
144
|
+
} nish_pool = {PTHREAD_MUTEX_INITIALIZER, PTHREAD_COND_INITIALIZER, PTHREAD_COND_INITIALIZER, 0, 0, 0, 0};
|
|
145
|
+
|
|
146
|
+
/* The front of the queue, or 0 when it is empty. The lock is held. `head` is
|
|
147
|
+
* also read without it, by a thread spinning before it parks, so it is
|
|
148
|
+
* written with an atomic store (relaxed: the lock still orders the job's
|
|
149
|
+
* contents, since nothing takes a job without it). */
|
|
150
|
+
static nish_job *nish_job_take(void) {
|
|
151
|
+
nish_job *j = nish_pool.head;
|
|
152
|
+
if (j) {
|
|
153
|
+
__atomic_store_n(&nish_pool.head, j->next, __ATOMIC_RELAXED);
|
|
154
|
+
if (!j->next) nish_pool.tail = 0;
|
|
155
|
+
}
|
|
156
|
+
return j;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/* One job finished, with the lock held. Nothing may touch `j` after the count
|
|
160
|
+
* reaches 0, since its caller is then free to return and take its frame. The
|
|
161
|
+
* count is read without the lock by a caller spinning, as `head` is. */
|
|
162
|
+
static void nish_job_done(nish_job *j) {
|
|
163
|
+
if (__atomic_sub_fetch(j->pending, 1, __ATOMIC_RELAXED) == 0) pthread_cond_broadcast(&nish_pool.done);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/* How long a thread that finds nothing to do looks again before it sleeps:
|
|
167
|
+
* 2,048 pauses, measured at 18 us on the four-core Xeon of docs/wp20-threads.md
|
|
168
|
+
* section 8e. A program that divides one region usually divides the next one
|
|
169
|
+
* straight after it, and a worker still spinning picks that up without the
|
|
170
|
+
* futex wake that costs more than the spin; a caller still spinning sees its
|
|
171
|
+
* last job finish without one either. The look is a relaxed load and a pause,
|
|
172
|
+
* which leaves the core to its hyperthread sibling meanwhile. */
|
|
173
|
+
#define NISH_POOL_SPIN 2048
|
|
174
|
+
|
|
175
|
+
static void nish_cpu_relax(void) {
|
|
176
|
+
#if defined(__x86_64__) || defined(__i386__)
|
|
177
|
+
__builtin_ia32_pause();
|
|
178
|
+
#elif defined(__aarch64__)
|
|
179
|
+
__asm__ __volatile__("yield");
|
|
180
|
+
#endif
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/* Takes jobs off the front of the queue and runs them until `*pending` is 0,
|
|
184
|
+
* spinning and then sleeping on `cv` while the queue is empty and the count is
|
|
185
|
+
* not. A worker passes a count that never reaches 0 and sleeps on `work`; a
|
|
186
|
+
* caller passes its own count and sleeps on `done`. The count is read under
|
|
187
|
+
* the lock before the caller returns, which orders every job's writes before
|
|
188
|
+
* it. A worker's arena is its own, not a caller's: the storage class is what
|
|
189
|
+
* makes the two disjoint (WP20 T0). Nothing a job leaves in it outlives the
|
|
190
|
+
* job -- a string or an array of scalars a chunk or a task answers, sends or
|
|
191
|
+
* stores through a guard is copied out before the job returns (runtime-cross.c),
|
|
192
|
+
* and the language refuses every other result that would point into the arena
|
|
193
|
+
* -- so it is rewound for the next one rather than freed, and kept only when it
|
|
194
|
+
* is one ordinary chunk. A larger one goes back to malloc, which under glibc
|
|
195
|
+
* keeps it for the next job (`nish_pool_ready`'s thresholds); a worker that
|
|
196
|
+
* then finds the queue empty for a second hands the memory back once before it
|
|
197
|
+
* sleeps for good, so a parked pool does not hold a burst's temporaries.
|
|
198
|
+
* `malloc_trim` alone cuts back only the main arena's top: a worker's own arena
|
|
199
|
+
* is cut back when a large free reaches its top and the top is past the trim
|
|
200
|
+
* threshold. So the worker lowers the threshold to glibc's own 128 KB, frees a
|
|
201
|
+
* 16 MB block through its arena to make that cut, and then trims. The
|
|
202
|
+
* threshold stays low until the next region starts (`nish_jobs_run`), so the
|
|
203
|
+
* blocks a join adopted from a worker's arena go back to the system too when
|
|
204
|
+
* the caller frees them later. The lowering and the restore both run under the
|
|
205
|
+
* pool lock, so a region never runs with the low threshold; the free and the
|
|
206
|
+
* trim run outside it, and a region that starts meanwhile only makes that free
|
|
207
|
+
* keep its block in the arena, to be trimmed after the next job it runs. */
|
|
208
|
+
static void nish_pool_drain(int64_t *pending, pthread_cond_t *cv, int worker) {
|
|
209
|
+
#ifdef __GLIBC__
|
|
210
|
+
int held = 0;
|
|
211
|
+
#endif
|
|
212
|
+
pthread_mutex_lock(&nish_pool.mu);
|
|
213
|
+
for (;;) {
|
|
214
|
+
nish_job *j = nish_job_take();
|
|
215
|
+
if (j) {
|
|
216
|
+
pthread_mutex_unlock(&nish_pool.mu);
|
|
217
|
+
j->run(j);
|
|
218
|
+
if (worker) {
|
|
219
|
+
nish_reset_arena();
|
|
220
|
+
if (nish_arena.cap > 65536) nish_free_arena();
|
|
221
|
+
#ifdef __GLIBC__
|
|
222
|
+
held = 1;
|
|
223
|
+
#endif
|
|
224
|
+
}
|
|
225
|
+
pthread_mutex_lock(&nish_pool.mu);
|
|
226
|
+
nish_job_done(j);
|
|
227
|
+
} else if (*pending == 0) {
|
|
228
|
+
break;
|
|
229
|
+
} else {
|
|
230
|
+
pthread_mutex_unlock(&nish_pool.mu);
|
|
231
|
+
for (int i = 0; i < NISH_POOL_SPIN && __atomic_load_n(pending, __ATOMIC_RELAXED) &&
|
|
232
|
+
!__atomic_load_n(&nish_pool.head, __ATOMIC_RELAXED);
|
|
233
|
+
i++)
|
|
234
|
+
nish_cpu_relax();
|
|
235
|
+
pthread_mutex_lock(&nish_pool.mu);
|
|
236
|
+
if (*pending != 0 && !nish_pool.head) {
|
|
237
|
+
#ifdef __GLIBC__
|
|
238
|
+
if (held) {
|
|
239
|
+
struct timespec t;
|
|
240
|
+
clock_gettime(CLOCK_REALTIME, &t);
|
|
241
|
+
t.tv_sec++;
|
|
242
|
+
if (pthread_cond_timedwait(cv, &nish_pool.mu, &t) != 0) {
|
|
243
|
+
held = 0;
|
|
244
|
+
if (!nish_pool.low) mallopt(M_TRIM_THRESHOLD, 128 << 10);
|
|
245
|
+
nish_pool.low = 1;
|
|
246
|
+
pthread_mutex_unlock(&nish_pool.mu);
|
|
247
|
+
void *volatile top = malloc(16 << 20);
|
|
248
|
+
free(top);
|
|
249
|
+
malloc_trim(0);
|
|
250
|
+
pthread_mutex_lock(&nish_pool.mu);
|
|
251
|
+
}
|
|
252
|
+
} else
|
|
253
|
+
#endif
|
|
254
|
+
pthread_cond_wait(cv, &nish_pool.mu);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
pthread_mutex_unlock(&nish_pool.mu);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
static void *nish_pool_worker(void *unused) {
|
|
262
|
+
(void)unused;
|
|
263
|
+
nish_par_depth = 1;
|
|
264
|
+
nish_pool_thread = 1;
|
|
265
|
+
int64_t never = 1;
|
|
266
|
+
nish_pool_drain(&never, &nish_pool.work, 1);
|
|
267
|
+
return 0;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/* A forked child has only the thread that forked, so it starts with an empty
|
|
271
|
+
* queue and no workers, and starts its own on its first divided call. It takes
|
|
272
|
+
* fresh copies of the lock and the condition variables rather than the ones
|
|
273
|
+
* the parent's workers may have held or been waiting on, so nothing has to be
|
|
274
|
+
* held across the fork. */
|
|
275
|
+
static void nish_pool_child(void) {
|
|
276
|
+
nish_pool.head = nish_pool.tail = 0;
|
|
277
|
+
if (nish_pool.state > 0) nish_pool.state = 0;
|
|
278
|
+
pthread_mutex_init(&nish_pool.mu, 0);
|
|
279
|
+
pthread_cond_init(&nish_pool.work, 0);
|
|
280
|
+
pthread_cond_init(&nish_pool.done, 0);
|
|
281
|
+
}
|
|
92
282
|
|
|
93
|
-
|
|
94
|
-
|
|
283
|
+
/* Whether jobs go to the pool, starting it on the first call: one worker per
|
|
284
|
+
* CPU but the caller's, up to `NISH_PAR_MAX`. A worker that cannot be created
|
|
285
|
+
* leaves fewer, and none is a pool that works, because the caller drains the
|
|
286
|
+
* queue itself. Detached, since nothing joins a worker: it parks until the
|
|
287
|
+
* process exits. */
|
|
288
|
+
static int nish_pool_ready(void) {
|
|
289
|
+
int state = __atomic_load_n(&nish_pool.state, __ATOMIC_ACQUIRE);
|
|
290
|
+
if (state != 0) return state > 0;
|
|
291
|
+
pthread_mutex_lock(&nish_pool.mu);
|
|
292
|
+
if (nish_pool.state == 0) {
|
|
293
|
+
int64_t n = nish_cpu_count() - 1;
|
|
294
|
+
if (n > NISH_PAR_MAX - 1) n = NISH_PAR_MAX - 1; /* a region queues at most this many */
|
|
295
|
+
for (int64_t i = 0; i < n; i++) {
|
|
296
|
+
pthread_t th;
|
|
297
|
+
if (pthread_create(&th, 0, nish_pool_worker, 0) != 0) break;
|
|
298
|
+
pthread_detach(th);
|
|
299
|
+
}
|
|
300
|
+
pthread_atfork(0, 0, nish_pool_child);
|
|
301
|
+
#ifdef __GLIBC__
|
|
302
|
+
/* glibc otherwise trims the heap top, or unmaps, every chunk of 128 KB or
|
|
303
|
+
* more an arena release frees, and a task or element that builds a 1 MB
|
|
304
|
+
* temporary then pays a page fault per 4 KB of it on every call: 200 such
|
|
305
|
+
* tasks took 314 ms at one thread, 48 ms with these. glibc's own threshold
|
|
306
|
+
* starts at 128 KB and rises only once an mmapped chunk is freed, to at
|
|
307
|
+
* most 32 MB on a 64-bit target with a trim threshold of twice that, so
|
|
308
|
+
* these are the values it reaches by itself, from the first call on; a
|
|
309
|
+
* worker's idle trim in `nish_pool_drain` lowers the trim threshold and
|
|
310
|
+
* hands the memory back once the pool parks, and the next region sets it
|
|
311
|
+
* to 64 MB again. Without that a parked pool kept each worker's arena top,
|
|
312
|
+
* 82 MB after four maps of 1.2 million strings on four threads, rather than
|
|
313
|
+
* 2.3 MB (`tests/link/par_map_idle`). Set here, once a program first runs work on other threads; a
|
|
314
|
+
* region below its grain, or at one thread, runs as the loop it would have
|
|
315
|
+
* been and keeps that loop's costs. A pool with no worker, at one CPU or
|
|
316
|
+
* `NISH_NUM_THREADS=1`, has nothing that parks to trim, and keeps what a
|
|
317
|
+
* join freed up to the 64 MB at which glibc trims the heap top itself.
|
|
318
|
+
* Trimming at the end of each join instead took that idle 18 MB to 2 MB,
|
|
319
|
+
* but took ten joins of eight 16 MB tasks from 93 ms to 121-138 ms, since
|
|
320
|
+
* every join faulted its temporaries in again. */
|
|
321
|
+
mallopt(M_MMAP_THRESHOLD, 32 << 20);
|
|
322
|
+
mallopt(M_TRIM_THRESHOLD, 64 << 20);
|
|
323
|
+
#endif
|
|
324
|
+
__atomic_store_n(&nish_pool.state, 1, __ATOMIC_RELEASE);
|
|
325
|
+
}
|
|
326
|
+
state = nish_pool.state;
|
|
327
|
+
pthread_mutex_unlock(&nish_pool.mu);
|
|
328
|
+
return state > 0;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/* The unpooled path's thread: one job, then the arena freed, since nothing
|
|
332
|
+
* else has a pointer to it once the thread exits. */
|
|
333
|
+
static void *nish_job_thread(void *p) {
|
|
334
|
+
nish_job *j = (nish_job *)p;
|
|
95
335
|
nish_par_depth = 1;
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
* two disjoint (WP20 T0), so a worker can release every byte it bumped
|
|
99
|
-
* without the parent losing one. A worker that did not do this would leak its
|
|
100
|
-
* whole arena at thread exit, since nothing else has a pointer to it. */
|
|
336
|
+
nish_pool_thread = 1;
|
|
337
|
+
j->run(j);
|
|
101
338
|
nish_free_arena();
|
|
102
339
|
return 0;
|
|
103
340
|
}
|
|
341
|
+
|
|
342
|
+
/* Runs `lead` on this thread and the list `rest` elsewhere, oldest first, and
|
|
343
|
+
* returns once every one of them has finished. The caller runs `lead` itself,
|
|
344
|
+
* so N jobs keep N-1 other threads busy and this one is not idle, and then
|
|
345
|
+
* takes queued jobs off the front until the queue is empty: a job no worker is
|
|
346
|
+
* free for runs here, in order, so running short of threads costs speed and
|
|
347
|
+
* never an answer, and a region never waits for a worker to wake when it
|
|
348
|
+
* could be doing the work. A job taken here runs in this thread's arena, as
|
|
349
|
+
* `lead` does.
|
|
350
|
+
*
|
|
351
|
+
* The queue is shared, so a caller can take a job another caller queued. That
|
|
352
|
+
* is safe for the same reason the order is: a job only ever waits on jobs
|
|
353
|
+
* queued before it, and those have all been taken by a thread already. */
|
|
354
|
+
static void nish_jobs_run(nish_job *lead, nish_job *rest) {
|
|
355
|
+
int64_t pending = 0;
|
|
356
|
+
int pooled = rest && nish_par_depth == 0 && nish_pool_ready();
|
|
357
|
+
nish_job *last = 0, *inline_from = 0;
|
|
358
|
+
if (pooled) {
|
|
359
|
+
for (nish_job *j = rest; j; j = j->next) {
|
|
360
|
+
j->pending = &pending;
|
|
361
|
+
pending++;
|
|
362
|
+
last = j;
|
|
363
|
+
}
|
|
364
|
+
pthread_mutex_lock(&nish_pool.mu);
|
|
365
|
+
if (nish_pool.tail) nish_pool.tail->next = rest;
|
|
366
|
+
else __atomic_store_n(&nish_pool.head, rest, __ATOMIC_RELAXED);
|
|
367
|
+
nish_pool.tail = last;
|
|
368
|
+
#ifdef __GLIBC__
|
|
369
|
+
/* A parked pool lowered the trim threshold (`nish_pool_drain`): a burst
|
|
370
|
+
keeps its temporaries again. */
|
|
371
|
+
if (nish_pool.low) mallopt(M_TRIM_THRESHOLD, 64 << 20);
|
|
372
|
+
nish_pool.low = 0;
|
|
373
|
+
#endif
|
|
374
|
+
/* One wake per job, rather than every parked worker for a job or two. */
|
|
375
|
+
for (int64_t i = 0; i < pending; i++) pthread_cond_signal(&nish_pool.work);
|
|
376
|
+
pthread_mutex_unlock(&nish_pool.mu);
|
|
377
|
+
} else {
|
|
378
|
+
/* Unpooled, a thread per job, and every job from the first thread that
|
|
379
|
+
* cannot be created on runs here. Inside a job there is no thread at all:
|
|
380
|
+
* a region or a scope in a chunk or a task runs on that one thread, in
|
|
381
|
+
* order, rather than multiplying the threads by the nesting depth. */
|
|
382
|
+
for (inline_from = rest; inline_from && nish_par_depth == 0; inline_from = inline_from->next)
|
|
383
|
+
if (pthread_create(&inline_from->th, 0, nish_job_thread, inline_from) != 0) break;
|
|
384
|
+
}
|
|
385
|
+
nish_par_depth++;
|
|
386
|
+
lead->run(lead);
|
|
387
|
+
if (pooled) {
|
|
388
|
+
nish_pool_drain(&pending, &nish_pool.done, 0);
|
|
389
|
+
} else {
|
|
390
|
+
for (nish_job *j = inline_from; j; j = j->next) j->run(j);
|
|
391
|
+
for (nish_job *j = rest; j != inline_from; j = j->next) pthread_join(j->th, 0);
|
|
392
|
+
}
|
|
393
|
+
nish_par_depth--;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/* A divided region: `pieces` contiguous pieces of `[0, len)`, at most one
|
|
397
|
+
* element apart in size, which the threads running it claim one at a time off
|
|
398
|
+
* `next`. It lives in the caller's frame until every piece has run. */
|
|
399
|
+
typedef struct {
|
|
400
|
+
nish_par_body body;
|
|
401
|
+
void *ctx;
|
|
402
|
+
int64_t base; /* the shortest piece's length */
|
|
403
|
+
int64_t rem; /* the first `rem` pieces are one longer */
|
|
404
|
+
int64_t pieces;
|
|
405
|
+
int64_t next; /* the next piece to claim; atomic */
|
|
406
|
+
} nish_par_region;
|
|
407
|
+
|
|
408
|
+
/* One thread's share of a region: the pieces it claims, however many. */
|
|
409
|
+
typedef struct {
|
|
410
|
+
nish_job job;
|
|
411
|
+
nish_par_region *region;
|
|
412
|
+
} nish_par_job;
|
|
413
|
+
|
|
414
|
+
static void nish_par_piece(nish_par_region *r, int64_t i) {
|
|
415
|
+
int64_t lo = i * r->base + (i < r->rem ? i : r->rem);
|
|
416
|
+
r->body(lo, lo + r->base + (i < r->rem ? 1 : 0), r->ctx);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/* Claims pieces until none is left. A worker rewinds its arena between them,
|
|
420
|
+
* and frees it when the chunk it kept is larger than an ordinary one, so each
|
|
421
|
+
* piece it runs finds the arena a whole job finds (`nish_pool_drain`). The
|
|
422
|
+
* claim is relaxed: the pieces are fixed before the jobs are queued, under the
|
|
423
|
+
* lock, and the join orders every piece's writes before the caller goes on. */
|
|
424
|
+
static void nish_par_run(nish_job *j) {
|
|
425
|
+
nish_par_region *r = ((nish_par_job *)j)->region;
|
|
426
|
+
int64_t i;
|
|
427
|
+
while ((i = __atomic_fetch_add(&r->next, 1, __ATOMIC_RELAXED)) < r->pieces) {
|
|
428
|
+
nish_par_piece(r, i);
|
|
429
|
+
if (nish_pool_thread) {
|
|
430
|
+
nish_reset_arena();
|
|
431
|
+
if (nish_arena.cap > 65536) nish_free_arena();
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/* The caller's share: piece 0 first, which `next` starts past, then whatever
|
|
437
|
+
* is left when it is done. */
|
|
438
|
+
static void nish_par_run_lead(nish_job *j) {
|
|
439
|
+
nish_par_piece(((nish_par_job *)j)->region, 0);
|
|
440
|
+
nish_par_run(j);
|
|
441
|
+
}
|
|
104
442
|
#endif
|
|
105
443
|
|
|
444
|
+
/* Without the pool every later region and join creates its threads and joins
|
|
445
|
+
* them, as before it existed. For a library that can be unloaded, a Node addon
|
|
446
|
+
* above all: a parked worker would outlive the code it runs. nish.h calls this
|
|
447
|
+
* from a constructor in the shim, before any call can start the pool. */
|
|
448
|
+
void nish_pool_disable(void) {
|
|
449
|
+
#ifdef NISH_PAR_REAL
|
|
450
|
+
pthread_mutex_lock(&nish_pool.mu);
|
|
451
|
+
if (nish_pool.state == 0) __atomic_store_n(&nish_pool.state, -1, __ATOMIC_RELAXED);
|
|
452
|
+
pthread_mutex_unlock(&nish_pool.mu);
|
|
453
|
+
#endif
|
|
454
|
+
}
|
|
455
|
+
|
|
106
456
|
void nish_parallel_range(nish_par_body body, void *ctx, int64_t len, int64_t grain) {
|
|
107
457
|
if (len <= 0) return;
|
|
108
458
|
#ifdef NISH_PAR_REAL
|
|
@@ -122,42 +472,34 @@ void nish_parallel_range(nish_par_body body, void *ctx, int64_t len, int64_t gra
|
|
|
122
472
|
return;
|
|
123
473
|
}
|
|
124
474
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
*
|
|
475
|
+
/* More pieces than threads, so a thread that runs slow -- a core shared
|
|
476
|
+
* with another process, a worker that woke late -- takes fewer of them
|
|
477
|
+
* rather than holding up the join: with one piece a thread, the join on the
|
|
478
|
+
* four-core Xeon of docs/BENCHMARKS.md waited a quarter of each region for
|
|
479
|
+
* the slowest. Never finer than the grain, which is what a piece must be
|
|
480
|
+
* worth; a claim is one atomic add. */
|
|
481
|
+
int64_t pieces = n * NISH_PAR_PIECES;
|
|
482
|
+
if (chunks < pieces) pieces = chunks;
|
|
483
|
+
/* Contiguous pieces, the remainder spread one element at a time over the
|
|
484
|
+
* first `len % pieces` of them, so the longest is never more than one
|
|
129
485
|
* element longer than the shortest. Contiguous rather than strided because
|
|
130
|
-
* two
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
int64_t at = 0;
|
|
486
|
+
* two threads then never write the same cache line except at a boundary. */
|
|
487
|
+
nish_par_region region = {body, ctx, len / pieces, len % pieces, pieces, 1};
|
|
488
|
+
nish_par_job jobs[NISH_PAR_MAX];
|
|
134
489
|
for (int64_t i = 0; i < n; i++) {
|
|
135
|
-
|
|
136
|
-
jobs[i].
|
|
137
|
-
jobs[i].
|
|
138
|
-
jobs[i].lo = at;
|
|
139
|
-
jobs[i].hi = at + take;
|
|
140
|
-
at += take;
|
|
490
|
+
jobs[i].job.next = &jobs[i + 1].job;
|
|
491
|
+
jobs[i].job.run = i == 0 ? nish_par_run_lead : nish_par_run;
|
|
492
|
+
jobs[i].region = ®ion;
|
|
141
493
|
}
|
|
142
494
|
|
|
143
|
-
/* The caller runs
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
started++;
|
|
152
|
-
}
|
|
153
|
-
nish_par_depth++;
|
|
154
|
-
jobs[0].body(jobs[0].lo, jobs[0].hi, jobs[0].ctx);
|
|
155
|
-
/* A chunk whose thread could not be created runs here. Running short of
|
|
156
|
-
* threads must cost speed and never correctness, so there is no path on which
|
|
157
|
-
* a range is silently skipped. */
|
|
158
|
-
for (int64_t i = (int64_t)started + 1; i < n; i++) jobs[i].body(jobs[i].lo, jobs[i].hi, jobs[i].ctx);
|
|
159
|
-
nish_par_depth--;
|
|
160
|
-
for (int i = 0; i < started; i++) pthread_join(th[i], 0);
|
|
495
|
+
/* The caller runs piece 0 itself, in its own arena, which means piece 0
|
|
496
|
+
* behaves exactly as a sequential call would -- the asymmetry is deliberate
|
|
497
|
+
* and is why what a body's result may hold is the surface's question rather
|
|
498
|
+
* than this file's: a string or an array of scalars is copied out of the
|
|
499
|
+
* piece's arena, whichever it ran in, by the trampoline the compiler emits
|
|
500
|
+
* (runtime-cross.c), and the surface refuses every other allocated result. */
|
|
501
|
+
jobs[n - 1].job.next = 0;
|
|
502
|
+
nish_jobs_run(&jobs[0].job, jobs[0].job.next);
|
|
161
503
|
#else
|
|
162
504
|
(void)grain;
|
|
163
505
|
body(0, len, ctx);
|
|
@@ -178,50 +520,53 @@ void nish_parallel_range(nish_par_body body, void *ctx, int64_t len, int64_t gra
|
|
|
178
520
|
* joins it, so the list needs no lock, and scopes may nest: a join takes its
|
|
179
521
|
* own scope's tasks out of the list and leaves the rest. */
|
|
180
522
|
typedef struct nish_task {
|
|
523
|
+
#ifdef NISH_PAR_REAL
|
|
524
|
+
nish_job job;
|
|
525
|
+
#endif
|
|
181
526
|
struct nish_task *next;
|
|
182
527
|
void *scope;
|
|
183
528
|
nish_task_fn run;
|
|
184
529
|
nish_task_fn finish;
|
|
185
|
-
#ifdef NISH_PAR_REAL
|
|
186
|
-
pthread_t th;
|
|
187
|
-
int started;
|
|
188
|
-
#endif
|
|
189
530
|
/* The copied payload, aligned for any field it holds. */
|
|
190
531
|
uint64_t payload[];
|
|
191
532
|
} nish_task;
|
|
192
533
|
|
|
193
534
|
static NISH_TLS nish_task *nish_tasks = 0;
|
|
194
535
|
|
|
536
|
+
#ifdef NISH_PAR_REAL
|
|
537
|
+
static void nish_task_run(nish_job *j) {
|
|
538
|
+
nish_task *t = (nish_task *)j;
|
|
539
|
+
t->run(t->payload);
|
|
540
|
+
}
|
|
541
|
+
#endif
|
|
542
|
+
|
|
543
|
+
/* The end of the program when an allocation of this file's fails: the scope's
|
|
544
|
+
* task list and a channel's buffer are the two, and neither has a way to go on
|
|
545
|
+
* without the memory. */
|
|
546
|
+
static void nish_par_oom(void) {
|
|
547
|
+
nish_panic_claim();
|
|
548
|
+
(void)!write(2, "nish: out of memory\n", 20);
|
|
549
|
+
nish_fatal_exit();
|
|
550
|
+
}
|
|
551
|
+
|
|
195
552
|
void nish_scope_spawn(void *scope, nish_task_fn run, nish_task_fn finish, const void *payload, int64_t size) {
|
|
196
553
|
nish_task *t = (nish_task *)malloc(sizeof(nish_task) + (size_t)size);
|
|
197
554
|
/* Out of memory is the end of the program, as it is for every other
|
|
198
555
|
* allocation the runtime makes: running the task here instead would store
|
|
199
556
|
* its answer ahead of tasks filed before it, and a later spawn into the same
|
|
200
557
|
* slot has to be the one that stays. */
|
|
201
|
-
if (!t)
|
|
202
|
-
(void)!write(2, "nish: out of memory\n", 20);
|
|
203
|
-
_exit(1);
|
|
204
|
-
}
|
|
558
|
+
if (!t) nish_par_oom();
|
|
205
559
|
t->scope = scope;
|
|
206
560
|
t->run = run;
|
|
207
561
|
t->finish = finish;
|
|
562
|
+
#ifdef NISH_PAR_REAL
|
|
563
|
+
t->job.run = nish_task_run;
|
|
564
|
+
#endif
|
|
208
565
|
memcpy(t->payload, payload, (size_t)size);
|
|
209
566
|
t->next = nish_tasks;
|
|
210
567
|
nish_tasks = t;
|
|
211
568
|
}
|
|
212
569
|
|
|
213
|
-
#ifdef NISH_PAR_REAL
|
|
214
|
-
static void *nish_task_worker(void *p) {
|
|
215
|
-
nish_task *t = (nish_task *)p;
|
|
216
|
-
/* A task is one thread's work, as a chunk is, so a region inside it runs on
|
|
217
|
-
* this thread rather than multiplying the threads. */
|
|
218
|
-
nish_par_depth = 1;
|
|
219
|
-
t->run(t->payload);
|
|
220
|
-
nish_free_arena(); /* as `nish_par_worker`: this thread's arena, and only it */
|
|
221
|
-
return 0;
|
|
222
|
-
}
|
|
223
|
-
#endif
|
|
224
|
-
|
|
225
570
|
void nish_scope_join(void *scope) {
|
|
226
571
|
/* This scope's tasks, oldest first: the list is newest first, and moving each
|
|
227
572
|
* one to the front of `mine` reverses it. */
|
|
@@ -232,6 +577,9 @@ void nish_scope_join(void *scope) {
|
|
|
232
577
|
if (t->scope == scope) {
|
|
233
578
|
*link = t->next;
|
|
234
579
|
t->next = mine;
|
|
580
|
+
#ifdef NISH_PAR_REAL
|
|
581
|
+
t->job.next = (nish_job *)mine;
|
|
582
|
+
#endif
|
|
235
583
|
mine = t;
|
|
236
584
|
} else {
|
|
237
585
|
link = &t->next;
|
|
@@ -239,21 +587,11 @@ void nish_scope_join(void *scope) {
|
|
|
239
587
|
}
|
|
240
588
|
if (!mine) return;
|
|
241
589
|
#ifdef NISH_PAR_REAL
|
|
242
|
-
/*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
}
|
|
248
|
-
nish_par_depth++;
|
|
249
|
-
mine->run(mine->payload);
|
|
250
|
-
for (nish_task *t = mine->next; t; t = t->next) {
|
|
251
|
-
if (!t->started) t->run(t->payload);
|
|
252
|
-
}
|
|
253
|
-
nish_par_depth--;
|
|
254
|
-
for (nish_task *t = mine->next; t; t = t->next) {
|
|
255
|
-
if (t->started) pthread_join(t->th, 0);
|
|
256
|
-
}
|
|
590
|
+
/* The first task on this thread and the rest on the pool, in the order they
|
|
591
|
+
* were spawned, which is the order a task no worker is free for runs in here,
|
|
592
|
+
* and the order every task runs in when the scope is joined inside another
|
|
593
|
+
* job (`nish_jobs_run`). */
|
|
594
|
+
nish_jobs_run(&mine->job, mine->job.next);
|
|
257
595
|
#else
|
|
258
596
|
for (nish_task *t = mine; t; t = t->next) t->run(t->payload);
|
|
259
597
|
#endif
|
|
@@ -266,3 +604,148 @@ void nish_scope_join(void *scope) {
|
|
|
266
604
|
free(t);
|
|
267
605
|
}
|
|
268
606
|
}
|
|
607
|
+
|
|
608
|
+
/* ---- A lock's contended path (wp29 P3) ----
|
|
609
|
+
*
|
|
610
|
+
* The compiler takes a free lock inline, so this runs only when the word was
|
|
611
|
+
* held: another task is inside its critical section, which the language keeps
|
|
612
|
+
* short -- one lock at a time, nothing waited on while it is held -- so a few
|
|
613
|
+
* relaxed reads usually see it free again. Past that the holder may have been
|
|
614
|
+
* descheduled, and spinning would only keep it off the core, so each further
|
|
615
|
+
* try first yields. The swap that takes the word is the acquire the inline
|
|
616
|
+
* path would have made. */
|
|
617
|
+
void nish_mutex_wait(int32_t *word) {
|
|
618
|
+
#ifdef NISH_PAR_REAL
|
|
619
|
+
int spins = 0;
|
|
620
|
+
#endif
|
|
621
|
+
for (;;) {
|
|
622
|
+
int32_t free_word = 0;
|
|
623
|
+
if (__atomic_load_n(word, __ATOMIC_RELAXED) == 0 &&
|
|
624
|
+
__atomic_compare_exchange_n(word, &free_word, 1, 0, __ATOMIC_ACQUIRE, __ATOMIC_RELAXED))
|
|
625
|
+
return;
|
|
626
|
+
#ifdef NISH_PAR_REAL
|
|
627
|
+
if (spins < 64) spins++;
|
|
628
|
+
else sched_yield();
|
|
629
|
+
#endif
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/* ---- A channel of scalars (wp29 P3, section 4.4) ----
|
|
634
|
+
*
|
|
635
|
+
* The `Channel` object lives in the arena of the thread that declared it, and
|
|
636
|
+
* holds one 64-bit word, `state`, that this file owns: 0 before the channel is
|
|
637
|
+
* first used, the address of its `nish_channel` after that, and
|
|
638
|
+
* `NISH_CHANNEL_DRAINED` once its receiver has drained it closed and it has
|
|
639
|
+
* been given back. Every function here takes the address of that word.
|
|
640
|
+
*
|
|
641
|
+
* The buffer is outside every arena, because the senders' arenas are freed
|
|
642
|
+
* when their scope joins and a value sent has to outlive its sender. Each slot
|
|
643
|
+
* is 8 bytes: the compiler widens a scalar into one and narrows it back
|
|
644
|
+
* (src/emit-parallel.ts). The channel is unbounded, so `send` never waits.
|
|
645
|
+
*
|
|
646
|
+
* `pending` is what keeps the channel open: 1 for the scope that has not yet
|
|
647
|
+
* started to join, plus 1 for each task filed that may send on it. The
|
|
648
|
+
* compiler adds a sender at its spawn and takes it away when the task returns,
|
|
649
|
+
* and takes the scope's 1 away just before the join (`nish_channel_count`,
|
|
650
|
+
* -1), so the channel closes when the join has started and every sender has
|
|
651
|
+
* returned -- at once, when no task may send on it. All three happen on the
|
|
652
|
+
* thread that declared the channel until the join, so the word is made there,
|
|
653
|
+
* before any task can touch it, and never by two threads at once.
|
|
654
|
+
*
|
|
655
|
+
* One receiver, the language's rule, takes values off the front. When it
|
|
656
|
+
* finds the channel closed and empty it gives the buffer back and records
|
|
657
|
+
* that, so a second loop over the channel ends at once. Nothing is left to
|
|
658
|
+
* touch it then: every sender has returned, and the parent sends only before
|
|
659
|
+
* the scope's block (docs/LANGUAGE.md, "Channels"). */
|
|
660
|
+
#define NISH_CHANNEL_DRAINED 1
|
|
661
|
+
|
|
662
|
+
typedef struct {
|
|
663
|
+
int64_t *slots;
|
|
664
|
+
int64_t head, tail, cap;
|
|
665
|
+
int64_t pending;
|
|
666
|
+
#ifdef NISH_PAR_REAL
|
|
667
|
+
pthread_mutex_t mu;
|
|
668
|
+
pthread_cond_t cv;
|
|
669
|
+
#endif
|
|
670
|
+
} nish_channel;
|
|
671
|
+
|
|
672
|
+
static nish_channel *nish_channel_of(int64_t *state) {
|
|
673
|
+
nish_channel *c = (nish_channel *)(intptr_t)*state;
|
|
674
|
+
if (!c) {
|
|
675
|
+
c = (nish_channel *)calloc(1, sizeof(nish_channel));
|
|
676
|
+
if (!c) nish_par_oom();
|
|
677
|
+
c->pending = 1;
|
|
678
|
+
#ifdef NISH_PAR_REAL
|
|
679
|
+
pthread_mutex_init(&c->mu, 0);
|
|
680
|
+
pthread_cond_init(&c->cv, 0);
|
|
681
|
+
#endif
|
|
682
|
+
*state = (int64_t)(intptr_t)c;
|
|
683
|
+
}
|
|
684
|
+
#ifdef NISH_PAR_REAL
|
|
685
|
+
pthread_mutex_lock(&c->mu);
|
|
686
|
+
#endif
|
|
687
|
+
return c;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/* Every path out of `nish_channel_of` holds the lock; this gives it back,
|
|
691
|
+
* waking the receiver first, since whatever changed may be what it waits on. */
|
|
692
|
+
static void nish_channel_done(nish_channel *c) {
|
|
693
|
+
#ifdef NISH_PAR_REAL
|
|
694
|
+
pthread_cond_broadcast(&c->cv);
|
|
695
|
+
pthread_mutex_unlock(&c->mu);
|
|
696
|
+
#else
|
|
697
|
+
(void)c;
|
|
698
|
+
#endif
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
void nish_channel_send(int64_t *state, int64_t value) {
|
|
702
|
+
/* The checker keeps a send off a drained channel (every sender has returned
|
|
703
|
+
* before it closes, and the parent sends only before its scope); this keeps
|
|
704
|
+
* the sentinel from being read as an address if that ever slips. */
|
|
705
|
+
if (*state == NISH_CHANNEL_DRAINED) return;
|
|
706
|
+
nish_channel *c = nish_channel_of(state);
|
|
707
|
+
if (c->tail == c->cap) {
|
|
708
|
+
c->cap = c->cap ? c->cap * 2 : 64;
|
|
709
|
+
c->slots = (int64_t *)realloc(c->slots, (size_t)c->cap * sizeof(int64_t));
|
|
710
|
+
if (!c->slots) nish_par_oom();
|
|
711
|
+
}
|
|
712
|
+
c->slots[c->tail++] = value;
|
|
713
|
+
nish_channel_done(c);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
void nish_channel_count(int64_t *state, int64_t delta) {
|
|
717
|
+
if (*state == NISH_CHANNEL_DRAINED) return; /* as in `nish_channel_send` */
|
|
718
|
+
nish_channel *c = nish_channel_of(state);
|
|
719
|
+
c->pending += delta;
|
|
720
|
+
nish_channel_done(c);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
int32_t nish_channel_receive(int64_t *state, int64_t *out) {
|
|
724
|
+
if (*state == NISH_CHANNEL_DRAINED) return 0;
|
|
725
|
+
nish_channel *c = nish_channel_of(state);
|
|
726
|
+
#ifdef NISH_PAR_REAL
|
|
727
|
+
while (c->head == c->tail && c->pending != 0) pthread_cond_wait(&c->cv, &c->mu);
|
|
728
|
+
#endif
|
|
729
|
+
if (c->head != c->tail) {
|
|
730
|
+
*out = c->slots[c->head++];
|
|
731
|
+
/* Emptied: the next value goes back to the front, so a buffer only grows
|
|
732
|
+
* while the senders are ahead of the receiver. */
|
|
733
|
+
if (c->head == c->tail) c->head = c->tail = 0;
|
|
734
|
+
nish_channel_done(c);
|
|
735
|
+
return 1;
|
|
736
|
+
}
|
|
737
|
+
/* Without threads every sender has returned by the time a receiver runs:
|
|
738
|
+
* the tasks run one after another in spawn order, and every sender is
|
|
739
|
+
* spawned before the one receiver (C6). An empty channel that is still open
|
|
740
|
+
* here is a broken invariant, not a wait. */
|
|
741
|
+
if (c->pending != 0) __builtin_trap();
|
|
742
|
+
#ifdef NISH_PAR_REAL
|
|
743
|
+
pthread_mutex_unlock(&c->mu);
|
|
744
|
+
pthread_mutex_destroy(&c->mu);
|
|
745
|
+
pthread_cond_destroy(&c->cv);
|
|
746
|
+
#endif
|
|
747
|
+
free(c->slots);
|
|
748
|
+
free(c);
|
|
749
|
+
*state = NISH_CHANNEL_DRAINED;
|
|
750
|
+
return 0;
|
|
751
|
+
}
|