@amritk/nish-x86_64-linux 0.17.0 → 0.20.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.
Files changed (61) hide show
  1. package/INSTALL.md +43 -25
  2. package/bin/nish +0 -0
  3. package/package.json +2 -2
  4. package/runtime/nish.d.ts +55 -5
  5. package/runtime/nish.h +231 -20
  6. package/runtime/nish.mjs +16 -0
  7. package/runtime/runtime-aes.c +490 -0
  8. package/runtime/runtime-cross.c +366 -0
  9. package/runtime/runtime-host.c +7 -6
  10. package/runtime/runtime-net.c +93 -34
  11. package/runtime/runtime-os.c +213 -15
  12. package/runtime/runtime-parallel.c +590 -107
  13. package/runtime/runtime-simd.c +203 -0
  14. package/runtime/runtime-wasm.c +16 -3
  15. package/runtime/runtime.c +89 -23
  16. package/runtime/shim.mjs +71 -1
  17. package/scripts/build.sh +225 -20
  18. package/std/README.md +86 -55
  19. package/std/bytes.ts +163 -0
  20. package/std/crypto/LICENSE-libsecp256k1 +19 -0
  21. package/std/crypto/aes.ts +238 -76
  22. package/std/crypto/chacha20poly1305.ts +43 -9
  23. package/std/crypto/ed25519.ts +840 -0
  24. package/std/crypto/hkdf.ts +304 -12
  25. package/std/crypto/hmac.ts +245 -53
  26. package/std/crypto/p256.ts +1376 -42
  27. package/std/crypto/x25519.ts +55 -37
  28. package/std/crypto/x509-random.ts +71 -0
  29. package/std/crypto/x509.ts +9 -9
  30. package/std/json-tokenizer.ts +1296 -0
  31. package/std/json-typed.ts +1014 -0
  32. package/std/json-value.ts +1163 -0
  33. package/std/json.ts +1412 -70
  34. package/std/net/buffer-pool.ts +151 -0
  35. package/std/net/hpack.ts +416 -112
  36. package/std/net/http-fields.ts +93 -0
  37. package/std/net/http1-server.ts +417 -55
  38. package/std/net/http1.ts +31 -6
  39. package/std/net/http2-tls.ts +112 -24
  40. package/std/net/http2.ts +333 -75
  41. package/std/net/http3-server.ts +231 -45
  42. package/std/net/http3.ts +187 -28
  43. package/std/net/prefork.ts +412 -0
  44. package/std/net/quic-conn-params.ts +86 -16
  45. package/std/net/quic-listener.ts +576 -219
  46. package/std/net/quic-packet.ts +443 -58
  47. package/std/net/quic-recovery.ts +361 -30
  48. package/std/net/quic-stream.ts +434 -28
  49. package/std/net/quic.ts +922 -206
  50. package/std/net/router.ts +676 -0
  51. package/std/net/tls/codec.ts +426 -27
  52. package/std/net/tls/record-server.ts +478 -132
  53. package/std/net/tls/record.ts +133 -42
  54. package/std/net/tls/schedule.ts +464 -55
  55. package/std/net/tls/ticket.ts +610 -0
  56. package/std/net/tls-tcp.ts +204 -57
  57. package/std/net/tls.ts +1132 -305
  58. package/std/net/url.ts +571 -0
  59. package/std/random.ts +94 -0
  60. package/std/text.ts +77 -0
  61. package/std/threads.ts +438 -14
@@ -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/run.js beside the other two, and a program that
12
- * runs nothing in parallel pays nothing for it: `-ffunction-sections
13
- * -Wl,--gc-sections` drops every function no program calls, whichever
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, and P2's scopes
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 <unistd.h>
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 and handle arrays on the stack -- 64 jobs is 2 KB and 64 handles 512
42
- * bytes -- so a region never allocates to divide itself, which matters because
43
- * the arena it would allocate from is the very thing being made per-thread. */
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
- /* Cached, because `sysconf(_SC_NPROCESSORS_ONLN)` is not a cheap read: glibc
47
- * answers it from `sched_getaffinity` or by parsing
48
- * /sys/devices/system/cpu/online, and measured here it is 3.7 us -- which
49
- * `nish_parallel_range` would otherwise pay on every region, including the ones
50
- * too small to divide, where it was the entire cost. The race between two
51
- * threads filling it is benign (both compute the same number) but it is still a
52
- * race, so the word is read and written with relaxed atomic builtins (a plain
53
- * `int64_t`, since `__atomic_*` does not take an `_Atomic`-qualified pointer):
54
- * on x86-64 that is
55
- * a plain load and store and costs nothing, and it is defined behaviour.
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 online CPU count changes under a running process therefore
58
- * keeps the count it started with, which is the right trade for a number that
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
- long got = sysconf(_SC_NPROCESSORS_ONLN);
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
- static void *nish_par_worker(void *p) {
94
- nish_par_job *j = (nish_par_job *)p;
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
- j->body(j->lo, j->hi, j->ctx);
97
- /* This thread's arena, not the parent's: the storage class is what makes the
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
- nish_par_job jobs[NISH_PAR_MAX];
126
- pthread_t th[NISH_PAR_MAX];
127
- /* Contiguous chunks, the remainder spread one element at a time over the
128
- * first `len % n` of them, so the longest chunk is never more than one
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 workers then never write the same cache line except at one boundary. */
131
- int64_t base = len / n;
132
- int64_t rem = len % n;
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
- int64_t take = base + (i < rem ? 1 : 0);
136
- jobs[i].body = body;
137
- jobs[i].ctx = ctx;
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 = &region;
141
493
  }
142
494
 
143
- /* The caller runs chunk 0 itself, so N-way parallelism costs N-1 spawns and
144
- * the calling thread is not idle while it waits. Its chunk also runs in the
145
- * caller's arena, which means chunk 0 behaves exactly as a sequential call
146
- * would -- the asymmetry is deliberate and is why a body whose result is
147
- * allocated is the surface's error rather than this file's. */
148
- int started = 0;
149
- for (int64_t i = 1; i < n; i++) {
150
- if (pthread_create(&th[started], 0, nish_par_worker, &jobs[i]) != 0) break;
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
+ _exit(1);
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
- /* Every task but the first on a thread of its own, and the first on this
243
- * one, so N tasks cost N-1 spawns and this thread is not idle. A task inside
244
- * a region or another task's thread runs here, as a nested region does. */
245
- for (nish_task *t = mine->next; t; t = t->next) {
246
- t->started = nish_par_depth == 0 && pthread_create(&t->th, 0, nish_task_worker, t) == 0;
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
+ }