2 * qsp.c - QEMU Synchronization Profiler
4 * Copyright (C) 2018, Emilio G. Cota <cota@braap.org>
6 * License: GNU GPL, version 2 or later.
7 * See the COPYING file in the top-level directory.
9 * QSP profiles the time spent in synchronization primitives, which can
10 * help diagnose performance problems, e.g. scalability issues when
13 * The primitives currently supported are mutexes, recursive mutexes and
14 * condition variables. Note that not all related functions are intercepted;
15 * instead we profile only those functions that can have a performance impact,
16 * either due to blocking (e.g. cond_wait, mutex_lock) or cache line
17 * contention (e.g. mutex_lock, mutex_trylock).
19 * QSP's design focuses on speed and scalability. This is achieved
20 * by having threads do their profiling entirely on thread-local data.
21 * The appropriate thread-local data is found via a QHT, i.e. a concurrent hash
22 * table. To aggregate data in order to generate a report, we iterate over
23 * all entries in the hash table. Depending on the number of threads and
24 * synchronization objects this might be expensive, but note that it is
25 * very rarely called -- reports are generated only when requested by users.
27 * Reports are generated as a table where each row represents a call site. A
28 * call site is the triplet formed by the __file__ and __LINE__ of the caller
29 * as well as the address of the "object" (i.e. mutex, rec. mutex or condvar)
30 * being operated on. Optionally, call sites that operate on different objects
31 * of the same type can be coalesced, which can be particularly useful when
32 * profiling dynamically-allocated objects.
34 * Alternative designs considered:
36 * - Use an off-the-shelf profiler such as mutrace. This is not a viable option
37 * for us because QEMU has __malloc_hook set (by one of the libraries it
38 * uses); leaving this hook unset is required to avoid deadlock in mutrace.
40 * - Use a glib HT for each thread, protecting each HT with its own lock.
41 * This isn't simpler than the current design, and is 10% slower in the
42 * atomic_add-bench microbenchmark (-m option).
44 * - For reports, just use a binary tree as we aggregate data, instead of having
45 * an intermediate hash table. This would simplify the code only slightly, but
46 * would perform badly if there were many threads and objects to track.
48 * - Wrap operations on qsp entries with RCU read-side critical sections, so
49 * that qsp_reset() can delete entries. Unfortunately, the overhead of calling
50 * rcu_read_lock/unlock slows down atomic_add-bench -m by 24%. Having
51 * a snapshot that is updated on qsp_reset() avoids this overhead.
54 * - Lennart Poettering's mutrace: http://0pointer.de/blog/projects/mutrace.html
55 * - Lozi, David, Thomas, Lawall and Muller. "Remote Core Locking: Migrating
56 * Critical-Section Execution to Improve the Performance of Multithreaded
57 * Applications", USENIX ATC'12.
59 #include "qemu/osdep.h"
60 #include "qemu/thread.h"
61 #include "qemu/timer.h"
64 #include "qemu/xxhash.h"
75 const char *file
; /* i.e. __FILE__; shortened later */
79 typedef struct QSPCallSite QSPCallSite
;
83 const QSPCallSite
*callsite
;
86 unsigned int n_objs
; /* count of coalesced objs; only used for reporting */
88 typedef struct QSPEntry QSPEntry
;
94 typedef struct QSPSnapshot QSPSnapshot
;
96 /* initial sizing for hash tables */
97 #define QSP_INITIAL_SIZE 64
99 /* If this file is moved, QSP_REL_PATH should be updated accordingly */
100 #define QSP_REL_PATH "util/qsp.c"
102 /* this file's full path. Used to present all call sites with relative paths */
103 static size_t qsp_qemu_path_len
;
105 /* the address of qsp_thread gives us a unique 'thread ID' */
106 static __thread
int qsp_thread
;
109 * Call sites are the same for all threads, so we track them in a separate hash
110 * table to save memory.
112 static struct qht qsp_callsite_ht
;
114 static struct qht qsp_ht
;
115 static QSPSnapshot
*qsp_snapshot
;
116 static bool qsp_initialized
, qsp_initializing
;
118 static const char * const qsp_typenames
[] = {
119 [QSP_MUTEX
] = "mutex",
120 [QSP_BQL_MUTEX
] = "BQL mutex",
121 [QSP_REC_MUTEX
] = "rec_mutex",
122 [QSP_CONDVAR
] = "condvar",
125 QemuMutexLockFunc qemu_bql_mutex_lock_func
= qemu_mutex_lock_impl
;
126 QemuMutexLockFunc qemu_mutex_lock_func
= qemu_mutex_lock_impl
;
127 QemuMutexTrylockFunc qemu_mutex_trylock_func
= qemu_mutex_trylock_impl
;
128 QemuRecMutexLockFunc qemu_rec_mutex_lock_func
= qemu_rec_mutex_lock_impl
;
129 QemuRecMutexTrylockFunc qemu_rec_mutex_trylock_func
=
130 qemu_rec_mutex_trylock_impl
;
131 QemuCondWaitFunc qemu_cond_wait_func
= qemu_cond_wait_impl
;
134 * It pays off to _not_ hash callsite->file; hashing a string is slow, and
135 * without it we still get a pretty unique hash.
138 uint32_t do_qsp_callsite_hash(const QSPCallSite
*callsite
, uint64_t ab
)
140 uint64_t cd
= (uint64_t)(uintptr_t)callsite
->obj
;
141 uint32_t e
= callsite
->line
;
142 uint32_t f
= callsite
->type
;
144 return qemu_xxhash6(ab
, cd
, e
, f
);
148 uint32_t qsp_callsite_hash(const QSPCallSite
*callsite
)
150 return do_qsp_callsite_hash(callsite
, 0);
153 static inline uint32_t do_qsp_entry_hash(const QSPEntry
*entry
, uint64_t a
)
155 return do_qsp_callsite_hash(entry
->callsite
, a
);
158 static uint32_t qsp_entry_hash(const QSPEntry
*entry
)
160 return do_qsp_entry_hash(entry
, (uint64_t)(uintptr_t)entry
->thread_ptr
);
163 static uint32_t qsp_entry_no_thread_hash(const QSPEntry
*entry
)
165 return do_qsp_entry_hash(entry
, 0);
168 /* without the objects we need to hash the file name to get a decent hash */
169 static uint32_t qsp_entry_no_thread_obj_hash(const QSPEntry
*entry
)
171 const QSPCallSite
*callsite
= entry
->callsite
;
172 uint64_t ab
= g_str_hash(callsite
->file
);
173 uint64_t cd
= callsite
->line
;
174 uint32_t e
= callsite
->type
;
176 return qemu_xxhash5(ab
, cd
, e
);
179 static bool qsp_callsite_cmp(const void *ap
, const void *bp
)
181 const QSPCallSite
*a
= ap
;
182 const QSPCallSite
*b
= bp
;
186 a
->line
== b
->line
&&
187 a
->type
== b
->type
&&
188 (a
->file
== b
->file
|| !strcmp(a
->file
, b
->file
)));
191 static bool qsp_callsite_no_obj_cmp(const void *ap
, const void *bp
)
193 const QSPCallSite
*a
= ap
;
194 const QSPCallSite
*b
= bp
;
197 (a
->line
== b
->line
&&
198 a
->type
== b
->type
&&
199 (a
->file
== b
->file
|| !strcmp(a
->file
, b
->file
)));
202 static bool qsp_entry_no_thread_cmp(const void *ap
, const void *bp
)
204 const QSPEntry
*a
= ap
;
205 const QSPEntry
*b
= bp
;
207 return qsp_callsite_cmp(a
->callsite
, b
->callsite
);
210 static bool qsp_entry_no_thread_obj_cmp(const void *ap
, const void *bp
)
212 const QSPEntry
*a
= ap
;
213 const QSPEntry
*b
= bp
;
215 return qsp_callsite_no_obj_cmp(a
->callsite
, b
->callsite
);
218 static bool qsp_entry_cmp(const void *ap
, const void *bp
)
220 const QSPEntry
*a
= ap
;
221 const QSPEntry
*b
= bp
;
223 return a
->thread_ptr
== b
->thread_ptr
&&
224 qsp_callsite_cmp(a
->callsite
, b
->callsite
);
228 * Normally we'd call this from a constructor function, but we want it to work
229 * via libutil as well.
231 static void qsp_do_init(void)
233 /* make sure this file's path in the tree is up to date with QSP_REL_PATH */
234 g_assert(strstr(__FILE__
, QSP_REL_PATH
));
235 qsp_qemu_path_len
= strlen(__FILE__
) - strlen(QSP_REL_PATH
);
237 qht_init(&qsp_ht
, qsp_entry_cmp
, QSP_INITIAL_SIZE
,
238 QHT_MODE_AUTO_RESIZE
| QHT_MODE_RAW_MUTEXES
);
239 qht_init(&qsp_callsite_ht
, qsp_callsite_cmp
, QSP_INITIAL_SIZE
,
240 QHT_MODE_AUTO_RESIZE
| QHT_MODE_RAW_MUTEXES
);
243 static __attribute__((noinline
)) void qsp_init__slowpath(void)
245 if (atomic_cmpxchg(&qsp_initializing
, false, true) == false) {
247 atomic_set(&qsp_initialized
, true);
249 while (!atomic_read(&qsp_initialized
)) {
255 /* qsp_init() must be called from _all_ exported functions */
256 static inline void qsp_init(void)
258 if (likely(atomic_read(&qsp_initialized
))) {
261 qsp_init__slowpath();
264 static QSPCallSite
*qsp_callsite_find(const QSPCallSite
*orig
)
266 QSPCallSite
*callsite
;
269 hash
= qsp_callsite_hash(orig
);
270 callsite
= qht_lookup(&qsp_callsite_ht
, orig
, hash
);
271 if (callsite
== NULL
) {
272 void *existing
= NULL
;
274 callsite
= g_new(QSPCallSite
, 1);
275 memcpy(callsite
, orig
, sizeof(*callsite
));
276 qht_insert(&qsp_callsite_ht
, callsite
, hash
, &existing
);
277 if (unlikely(existing
)) {
286 qsp_entry_create(struct qht
*ht
, const QSPEntry
*entry
, uint32_t hash
)
289 void *existing
= NULL
;
291 e
= g_new0(QSPEntry
, 1);
292 e
->thread_ptr
= entry
->thread_ptr
;
293 e
->callsite
= qsp_callsite_find(entry
->callsite
);
295 qht_insert(ht
, e
, hash
, &existing
);
296 if (unlikely(existing
)) {
304 qsp_entry_find(struct qht
*ht
, const QSPEntry
*entry
, uint32_t hash
)
308 e
= qht_lookup(ht
, entry
, hash
);
310 e
= qsp_entry_create(ht
, entry
, hash
);
316 * Note: Entries are never removed, so callers do not have to be in an RCU
317 * read-side critical section.
319 static QSPEntry
*qsp_entry_get(const void *obj
, const char *file
, int line
,
322 QSPCallSite callsite
= {
333 orig
.thread_ptr
= &qsp_thread
;
334 orig
.callsite
= &callsite
;
336 hash
= qsp_entry_hash(&orig
);
337 return qsp_entry_find(&qsp_ht
, &orig
, hash
);
341 * @e is in the global hash table; it is only written to by the current thread,
342 * so we write to it atomically (as in "write once") to prevent torn reads.
344 static inline void do_qsp_entry_record(QSPEntry
*e
, int64_t delta
, bool acq
)
346 atomic_set_u64(&e
->ns
, e
->ns
+ delta
);
348 atomic_set_u64(&e
->n_acqs
, e
->n_acqs
+ 1);
352 static inline void qsp_entry_record(QSPEntry
*e
, int64_t delta
)
354 do_qsp_entry_record(e
, delta
, true);
357 #define QSP_GEN_VOID(type_, qsp_t_, func_, impl_) \
358 static void func_(type_ *obj, const char *file, int line) \
364 impl_(obj, file, line); \
367 e = qsp_entry_get(obj, file, line, qsp_t_); \
368 qsp_entry_record(e, t1 - t0); \
371 #define QSP_GEN_RET1(type_, qsp_t_, func_, impl_) \
372 static int func_(type_ *obj, const char *file, int line) \
379 err = impl_(obj, file, line); \
382 e = qsp_entry_get(obj, file, line, qsp_t_); \
383 do_qsp_entry_record(e, t1 - t0, !err); \
387 QSP_GEN_VOID(QemuMutex
, QSP_BQL_MUTEX
, qsp_bql_mutex_lock
, qemu_mutex_lock_impl
)
388 QSP_GEN_VOID(QemuMutex
, QSP_MUTEX
, qsp_mutex_lock
, qemu_mutex_lock_impl
)
389 QSP_GEN_RET1(QemuMutex
, QSP_MUTEX
, qsp_mutex_trylock
, qemu_mutex_trylock_impl
)
391 QSP_GEN_VOID(QemuRecMutex
, QSP_REC_MUTEX
, qsp_rec_mutex_lock
,
392 qemu_rec_mutex_lock_impl
)
393 QSP_GEN_RET1(QemuRecMutex
, QSP_REC_MUTEX
, qsp_rec_mutex_trylock
,
394 qemu_rec_mutex_trylock_impl
)
400 qsp_cond_wait(QemuCond
*cond
, QemuMutex
*mutex
, const char *file
, int line
)
406 qemu_cond_wait_impl(cond
, mutex
, file
, line
);
409 e
= qsp_entry_get(cond
, file
, line
, QSP_CONDVAR
);
410 qsp_entry_record(e
, t1
- t0
);
413 bool qsp_is_enabled(void)
415 return atomic_read(&qemu_mutex_lock_func
) == qsp_mutex_lock
;
418 void qsp_enable(void)
420 atomic_set(&qemu_mutex_lock_func
, qsp_mutex_lock
);
421 atomic_set(&qemu_mutex_trylock_func
, qsp_mutex_trylock
);
422 atomic_set(&qemu_bql_mutex_lock_func
, qsp_bql_mutex_lock
);
423 atomic_set(&qemu_rec_mutex_lock_func
, qsp_rec_mutex_lock
);
424 atomic_set(&qemu_rec_mutex_trylock_func
, qsp_rec_mutex_trylock
);
425 atomic_set(&qemu_cond_wait_func
, qsp_cond_wait
);
428 void qsp_disable(void)
430 atomic_set(&qemu_mutex_lock_func
, qemu_mutex_lock_impl
);
431 atomic_set(&qemu_mutex_trylock_func
, qemu_mutex_trylock_impl
);
432 atomic_set(&qemu_bql_mutex_lock_func
, qemu_mutex_lock_impl
);
433 atomic_set(&qemu_rec_mutex_lock_func
, qemu_rec_mutex_lock_impl
);
434 atomic_set(&qemu_rec_mutex_trylock_func
, qemu_rec_mutex_trylock_impl
);
435 atomic_set(&qemu_cond_wait_func
, qemu_cond_wait_impl
);
438 static gint
qsp_tree_cmp(gconstpointer ap
, gconstpointer bp
, gpointer up
)
440 const QSPEntry
*a
= ap
;
441 const QSPEntry
*b
= bp
;
442 enum QSPSortBy sort_by
= *(enum QSPSortBy
*)up
;
443 const QSPCallSite
*ca
;
444 const QSPCallSite
*cb
;
447 case QSP_SORT_BY_TOTAL_WAIT_TIME
:
450 } else if (a
->ns
< b
->ns
) {
454 case QSP_SORT_BY_AVG_WAIT_TIME
:
456 double avg_a
= a
->n_acqs
? a
->ns
/ a
->n_acqs
: 0;
457 double avg_b
= b
->n_acqs
? b
->ns
/ b
->n_acqs
: 0;
461 } else if (avg_a
< avg_b
) {
467 g_assert_not_reached();
472 /* Break the tie with the object's address */
473 if (ca
->obj
< cb
->obj
) {
475 } else if (ca
->obj
> cb
->obj
) {
480 /* same obj. Break the tie with the callsite's file */
481 cmp
= strcmp(ca
->file
, cb
->file
);
485 /* same callsite file. Break the tie with the callsite's line */
486 g_assert(ca
->line
!= cb
->line
);
487 if (ca
->line
< cb
->line
) {
489 } else if (ca
->line
> cb
->line
) {
492 /* break the tie with the callsite's type */
493 return cb
->type
- ca
->type
;
498 static void qsp_sort(void *p
, uint32_t h
, void *userp
)
503 g_tree_insert(tree
, e
, NULL
);
506 static void qsp_aggregate(void *p
, uint32_t h
, void *up
)
509 const QSPEntry
*e
= p
;
513 hash
= qsp_entry_no_thread_hash(e
);
514 agg
= qsp_entry_find(ht
, e
, hash
);
516 * The entry is in the global hash table; read from it atomically (as in
519 agg
->ns
+= atomic_read_u64(&e
->ns
);
520 agg
->n_acqs
+= atomic_read_u64(&e
->n_acqs
);
523 static void qsp_iter_diff(void *p
, uint32_t hash
, void *htp
)
525 struct qht
*ht
= htp
;
529 new = qht_lookup(ht
, old
, hash
);
530 /* entries are never deleted, so we must have this one */
531 g_assert(new != NULL
);
532 /* our reading of the stats happened after the snapshot was taken */
533 g_assert(new->n_acqs
>= old
->n_acqs
);
534 g_assert(new->ns
>= old
->ns
);
536 new->n_acqs
-= old
->n_acqs
;
539 /* No point in reporting an empty entry */
540 if (new->n_acqs
== 0 && new->ns
== 0) {
541 bool removed
= qht_remove(ht
, new, hash
);
548 static void qsp_diff(struct qht
*orig
, struct qht
*new)
550 qht_iter(orig
, qsp_iter_diff
, new);
553 static void qsp_iter_callsite_coalesce(void *p
, uint32_t h
, void *htp
)
555 struct qht
*ht
= htp
;
560 hash
= qsp_entry_no_thread_obj_hash(old
);
561 e
= qht_lookup(ht
, old
, hash
);
563 e
= qsp_entry_create(ht
, old
, hash
);
565 } else if (e
->callsite
->obj
!= old
->callsite
->obj
) {
569 e
->n_acqs
+= old
->n_acqs
;
572 static void qsp_ht_delete(void *p
, uint32_t h
, void *htp
)
577 static void qsp_mktree(GTree
*tree
, bool callsite_coalesce
)
580 struct qht ht
, coalesce_ht
;
584 * First, see if there's a prior snapshot, so that we read the global hash
585 * table _after_ the snapshot has been created, which guarantees that
586 * the entries we'll read will be a superset of the snapshot's entries.
588 * We must remain in an RCU read-side critical section until we're done
592 snap
= atomic_rcu_read(&qsp_snapshot
);
594 /* Aggregate all results from the global hash table into a local one */
595 qht_init(&ht
, qsp_entry_no_thread_cmp
, QSP_INITIAL_SIZE
,
596 QHT_MODE_AUTO_RESIZE
| QHT_MODE_RAW_MUTEXES
);
597 qht_iter(&qsp_ht
, qsp_aggregate
, &ht
);
599 /* compute the difference wrt the snapshot, if any */
601 qsp_diff(&snap
->ht
, &ht
);
603 /* done with the snapshot; RCU can reclaim it */
607 if (callsite_coalesce
) {
608 qht_init(&coalesce_ht
, qsp_entry_no_thread_obj_cmp
, QSP_INITIAL_SIZE
,
609 QHT_MODE_AUTO_RESIZE
| QHT_MODE_RAW_MUTEXES
);
610 qht_iter(&ht
, qsp_iter_callsite_coalesce
, &coalesce_ht
);
612 /* free the previous hash table, and point htp to coalesce_ht */
613 qht_iter(&ht
, qsp_ht_delete
, NULL
);
618 /* sort the hash table elements by using a tree */
619 qht_iter(htp
, qsp_sort
, tree
);
621 /* free the hash table, but keep the elements (those are in the tree now) */
625 /* free string with g_free */
626 static char *qsp_at(const QSPCallSite
*callsite
)
628 GString
*s
= g_string_new(NULL
);
629 const char *shortened
;
631 /* remove the absolute path to qemu */
632 if (unlikely(strlen(callsite
->file
) < qsp_qemu_path_len
)) {
633 shortened
= callsite
->file
;
635 shortened
= callsite
->file
+ qsp_qemu_path_len
;
637 g_string_append_printf(s
, "%s:%u", shortened
, callsite
->line
);
638 return g_string_free(s
, FALSE
);
641 struct QSPReportEntry
{
644 const char *typename
;
650 typedef struct QSPReportEntry QSPReportEntry
;
653 QSPReportEntry
*entries
;
655 size_t max_n_entries
;
657 typedef struct QSPReport QSPReport
;
659 static gboolean
qsp_tree_report(gpointer key
, gpointer value
, gpointer udata
)
661 const QSPEntry
*e
= key
;
662 QSPReport
*report
= udata
;
663 QSPReportEntry
*entry
;
665 if (report
->n_entries
== report
->max_n_entries
) {
668 entry
= &report
->entries
[report
->n_entries
];
671 entry
->obj
= e
->callsite
->obj
;
672 entry
->n_objs
= e
->n_objs
;
673 entry
->callsite_at
= qsp_at(e
->callsite
);
674 entry
->typename
= qsp_typenames
[e
->callsite
->type
];
675 entry
->time_s
= e
->ns
* 1e-9;
676 entry
->n_acqs
= e
->n_acqs
;
677 entry
->ns_avg
= e
->n_acqs
? e
->ns
/ e
->n_acqs
: 0;
682 pr_report(const QSPReport
*rep
, FILE *f
, fprintf_function pr
)
686 int callsite_len
= 0;
691 /* find out the maximum length of all 'callsite' fields */
692 for (i
= 0; i
< rep
->n_entries
; i
++) {
693 const QSPReportEntry
*e
= &rep
->entries
[i
];
694 size_t len
= strlen(e
->callsite_at
);
701 callsite_len
= MAX(max_len
, strlen("Call site"));
702 /* white space to leave to the right of "Call site" */
703 callsite_rspace
= callsite_len
- strlen("Call site");
705 pr(f
, "Type Object Call site%*s Wait Time (s) "
706 " Count Average (us)\n", callsite_rspace
, "");
708 /* build a horizontal rule with dashes */
709 n_dashes
= 79 + callsite_rspace
;
710 dashes
= g_malloc(n_dashes
+ 1);
711 memset(dashes
, '-', n_dashes
);
712 dashes
[n_dashes
] = '\0';
713 pr(f
, "%s\n", dashes
);
715 for (i
= 0; i
< rep
->n_entries
; i
++) {
716 const QSPReportEntry
*e
= &rep
->entries
[i
];
717 GString
*s
= g_string_new(NULL
);
719 g_string_append_printf(s
, "%-9s ", e
->typename
);
721 g_string_append_printf(s
, "[%12u]", e
->n_objs
);
723 g_string_append_printf(s
, "%14p", e
->obj
);
725 g_string_append_printf(s
, " %s%*s %13.5f %12" PRIu64
" %12.2f\n",
727 callsite_len
- (int)strlen(e
->callsite_at
), "",
728 e
->time_s
, e
->n_acqs
, e
->ns_avg
* 1e-3);
730 g_string_free(s
, TRUE
);
733 pr(f
, "%s\n", dashes
);
737 static void report_destroy(QSPReport
*rep
)
741 for (i
= 0; i
< rep
->n_entries
; i
++) {
742 QSPReportEntry
*e
= &rep
->entries
[i
];
744 g_free(e
->callsite_at
);
746 g_free(rep
->entries
);
749 void qsp_report(FILE *f
, fprintf_function cpu_fprintf
, size_t max
,
750 enum QSPSortBy sort_by
, bool callsite_coalesce
)
752 GTree
*tree
= g_tree_new_full(qsp_tree_cmp
, &sort_by
, g_free
, NULL
);
757 rep
.entries
= g_new0(QSPReportEntry
, max
);
759 rep
.max_n_entries
= max
;
761 qsp_mktree(tree
, callsite_coalesce
);
762 g_tree_foreach(tree
, qsp_tree_report
, &rep
);
763 g_tree_destroy(tree
);
765 pr_report(&rep
, f
, cpu_fprintf
);
766 report_destroy(&rep
);
769 static void qsp_snapshot_destroy(QSPSnapshot
*snap
)
771 qht_iter(&snap
->ht
, qsp_ht_delete
, NULL
);
772 qht_destroy(&snap
->ht
);
778 QSPSnapshot
*new = g_new(QSPSnapshot
, 1);
783 qht_init(&new->ht
, qsp_entry_cmp
, QSP_INITIAL_SIZE
,
784 QHT_MODE_AUTO_RESIZE
| QHT_MODE_RAW_MUTEXES
);
786 /* take a snapshot of the current state */
787 qht_iter(&qsp_ht
, qsp_aggregate
, &new->ht
);
789 /* replace the previous snapshot, if any */
790 old
= atomic_xchg(&qsp_snapshot
, new);
792 call_rcu(old
, qsp_snapshot_destroy
, rcu
);