Skip to contentSkip to conversation

3 cacheline io_kiocb

RFC In review Pavel Begunkov 2 participants 8 messages Jul 25, 2020

Subsystems (2)
lore
Share & tools
lore
#0 lore raw

cover 3 cacheline io_kiocb

That's not final for a several reasons, but good enough for discussion. That brings io_kiocb down to 192B. I didn't try to benchmark it properly, but quick nop test gave +5% throughput increase. 7531 vs 7910 KIOPS with fio/t/io_uring

The whole situation is obviously a bunch of tradeoffs. For instance, instead of shrinking it, we can inline apoll to speed apoll path.

[2/2] just for a reference, I'm thinking about other ways to shrink it. e.g. ->link_list can be a single-linked list with linked tiemouts storing a back-reference. This can turn out to be better, because that would move ->fixed_file_refs to the 2nd cacheline, so we won't ever touch 3rd cacheline in the submission path.
Any other ideas?

note: on top of for-5.9/io_uring,
f56040b819998 ("io_uring: deduplicate io_grab_files() calls")

Pavel Begunkov (2):
io_uring: allocate req->work dynamically
io_uring: unionise ->apoll and ->work

 fs/io-wq.h    |   1 +
 fs/io_uring.c | 207 ++++++++++++++++++++++++++------------------------
 2 files changed, 110 insertions(+), 98 deletions(-)
Signature · 1 line
2.24.0
#1 lore raw

PATCH 1/2 io_uring: allocate req->work dynamically

req->work takes a lot of space and is not needed in hot path, don't embed it into struct io_kiocb and allocate dynamically. The changes are pretty straightforward, the only noticible thing is a ->private field in io_wq_work growing it to 64B, and used to store a reference to the request.

That shrinks io_kiocb to 200 bytes

---
 fs/io-wq.h    |   1 +
 fs/io_uring.c | 203 +++++++++++++++++++++++++-------------------------
 2 files changed, 104 insertions(+), 100 deletions(-)
