Line data Source code
1 : #define _GNU_SOURCE
2 :
3 : #include "fd_accdb.h"
4 : #include "../../util/fd_util.h"
5 :
6 : #include <pthread.h>
7 : #include <sched.h>
8 : #include <stdlib.h>
9 : #include <string.h>
10 : #include <unistd.h>
11 : #include <sys/mman.h>
12 :
13 : /* bench_accdb_mt: the accdb acquire/release hot path under many
14 : concurrent joins, the way exec tiles drive it.
15 :
16 : N worker threads, each with its own accdb join, run a transaction mix
17 : against one shared accdb on one fork: acquire read-only and writable
18 : accounts, mutate the writable ones, release with commit or revert.
19 : As the replay scheduler guarantees (fd_accdb.h), a writable acquire
20 : of an account never overlaps another acquire of it: each worker
21 : writes only its own partition of the accounts, and reads either its
22 : own partition, a shared range that is never written, or the shared
23 : hot read-only accounts. A background thread runs
24 : fd_accdb_background.
25 :
26 : --accounts total accounts (default 1M)
27 : --threads worker thread counts to sweep, e.g. 1,4,8,15
28 : (default 4); every step reuses the same accdb
29 : --duration seconds per step (default 10)
30 : --cache-gib cache size, fractional allowed (default 4); a
31 : working set larger than the cache exercises CLOCK
32 : eviction and write-back
33 : --cpu0 first CPU workers are pinned to (default 1; the
34 : background thread runs on cpu0-1)
35 : --mix "transfer" (1 RO + 2 RW accounts) or "mainnet" (the
36 : account counts and failure rates of bench_accdb_txn's
37 : mainnet mix); every account is BENCH_DATA_SZ bytes
38 : --ro-hot N N read-only accounts shared by every thread: every
39 : read-only acquire comes from them (as far as the
40 : txn's read-only count allows), like a few hot
41 : programs (default 0: off)
42 : --per-thread N each thread writes its own N accounts (default 0:
43 : the threads split --accounts); --accounts is then
44 : ro-hot + max threads * N
45 : --data-sz2 B every committed write alternates the account between
46 : BENCH_DATA_SZ and B bytes, so each release changes
47 : size class (default 0: off)
48 : --preset hot transfer mix, 4 hot read-only accounts, 512 accounts
49 : per thread, 3 GiB cache: everything stays cached, so
50 : it measures only what the threads share
51 :
52 : Reports per step total txn/s, per-thread acquire+release ns/txn,
53 : throughput speedup over the first step (perfect scaling from 1
54 : thread reads as the thread count), acquire/release ns per account
55 : and reservation retries (acquire_failed). */
56 :
57 : static uchar dummy_owner[ 32 ] = { 0xEE };
58 :
59 0 : #define BENCH_DATA_SZ (165UL)
60 0 : #define BENCH_MAX_ACCTS_PER_TXN (64UL)
61 0 : #define BENCH_MAX_THREADS (64UL)
62 0 : #define BENCH_MAX_LIVE_SLOTS (64UL)
63 0 : #define BENCH_PARTITION_CNT (8192UL)
64 0 : #define BENCH_PARTITION_SZ (1UL<<30UL)
65 0 : #define BENCH_MIN_RESERVED (192UL)
66 :
67 : /* Transaction archetype: ro_cnt read-only and rw_cnt writable
68 : accounts, picked with frequency weight (parts per 10000), reverted
69 : with probability fail (parts per 10000). */
70 :
71 : struct txn_archetype {
72 : uint ro_cnt;
73 : uint rw_cnt;
74 : uint weight;
75 : uint fail;
76 : };
77 :
78 : typedef struct txn_archetype txn_archetype_t;
79 :
80 : /* The account counts and failure rates of bench_accdb_txn's mainnet
81 : archetypes. */
82 :
83 : static txn_archetype_t const MIX_MAINNET[] = {
84 : /* ro rw weight fail */
85 : { 0, 1, 300, 25 }, /* single write */
86 : { 1, 2, 5930, 25 }, /* simple transfer/token op */
87 : { 2, 3, 330, 400 }, /* simple swap */
88 : { 4, 4, 370, 600 }, /* small DeFi */
89 : { 6, 6, 505, 700 }, /* medium DeFi / AMM */
90 : { 12, 12, 1354, 2700 }, /* complex DeFi */
91 : { 24, 24, 891, 3400 }, /* multi-hop DeFi */
92 : { 2, 48, 320, 1700 }, /* bulk operations */
93 : };
94 :
95 : static txn_archetype_t const MIX_TRANSFER[] = {
96 : /* ro rw weight fail */
97 : { 1, 2, 10000, 0 },
98 : };
99 :
100 0 : #define MIX_MAINNET_CNT (sizeof(MIX_MAINNET)/sizeof(MIX_MAINNET[0]))
101 0 : #define MIX_TRANSFER_CNT (sizeof(MIX_TRANSFER)/sizeof(MIX_TRANSFER[0]))
102 : #define MIX_MAX (8UL)
103 :
104 : /* Shared state, read by the workers. Set before they start, except
105 : g_go and g_stop, which are only accessed with FD_VOLATILE. */
106 :
107 : static txn_archetype_t const * g_mix;
108 : static ulong g_mix_cnt;
109 : static ulong g_accounts;
110 : static ulong g_threads;
111 : static ulong g_ro0; /* accounts [g_ro0,g_accounts) are never written */
112 : static ulong g_ro_hot; /* accounts [0,g_ro_hot) are shared read-only */
113 : static ulong g_per_thread;
114 : static ulong g_data_sz2;
115 : static ulong g_cpu0;
116 : static fd_accdb_fork_id_t g_fork;
117 : static int g_go;
118 : static int g_stop;
119 :
120 : static void
121 : make_pubkey( uchar pubkey[ static 32 ],
122 0 : ulong idx ) {
123 0 : fd_memset( pubkey, 0, 32UL );
124 0 : FD_STORE( ulong, pubkey, idx );
125 0 : FD_STORE( ulong, pubkey+8UL, fd_ulong_hash( idx ) );
126 0 : }
127 :
128 : static void
129 0 : pin_cpu( ulong cpu ) {
130 0 : cpu_set_t set;
131 0 : CPU_ZERO( &set );
132 0 : CPU_SET( cpu, &set );
133 0 : if( FD_UNLIKELY( pthread_setaffinity_np( pthread_self(), sizeof(set), &set ) ) ) FD_LOG_WARNING(( "could not pin to cpu %lu", cpu ));
134 0 : }
135 :
136 : struct __attribute__((aligned(64))) worker {
137 : ulong idx;
138 : fd_accdb_t * accdb;
139 :
140 : ulong txn_cnt;
141 : ulong acc_cnt;
142 : long acquire_ticks;
143 : long release_ticks;
144 : };
145 :
146 : typedef struct worker worker_t;
147 :
148 : static void *
149 0 : worker_main( void * arg ) {
150 0 : worker_t * w = (worker_t *)arg;
151 0 : pin_cpu( g_cpu0+w->idx );
152 :
153 0 : fd_rng_t _rng[1];
154 0 : fd_rng_t * rng = fd_rng_join( fd_rng_new( _rng, (uint)( 1000UL+w->idx ), 0UL ) );
155 :
156 : /* This worker writes [part0,part0+part). */
157 0 : ulong part = g_per_thread ? g_per_thread : ( g_ro0-g_ro_hot )/g_threads;
158 0 : ulong part0 = g_ro_hot + w->idx*part;
159 :
160 0 : uint cumul[ MIX_MAX ];
161 0 : uint sum = 0U;
162 0 : for( ulong i=0UL; i<g_mix_cnt; i++ ) {
163 0 : sum += g_mix[ i ].weight;
164 0 : cumul[ i ] = sum;
165 0 : }
166 :
167 0 : uchar pubkeys [ BENCH_MAX_ACCTS_PER_TXN ][ 32 ];
168 0 : uchar const * pks [ BENCH_MAX_ACCTS_PER_TXN ];
169 0 : ulong ids [ BENCH_MAX_ACCTS_PER_TXN ];
170 0 : int writable[ BENCH_MAX_ACCTS_PER_TXN ];
171 0 : fd_acc_t accs [ BENCH_MAX_ACCTS_PER_TXN ];
172 0 : for( ulong i=0UL; i<BENCH_MAX_ACCTS_PER_TXN; i++ ) pks[ i ] = pubkeys[ i ];
173 :
174 0 : while( !FD_VOLATILE_CONST( g_go ) ) FD_SPIN_PAUSE();
175 :
176 0 : while( !FD_VOLATILE_CONST( g_stop ) ) {
177 0 : uint r = fd_rng_uint( rng )%sum;
178 0 : ulong a = 0UL;
179 0 : while( r>=cumul[ a ] ) a++;
180 0 : txn_archetype_t const * arch = &g_mix[ a ];
181 0 : ulong rw_cnt = (ulong)arch->rw_cnt;
182 0 : ulong cnt = rw_cnt + (ulong)arch->ro_cnt;
183 :
184 : /* Distinct accounts within a txn: writable ones from our
185 : partition, read-only ones from the hot set first, then the
186 : never-written range or our partition. */
187 0 : for( ulong i=0UL; i<cnt; i++ ) {
188 0 : ulong id;
189 0 : for(;;) {
190 0 : if( i<rw_cnt ) id = part0 + fd_rng_ulong( rng )%part;
191 0 : else if( i-rw_cnt<g_ro_hot ) id = fd_rng_ulong( rng )%g_ro_hot;
192 0 : else if( g_ro0<g_accounts &&
193 0 : ( fd_rng_uint( rng )&1U ) ) id = g_ro0 + fd_rng_ulong( rng )%( g_accounts-g_ro0 );
194 0 : else id = part0 + fd_rng_ulong( rng )%part;
195 0 : int dup = 0;
196 0 : for( ulong j=0UL; j<i; j++ ) dup |= ids[ j ]==id;
197 0 : if( FD_LIKELY( !dup ) ) break;
198 0 : }
199 0 : ids[ i ] = id;
200 0 : make_pubkey( pubkeys[ i ], id );
201 0 : writable[ i ] = i<rw_cnt;
202 0 : }
203 0 : memset( accs, 0, cnt*sizeof(fd_acc_t) );
204 :
205 0 : long tacq_start = fd_tickcount();
206 0 : fd_accdb_acquire( w->accdb, g_fork, cnt, pks, writable, accs );
207 0 : long tacq = fd_tickcount()-tacq_start;
208 :
209 0 : int commit = ( fd_rng_uint( rng )%10000U )>=arch->fail;
210 0 : for( ulong i=0UL; i<rw_cnt; i++ ) {
211 0 : accs[ i ].commit = commit;
212 0 : if( !commit ) continue;
213 0 : if( FD_LIKELY( accs[ i ].data && accs[ i ].data_len ) ) accs[ i ].data[ 0 ] ^= 1;
214 0 : if( g_data_sz2 ) accs[ i ].data_len = accs[ i ].data_len==g_data_sz2 ? BENCH_DATA_SZ : g_data_sz2;
215 0 : }
216 :
217 0 : long trel_start = fd_tickcount();
218 0 : fd_accdb_release( w->accdb, cnt, accs );
219 0 : long trel = fd_tickcount()-trel_start;
220 :
221 0 : w->txn_cnt++;
222 0 : w->acc_cnt += cnt;
223 0 : w->acquire_ticks += tacq;
224 0 : w->release_ticks += trel;
225 0 : }
226 :
227 0 : fd_rng_delete( fd_rng_leave( rng ) );
228 0 : return NULL;
229 0 : }
230 :
231 : static void *
232 0 : background_main( void * arg ) {
233 0 : fd_accdb_t * accdb = (fd_accdb_t *)arg;
234 0 : pin_cpu( g_cpu0 ? g_cpu0-1UL : 0UL );
235 0 : while( !FD_VOLATILE_CONST( g_stop ) ) {
236 0 : int charge_busy = 0;
237 0 : fd_accdb_background( accdb, &charge_busy );
238 0 : if( !charge_busy ) FD_SPIN_PAUSE();
239 0 : }
240 0 : return NULL;
241 0 : }
242 :
243 : int
244 : main( int argc,
245 0 : char ** argv ) {
246 0 : fd_boot( &argc, &argv );
247 :
248 0 : char const * preset = fd_env_strip_cmdline_cstr( &argc, &argv, "--preset", NULL, "" );
249 0 : int hot = !strcmp( preset, "hot" );
250 0 : if( FD_UNLIKELY( preset[0] && !hot ) ) FD_LOG_ERR(( "unknown --preset %s", preset ));
251 :
252 0 : g_accounts = fd_env_strip_cmdline_ulong ( &argc, &argv, "--accounts", NULL, 1000000UL );
253 0 : char const * threads_str = fd_env_strip_cmdline_cstr ( &argc, &argv, "--threads", NULL, "4" );
254 0 : ulong duration_s = fd_env_strip_cmdline_ulong ( &argc, &argv, "--duration", NULL, 10UL );
255 0 : double cache_gib = fd_env_strip_cmdline_double( &argc, &argv, "--cache-gib", NULL, hot ? 3.0 : 4.0 );
256 0 : g_cpu0 = fd_env_strip_cmdline_ulong ( &argc, &argv, "--cpu0", NULL, 1UL );
257 0 : char const * mix = fd_env_strip_cmdline_cstr ( &argc, &argv, "--mix", NULL, "transfer" );
258 0 : g_ro_hot = fd_env_strip_cmdline_ulong ( &argc, &argv, "--ro-hot", NULL, hot ? 4UL : 0UL );
259 0 : g_per_thread = fd_env_strip_cmdline_ulong ( &argc, &argv, "--per-thread", NULL, hot ? 512UL : 0UL );
260 0 : g_data_sz2 = fd_env_strip_cmdline_ulong ( &argc, &argv, "--data-sz2", NULL, 0UL );
261 :
262 0 : if( !strcmp( mix, "mainnet" ) ) { g_mix = MIX_MAINNET; g_mix_cnt = MIX_MAINNET_CNT; }
263 0 : else if( !strcmp( mix, "transfer" ) ) { g_mix = MIX_TRANSFER; g_mix_cnt = MIX_TRANSFER_CNT; }
264 0 : else FD_LOG_ERR(( "unknown --mix %s", mix ));
265 0 : FD_TEST( g_mix_cnt<=MIX_MAX );
266 0 : FD_TEST( g_data_sz2<=(10UL<<20UL) );
267 :
268 0 : ulong sweep[ BENCH_MAX_THREADS ];
269 0 : ulong sweep_cnt = 0UL;
270 0 : ulong max_threads = 0UL;
271 0 : for( char const * p=threads_str; *p; ) {
272 0 : char * end;
273 0 : ulong t = strtoul( p, &end, 10 );
274 0 : FD_TEST( end!=p && t && t<=BENCH_MAX_THREADS && sweep_cnt<BENCH_MAX_THREADS );
275 0 : sweep[ sweep_cnt++ ] = t;
276 0 : max_threads = fd_ulong_max( max_threads, t );
277 0 : p = *end==',' ? end+1 : end;
278 0 : }
279 0 : if( FD_UNLIKELY( !sweep_cnt ) ) FD_LOG_ERR(( "--threads needs at least one thread count" ));
280 0 : if( FD_UNLIKELY( !duration_s ) ) FD_LOG_ERR(( "--duration must be positive" ));
281 :
282 : /* Each txn picks distinct accounts: its writable ones and the
283 : read-only ones beyond the hot set may all come from the worker's
284 : partition, so the partition must hold that many for every
285 : archetype (else the duplicate-rejection loop never ends). */
286 0 : ulong max_part_cnt = 0UL;
287 0 : for( ulong i=0UL; i<g_mix_cnt; i++ ) {
288 0 : ulong ro_cold = (ulong)g_mix[ i ].ro_cnt - fd_ulong_min( (ulong)g_mix[ i ].ro_cnt, g_ro_hot );
289 0 : max_part_cnt = fd_ulong_max( max_part_cnt, (ulong)g_mix[ i ].rw_cnt + ro_cold );
290 0 : }
291 0 : if( g_per_thread ) {
292 : /* Hot read-only accounts, then one partition per thread; no
293 : never-written range. */
294 0 : if( FD_UNLIKELY( g_per_thread<max_part_cnt ) ) FD_LOG_ERR(( "--per-thread %lu < %lu accounts some txn needs", g_per_thread, max_part_cnt ));
295 0 : g_accounts = g_ro_hot + max_threads*g_per_thread;
296 0 : g_ro0 = g_accounts;
297 0 : } else {
298 0 : g_ro0 = g_accounts - g_accounts/8UL;
299 0 : if( FD_UNLIKELY( g_ro0<=g_ro_hot || ( g_ro0-g_ro_hot )/max_threads<max_part_cnt ) )
300 0 : FD_LOG_ERR(( "--accounts %lu too small: partitions need %lu accounts each", g_accounts, max_part_cnt ));
301 0 : }
302 :
303 0 : int fd = memfd_create( "accdb_mt", 0 );
304 0 : if( FD_UNLIKELY( fd<0 ) ) FD_LOG_ERR(( "memfd_create failed" ));
305 :
306 0 : ulong cache_fp = (ulong)( cache_gib*(double)(1UL<<30UL) );
307 0 : ulong joiner_cnt = max_threads+2UL;
308 0 : ulong max_accounts = 2UL*g_accounts;
309 0 : ulong writes = g_accounts+4096UL;
310 0 : ulong shmem_fp = fd_accdb_shmem_footprint( max_accounts, BENCH_MAX_LIVE_SLOTS, writes, BENCH_PARTITION_CNT,
311 0 : cache_fp, BENCH_MIN_RESERVED, joiner_cnt, 0UL );
312 0 : FD_TEST( shmem_fp );
313 0 : void * shmem_mem = mmap( NULL, shmem_fp, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_NORESERVE, -1, 0 );
314 0 : FD_TEST( shmem_mem!=MAP_FAILED );
315 0 : fd_accdb_shmem_t * shmem = fd_accdb_shmem_join(
316 0 : fd_accdb_shmem_new( shmem_mem, max_accounts, BENCH_MAX_LIVE_SLOTS, writes, BENCH_PARTITION_CNT,
317 0 : BENCH_PARTITION_SZ, cache_fp, BENCH_MIN_RESERVED, 0, 42UL, joiner_cnt, 0UL ) );
318 0 : FD_TEST( shmem );
319 :
320 : /* One join per worker, plus the background join (compaction). */
321 0 : fd_accdb_t * joins[ BENCH_MAX_THREADS+1UL ];
322 0 : for( ulong i=0UL; i<=max_threads; i++ ) {
323 0 : int compaction = i==max_threads;
324 0 : void * mem = aligned_alloc( fd_accdb_align(), fd_accdb_footprint( BENCH_MAX_LIVE_SLOTS, compaction ) );
325 0 : FD_TEST( mem );
326 0 : joins[ i ] = fd_accdb_join( fd_accdb_new( mem, shmem, fd, 0UL, NULL, NULL, 0UL, compaction ) );
327 0 : FD_TEST( joins[ i ] );
328 0 : }
329 0 : fd_accdb_t * bg = joins[ max_threads ];
330 :
331 : /* Populate on a fork, root it (hot accounts are normally rooted), and
332 : run on a child. */
333 0 : fd_accdb_fork_id_t root = fd_accdb_attach_child( joins[ 0 ], (fd_accdb_fork_id_t){ .val = USHORT_MAX } );
334 0 : g_fork = fd_accdb_attach_child( joins[ 0 ], root );
335 0 : FD_LOG_NOTICE(( "populating %lu accounts ...", g_accounts ));
336 0 : for( ulong i=0UL; i<g_accounts; i++ ) {
337 0 : uchar pubkey[ 32 ];
338 0 : make_pubkey( pubkey, i );
339 0 : uchar const * pks[1] = { pubkey };
340 0 : int wr [1] = { 1 };
341 0 : fd_acc_t acc[1];
342 0 : memset( acc, 0, sizeof(acc) );
343 0 : fd_accdb_acquire( joins[ 0 ], g_fork, 1UL, pks, wr, acc );
344 0 : acc[0].lamports = i+1UL;
345 0 : acc[0].data_len = BENCH_DATA_SZ;
346 0 : memcpy( acc[0].owner, dummy_owner, 32UL );
347 0 : memset( acc[0].data, (uchar)i, BENCH_DATA_SZ );
348 0 : acc[0].commit = 1;
349 0 : fd_accdb_release( joins[ 0 ], 1UL, acc );
350 0 : }
351 0 : fd_accdb_advance_root( joins[ 0 ], g_fork );
352 0 : for( ulong i=0UL; i<64UL; i++ ) {
353 0 : int charge_busy = 0;
354 0 : fd_accdb_background( bg, &charge_busy );
355 0 : }
356 0 : g_fork = fd_accdb_attach_child( joins[ 0 ], g_fork );
357 :
358 : /* Ticks per ns, to convert the workers' tick counts. */
359 0 : long cal_ns0 = fd_log_wallclock();
360 0 : long cal_ticks0 = fd_tickcount();
361 0 : while( fd_log_wallclock()-cal_ns0<20000000L ) FD_SPIN_PAUSE();
362 0 : double tick_per_ns = (double)( fd_tickcount()-cal_ticks0 )/(double)( fd_log_wallclock()-cal_ns0 );
363 :
364 0 : worker_t * workers = aligned_alloc( alignof(worker_t), BENCH_MAX_THREADS*sizeof(worker_t) );
365 0 : FD_TEST( workers );
366 0 : pthread_t threads[ BENCH_MAX_THREADS ];
367 0 : double tps0 = 0.0;
368 :
369 0 : for( ulong s=0UL; s<sweep_cnt; s++ ) {
370 0 : g_threads = sweep[ s ];
371 0 : FD_VOLATILE( g_go ) = 0;
372 0 : FD_VOLATILE( g_stop ) = 0;
373 :
374 0 : ulong failed0 = 0UL;
375 0 : for( ulong i=0UL; i<g_threads; i++ ) failed0 += fd_accdb_metrics( joins[ i ] )->acquire_failed;
376 :
377 0 : pthread_t bg_thread;
378 0 : FD_TEST( !pthread_create( &bg_thread, NULL, background_main, bg ) );
379 0 : memset( workers, 0, g_threads*sizeof(worker_t) );
380 0 : for( ulong i=0UL; i<g_threads; i++ ) {
381 0 : workers[ i ].idx = i;
382 0 : workers[ i ].accdb = joins[ i ];
383 0 : FD_TEST( !pthread_create( &threads[ i ], NULL, worker_main, &workers[ i ] ) );
384 0 : }
385 :
386 0 : long start = fd_log_wallclock();
387 0 : FD_VOLATILE( g_go ) = 1;
388 0 : while( fd_log_wallclock()-start<(long)duration_s*1000000000L ) usleep( 10000 );
389 0 : FD_VOLATILE( g_stop ) = 1;
390 0 : long elapsed = fd_log_wallclock()-start;
391 0 : for( ulong i=0UL; i<g_threads; i++ ) FD_TEST( !pthread_join( threads[ i ], NULL ) );
392 0 : FD_TEST( !pthread_join( bg_thread, NULL ) );
393 :
394 0 : ulong txn_cnt = 0UL;
395 0 : ulong acc_cnt = 0UL;
396 0 : ulong failed = 0UL;
397 0 : double acquire_ticks = 0.0;
398 0 : double release_ticks = 0.0;
399 0 : for( ulong i=0UL; i<g_threads; i++ ) {
400 0 : txn_cnt += workers[ i ].txn_cnt;
401 0 : acc_cnt += workers[ i ].acc_cnt;
402 0 : acquire_ticks += (double)workers[ i ].acquire_ticks;
403 0 : release_ticks += (double)workers[ i ].release_ticks;
404 0 : failed += fd_accdb_metrics( joins[ i ] )->acquire_failed;
405 0 : }
406 0 : double secs = (double)elapsed/1e9;
407 0 : double ns_txn = ( acquire_ticks+release_ticks )/tick_per_ns/(double)txn_cnt; /* per thread */
408 0 : double tps = (double)txn_cnt/secs;
409 0 : if( !s ) tps0 = tps;
410 0 : FD_LOG_NOTICE(( "RESULT mix=%s threads=%lu accounts=%lu cache_gib=%.2f ro_hot=%lu per_thread=%lu data_sz2=%lu: "
411 0 : "%.0f txn/s, per thread %.0f txn/s, %.0f ns/txn, scaling %.2fx, acquire %.0f ns/acc, release %.0f ns/acc, reserve retries %lu",
412 0 : mix, g_threads, g_accounts, cache_gib, g_ro_hot, g_per_thread, g_data_sz2,
413 0 : tps, tps/(double)g_threads, ns_txn, tps/tps0,
414 0 : acquire_ticks/tick_per_ns/(double)acc_cnt, release_ticks/tick_per_ns/(double)acc_cnt, failed-failed0 ));
415 0 : }
416 :
417 0 : free( workers );
418 0 : for( ulong i=0UL; i<=max_threads; i++ ) free( joins[ i ] );
419 0 : munmap( shmem_mem, shmem_fp );
420 0 : close( fd );
421 0 : fd_halt();
422 0 : return 0;
423 0 : }
|