fs/io-wq.h +1 -0
index ddaf9614cf9b..1358e022ed4b 100644
@@ -85,6 +85,7 @@ static inline void wq_list_del(struct io_wq_work_list *list,
 
 struct io_wq_work {
 	struct io_wq_work_node list;
+	void *private;
 	struct files_struct *files;
 	struct mm_struct *mm;
 	const struct cred *creds;
fs/io_uring.c +103 -100

435 lines not on this page · raw message

Signature · 1 line
2.24.0
#2 lore raw

PATCH 2/2 io_uring: unionise ->apoll and ->work

In reply to Pavel Begunkov

Save a bit of space by placing ->apoll and ->work ptrs into a union, making io_kiocb to take 192B (3 cachelines)

note: this patch is just for reference, there are other probably better ways to save 8B.

---
 fs/io_uring.c | 14 +++++++++++---
 1 file changed, 11 insertions(+), 3 deletions(-)
fs/io_uring.c +11 -3
index ef4c6e50aa4f..6894a9a5db30 100644
@@ -597,6 +597,7 @@ enum {
 struct async_poll {
 	struct io_poll_iocb	poll;
 	struct io_poll_iocb	*double_poll;
+	struct io_wq_work	*work;
 };
 
 /*
@@ -658,8 +659,10 @@ struct io_kiocb {
 	 * async armed poll handlers for regular commands.
 	 */
 	struct hlist_node	hash_node;
-	struct async_poll	*apoll;
-	struct io_wq_work	*work;
+	union {
+		struct async_poll	*apoll;
+		struct io_wq_work	*work;
+	};
 	struct callback_head	task_work;
 };
 
@@ -4676,6 +4679,8 @@ static void io_async_task_func(struct callback_head *cb)
 	io_poll_remove_double(req, apoll->double_poll);
 	spin_unlock_irq(&ctx->completion_lock);
 
+	req->work = apoll->work;
+
 	if (!READ_ONCE(apoll->poll.canceled))
 		__io_req_task_submit(req);
 	else
@@ -4765,6 +4770,7 @@ static bool io_arm_poll_handler(struct io_kiocb *req)
 	if (unlikely(!apoll))
 		return false;
 	apoll->double_poll = NULL;
+	apoll->work = req->work;
 
 	req->flags |= REQ_F_POLLED;
 	io_get_req_task(req);
@@ -4785,6 +4791,7 @@ static bool io_arm_poll_handler(struct io_kiocb *req)
 	if (ret) {
 		io_poll_remove_double(req, apoll->double_poll);
 		spin_unlock_irq(&ctx->completion_lock);
+		req->work = apoll->work;
 		kfree(apoll->double_poll);
 		kfree(apoll);
 		return false;
@@ -4826,6 +4833,7 @@ static bool io_poll_remove_one(struct io_kiocb *req)
 		/* non-poll requests have submit ref still */
 		do_complete = __io_poll_remove_one(req, &apoll->poll);
 		if (do_complete) {
+			req->work = apoll->work;
 			io_put_req(req);
 			kfree(apoll->double_poll);
 			kfree(apoll);
@@ -4962,7 +4970,7 @@ static int io_poll_add(struct io_kiocb *req)
 
 	/* ->work is in union with hash_node and others */
 	io_req_clean_work(req);
-	req->flags &= ~REQ_F_WORK_INITIALIZED;
+	req->work = NULL;
 
 	INIT_HLIST_NODE(&req->hash_node);
 	ipt.pt._qproc = io_poll_queue_proc;
Signature · 1 line
2.24.0
maintainer
#3 lore raw
In reply to Pavel Begunkov
Quoted 8 lines · Pavel Begunkov

That's not final for a several reasons, but good enough for discussion. That brings io_kiocb down to 192B. I didn't try to benchmark it properly, but quick nop test gave +5% throughput increase. 7531 vs 7910 KIOPS with fio/t/io_uring

The whole situation is obviously a bunch of tradeoffs. For instance, instead of shrinking it, we can inline apoll to speed apoll path.

Quoted 6 lines

[2/2] just for a reference, I'm thinking about other ways to shrink it. e.g. ->link_list can be a single-linked list with linked tiemouts storing a back-reference. This can turn out to be better, because that would move ->fixed_file_refs to the 2nd cacheline, so we won't ever touch 3rd cacheline in the submission path.
Any other ideas?

Nothing noticeable for me, still about the same performance. But generally speaking, I don't necessarily think we need to go all in on making this as tiny as possible. It's much more important to chase the items where we only use 2 cachelines for the hot path, and then we have the extra space in there already for the semi hot paths like poll driven retry. Yes, we're still allocating from a pool that has slightly larger objects, but that doesn't really matter _that_ much. Avoiding an extra kmalloc+kfree for the semi hot paths are a bigger deal than making io_kiocb smaller and smaller.

That said, for no-brainer changes, we absolutely should make it smaller. I just don't want to jump through convoluted hoops to get there.

Signature · 1 line
Jens Axboe
#4 lore raw
Quoted 26 lines · Jens Axboe
Quoted 8 lines · Pavel Begunkov

That's not final for a several reasons, but good enough for discussion. That brings io_kiocb down to 192B. I didn't try to benchmark it properly, but quick nop test gave +5% throughput increase. 7531 vs 7910 KIOPS with fio/t/io_uring

The whole situation is obviously a bunch of tradeoffs. For instance, instead of shrinking it, we can inline apoll to speed apoll path.

Quoted 6 lines

[2/2] just for a reference, I'm thinking about other ways to shrink it. e.g. ->link_list can be a single-linked list with linked tiemouts storing a back-reference. This can turn out to be better, because that would move ->fixed_file_refs to the 2nd cacheline, so we won't ever touch 3rd cacheline in the submission path.
Any other ideas?

Nothing noticeable for me, still about the same performance. But generally speaking, I don't necessarily think we need to go all in on making this as tiny as possible. It's much more important to chase the items where we only use 2 cachelines for the hot path, and then we have the extra space in there already for the semi hot paths like poll driven retry. Yes, we're still allocating from a pool that has slightly larger objects, but that doesn't really matter _that_ much. Avoiding an extra kmalloc+kfree for the semi hot paths are a bigger deal than making io_kiocb smaller and smaller.

Quoted 2 lines

That said, for no-brainer changes, we absolutely should make it smaller. I just don't want to jump through convoluted hoops to get there.

Agree, but that's not the end goal. The first point is to kill the union, but it already has enough space for that.

The second is to see, whether we can use the space in a better way. From the high level perspective ->apoll and ->work are alike and both serve to provide asynchronous paths, hence the idea to swap them naturally comes to mind. TBH, I don't think it'd do much, because init of ->io would probably hide any benefit.

Signature · 1 line
Pavel Begunkov
maintainer
#5 lore raw
Quoted 27 lines · Pavel Begunkov
Quoted 26 lines · Jens Axboe
Quoted 8 lines · Pavel Begunkov

That's not final for a several reasons, but good enough for discussion. That brings io_kiocb down to 192B. I didn't try to benchmark it properly, but quick nop test gave +5% throughput increase. 7531 vs 7910 KIOPS with fio/t/io_uring

The whole situation is obviously a bunch of tradeoffs. For instance, instead of shrinking it, we can inline apoll to speed apoll path.

Quoted 6 lines

[2/2] just for a reference, I'm thinking about other ways to shrink it. e.g. ->link_list can be a single-linked list with linked tiemouts storing a back-reference. This can turn out to be better, because that would move ->fixed_file_refs to the 2nd cacheline, so we won't ever touch 3rd cacheline in the submission path.
Any other ideas?

Nothing noticeable for me, still about the same performance. But generally speaking, I don't necessarily think we need to go all in on making this as tiny as possible. It's much more important to chase the items where we only use 2 cachelines for the hot path, and then we have the extra space in there already for the semi hot paths like poll driven retry. Yes, we're still allocating from a pool that has slightly larger objects, but that doesn't really matter _that_ much. Avoiding an extra kmalloc+kfree for the semi hot paths are a bigger deal than making io_kiocb smaller and smaller.

Quoted 5 lines
Quoted 2 lines

That said, for no-brainer changes, we absolutely should make it smaller. I just don't want to jump through convoluted hoops to get there.

Agree, but that's not the end goal. The first point is to kill the union, but it already has enough space for that.

Right

Quoted 4 lines

The second is to see, whether we can use the space in a better way. From the high level perspective ->apoll and ->work are alike and both serve to provide asynchronous paths, hence the idea to swap them naturally comes to mind.

Totally agree, which is why the union of those kind of makes sense. We're definitely NOT using them at the same time, but the fact that we had various mm/creds/whatnot in the work_struct made that a bit iffy.

Quoted 2 lines

TBH, I don't think it'd do much, because init of ->io would probably hide any benefit.

There should be no ->io init/alloc for this test case.

Signature · 1 line
Jens Axboe
#6 lore raw
Quoted 36 lines · Jens Axboe
Quoted 27 lines · Pavel Begunkov
Quoted 26 lines · Jens Axboe
Quoted 8 lines · Pavel Begunkov

That's not final for a several reasons, but good enough for discussion. That brings io_kiocb down to 192B. I didn't try to benchmark it properly, but quick nop test gave +5% throughput increase. 7531 vs 7910 KIOPS with fio/t/io_uring

The whole situation is obviously a bunch of tradeoffs. For instance, instead of shrinking it, we can inline apoll to speed apoll path.

Quoted 6 lines

[2/2] just for a reference, I'm thinking about other ways to shrink it. e.g. ->link_list can be a single-linked list with linked tiemouts storing a back-reference. This can turn out to be better, because that would move ->fixed_file_refs to the 2nd cacheline, so we won't ever touch 3rd cacheline in the submission path.
Any other ideas?

Nothing noticeable for me, still about the same performance. But generally speaking, I don't necessarily think we need to go all in on making this as tiny as possible. It's much more important to chase the items where we only use 2 cachelines for the hot path, and then we have the extra space in there already for the semi hot paths like poll driven retry. Yes, we're still allocating from a pool that has slightly larger objects, but that doesn't really matter _that_ much. Avoiding an extra kmalloc+kfree for the semi hot paths are a bigger deal than making io_kiocb smaller and smaller.

Quoted 5 lines
Quoted 2 lines

That said, for no-brainer changes, we absolutely should make it smaller. I just don't want to jump through convoluted hoops to get there.

Agree, but that's not the end goal. The first point is to kill the union, but it already has enough space for that.

Right

Quoted 8 lines
Quoted 4 lines

The second is to see, whether we can use the space in a better way. From the high level perspective ->apoll and ->work are alike and both serve to provide asynchronous paths, hence the idea to swap them naturally comes to mind.

Totally agree, which is why the union of those kind of makes sense. We're definitely NOT using them at the same time, but the fact that we had various mm/creds/whatnot in the work_struct made that a bit iffy.

Thinking of it, if combined with work de-init as you proposed before, it's probably possible to make a layout similar to the one below

struct io_kiocb {
	...
	struct hlist_node	hash_node;
	struct callback_head	task_work;	
	union {
		struct io_wq_work	work;
		struct async_poll	apoll;
	};
};

Saves ->apoll kmalloc(), and the actual work de-init would be negligibly rare. Worth to try

Quoted 5 lines
Quoted 3 lines

TBH, I don't think it'd do much, because init of ->io would probably hide any benefit.

There should be no ->io init/alloc for this test case.

I mean, before getting into io_arm_poll_handler(), it should get -EAGAIN in io_{read,write}() and initialise ->io in io_setup_async_rw(), at least for READV, WRITEV.

Signature · 1 line
Pavel Begunkov
maintainer
#7 lore raw
Quoted 59 lines · Pavel Begunkov
Quoted 36 lines · Jens Axboe
Quoted 27 lines · Pavel Begunkov
Quoted 26 lines · Jens Axboe
Quoted 8 lines · Pavel Begunkov

That's not final for a several reasons, but good enough for discussion. That brings io_kiocb down to 192B. I didn't try to benchmark it properly, but quick nop test gave +5% throughput increase. 7531 vs 7910 KIOPS with fio/t/io_uring

The whole situation is obviously a bunch of tradeoffs. For instance, instead of shrinking it, we can inline apoll to speed apoll path.

Quoted 6 lines

[2/2] just for a reference, I'm thinking about other ways to shrink it. e.g. ->link_list can be a single-linked list with linked tiemouts storing a back-reference. This can turn out to be better, because that would move ->fixed_file_refs to the 2nd cacheline, so we won't ever touch 3rd cacheline in the submission path.
Any other ideas?

Nothing noticeable for me, still about the same performance. But generally speaking, I don't necessarily think we need to go all in on making this as tiny as possible. It's much more important to chase the items where we only use 2 cachelines for the hot path, and then we have the extra space in there already for the semi hot paths like poll driven retry. Yes, we're still allocating from a pool that has slightly larger objects, but that doesn't really matter _that_ much. Avoiding an extra kmalloc+kfree for the semi hot paths are a bigger deal than making io_kiocb smaller and smaller.

Quoted 5 lines
Quoted 2 lines

That said, for no-brainer changes, we absolutely should make it smaller. I just don't want to jump through convoluted hoops to get there.

Agree, but that's not the end goal. The first point is to kill the union, but it already has enough space for that.

Right

Quoted 8 lines
Quoted 4 lines

The second is to see, whether we can use the space in a better way. From the high level perspective ->apoll and ->work are alike and both serve to provide asynchronous paths, hence the idea to swap them naturally comes to mind.

Totally agree, which is why the union of those kind of makes sense. We're definitely NOT using them at the same time, but the fact that we had various mm/creds/whatnot in the work_struct made that a bit iffy.

Thinking of it, if combined with work de-init as you proposed before, it's probably possible to make a layout similar to the one below

struct io_kiocb {
	...
	struct hlist_node	hash_node;
	struct callback_head	task_work;	
	union {
		struct io_wq_work	work;
		struct async_poll	apoll;
	};
};
Quoted 2 lines

Saves ->apoll kmalloc(), and the actual work de-init would be negligibly rare. Worth to try

And hopefully get rid of the stupid apoll->work and the copy back and forth... But yes, this would be most excellent, and an ideal layout.

Quoted 8 lines
Quoted 4 lines
Quoted 2 lines

TBH, I don't think it'd do much, because init of ->io would probably hide any benefit.

There should be no ->io init/alloc for this test case.

I mean, before getting into io_arm_poll_handler(), it should get -EAGAIN in io_{read,write}() and initialise ->io in io_setup_async_rw(), at least for READV, WRITEV.

Sure, but for my testing, there's never an EAGAIN, so I won't be exercising that path for the peak testing.

Signature · 1 line
Jens Axboe