Skip to contentSkip to conversation

qemu-coroutine: use a ring per thread for the pool

RFC PATCH In review Peter Lieven 3 participants 27 messages Nov 27, 2014 – Nov 28, 2014 1 message not in the archive Block layer

lore
Share & tools
lore
#0 lore raw

cover qemu-coroutine: use a ring per thread for the pool

This series removes the need for locking the coroutine pool. During collection of perf data it showed up that there is serious time spend in locking the coroutine pool during heavy I/O from the vServer.

This also needs serious benchmarking. Using Kevins qemu-img bench there is a 3-4% speedup. Inside vServers it seems to heavily dependend on the individual CPU and SMP configuration.

I just wanted to post these patches here to discuss if this is an idea that we should look futher into.

KNOWN ISSUES:
- the I/O thread qobject is not destroyed on qemu termination. These seems to be the case for all qobjects?! Therefore the per thread coroutine pool is not cleaned up.
- without ioeventfd there is effectively no longer a coroutine pool since the coroutines are created in a different thread then they are freed.

Peter Lieven (3):
Revert "coroutine: make pool size dynamic" block/block-backend.c: remove coroutine pool reservation
qemu-coroutine: use a ring per thread for the pool

 block/block-backend.c     |    6 ----
 include/block/coroutine.h |   13 +-------
 iothread.c                |    3 ++
 qemu-coroutine.c          |   78 +++++++++++++++------------------------------
 4 files changed, 30 insertions(+), 70 deletions(-)
Signature · 1 line
1.7.9.5
#1 lore raw

RFC PATCH 1/3 Revert "coroutine: make pool size dynamic"

This reverts commit ac2662a913ee5854b1269256adbdc14e57ba480a.

---
 include/block/coroutine.h |   11 -----------
 qemu-coroutine.c          |   26 +++-----------------------
 2 files changed, 3 insertions(+), 34 deletions(-)
include/block/coroutine.h +0 -11
index 793df0e..1d9343d 100644
@@ -215,15 +215,4 @@ void coroutine_fn co_aio_sleep_ns(AioContext *ctx, QEMUClockType type,
  * Note that this function clobbers the handlers for the file descriptor.
  */
 void coroutine_fn yield_until_fd_readable(int fd);
-
-/**
- * Add or subtract from the coroutine pool size
- *
- * The coroutine implementation keeps a pool of coroutines to be reused by
- * qemu_coroutine_create().  This makes coroutine creation cheap.  Heavy
- * coroutine users should call this to reserve pool space.  Call it again with
- * a negative number to release pool space.
- */
-void qemu_coroutine_adjust_pool_size(int n);
-
 #endif /* QEMU_COROUTINE_H */
qemu-coroutine.c +3 -23
index bd574aa..4708521 100644
@@ -19,14 +19,14 @@
 #include "block/coroutine_int.h"
 
 enum {
-    POOL_DEFAULT_SIZE = 64,
+    /* Maximum free pool size prevents holding too many freed coroutines */
+    POOL_MAX_SIZE = 64,
 };
 
 /** Free list to speed up creation */
 static QemuMutex pool_lock;
 static QSLIST_HEAD(, Coroutine) pool = QSLIST_HEAD_INITIALIZER(pool);
 static unsigned int pool_size;
-static unsigned int pool_max_size = POOL_DEFAULT_SIZE;
 
 Coroutine *qemu_coroutine_create(CoroutineEntry *entry)
 {
@@ -55,7 +55,7 @@ static void coroutine_delete(Coroutine *co)
 {
     if (CONFIG_COROUTINE_POOL) {
         qemu_mutex_lock(&pool_lock);
-        if (pool_size < pool_max_size) {
+        if (pool_size < POOL_MAX_SIZE) {
             QSLIST_INSERT_HEAD(&pool, co, pool_next);
             co->caller = NULL;
             pool_size++;
@@ -137,23 +137,3 @@ void coroutine_fn qemu_coroutine_yield(void)
     self->caller = NULL;
     coroutine_swap(self, to);
 }
-
-void qemu_coroutine_adjust_pool_size(int n)
-{
-    qemu_mutex_lock(&pool_lock);
-
-    pool_max_size += n;
-
-    /* Callers should never take away more than they added */
-    assert(pool_max_size >= POOL_DEFAULT_SIZE);
-
-    /* Trim oversized pool down to new max */
-    while (pool_size > pool_max_size) {
-        Coroutine *co = QSLIST_FIRST(&pool);
-        QSLIST_REMOVE_HEAD(&pool, pool_next);
-        pool_size--;
-        qemu_coroutine_delete(co);
-    }
-
-    qemu_mutex_unlock(&pool_lock);
-}
Signature · 1 line
1.7.9.5
#2 lore raw

Re: [Qemu-devel] [RFC PATCH 1/3] Revert "coroutine: make pool size dynamic"

Quoted 1 line · Peter Lieven

This reverts commit ac2662a913ee5854b1269256adbdc14e57ba480a.

Justification?

Won't we hit the same problem as the global pool: either pool size is too large or too small. If you exceed the pool size performance drops dramatically.

Stefan

#3 lore raw

Re: [Qemu-devel] [RFC PATCH 1/3] Revert "coroutine: make pool size dynamic"

Am 28.11.2014 um 13:42 schrieb Stefan Hajnoczi:

Quoted 7 lines · Peter Lieven
Quoted 1 line

This reverts commit ac2662a913ee5854b1269256adbdc14e57ba480a.

Justification?

Won't we hit the same problem as the global pool: either pool size is too large or too small. If you exceed the pool size performance drops dramatically.

Each thread has its own pool. But you can ignore this series I think. The idea Paolo came up with seems to be better. With
my last improvement it seems to have equal performance without the drawback of different threads creating and destroying the coroutines.

Peter

#4 lore raw

RFC PATCH 2/3 block/block-backend.c: remove coroutine pool reservation

In reply to Peter Lieven
---
 block/block-backend.c |    6 ------
 1 file changed, 6 deletions(-)
block/block-backend.c +0 -6
index d0692b1..cf36ffb 100644
@@ -15,9 +15,6 @@
 #include "sysemu/blockdev.h"
 #include "qapi-event.h"
 
-/* Number of coroutines to reserve per attached device model */
-#define COROUTINE_POOL_RESERVATION 64
-
 struct BlockBackend {
     char *name;
     int refcnt;
@@ -261,8 +258,6 @@ int blk_attach_dev(BlockBackend *blk, void *dev)
     blk->dev = dev;
     bdrv_iostatus_reset(blk->bs);
 
-    /* We're expecting I/O from the device so bump up coroutine pool size */
-    qemu_coroutine_adjust_pool_size(COROUTINE_POOL_RESERVATION);
     return 0;
 }
 
@@ -290,7 +285,6 @@ void blk_detach_dev(BlockBackend *blk, void *dev)
     blk->dev_ops = NULL;
     blk->dev_opaque = NULL;
     bdrv_set_guest_block_size(blk->bs, 512);
-    qemu_coroutine_adjust_pool_size(-COROUTINE_POOL_RESERVATION);
     blk_unref(blk);
 }
 
Signature · 1 line
1.7.9.5
#5 lore raw

RFC PATCH 3/3 qemu-coroutine: use a ring per thread for the pool

In reply to Peter Lieven

This patch creates a ring structure for the coroutine pool instead of a linked list. The implementation of the list has the issue that it always throws aways the latest coroutines instead of the oldest ones. This is a drawback since the latest used coroutines are more likely cached than old ones.

Furthermore this patch creates a coroutine pool per thread to remove the need for locking.

---
 include/block/coroutine.h |    2 +-
 iothread.c                |    3 +++
 qemu-coroutine.c          |   54 ++++++++++++++++++++-------------------------
 3 files changed, 28 insertions(+), 31 deletions(-)
include/block/coroutine.h +1 -1
index 1d9343d..f0b3a2d 100644
@@ -96,7 +96,7 @@ Coroutine *coroutine_fn qemu_coroutine_self(void);
  */
 bool qemu_in_coroutine(void);
 
-
+void coroutine_pool_cleanup(void);
 
 /**
  * CoQueues are a mechanism to queue coroutines in order to continue executing
iothread.c +3 -0
index 342a23f..b53529b 100644
@@ -15,6 +15,7 @@
 #include "qom/object_interfaces.h"
 #include "qemu/module.h"
 #include "block/aio.h"
+#include "block/coroutine.h"
 #include "sysemu/iothread.h"
 #include "qmp-commands.h"
 #include "qemu/error-report.h"
@@ -47,6 +48,8 @@ static void *iothread_run(void *opaque)
         }
         aio_context_release(iothread->ctx);
     }
+
+    coroutine_pool_cleanup();
     return NULL;
 }
 
qemu-coroutine.c +24 -30

82 lines not on this page · raw message

Signature · 1 line
1.7.9.5
#6 lore raw

qemu-coroutine.c enum { · line 31

Quoted 6 lines · Peter Lieven
qemu-coroutine.c +5 -0
+static __thread struct CoRoutinePool {
+    Coroutine *ptrs[POOL_MAX_SIZE];
+    unsigned int size;
+    unsigned int nextfree;
+} CoPool;

The per-thread ring unfortunately didn't work well last time it was tested. Devices that do not use ioeventfd (not just the slow ones, even decently performing ones like ahci, nvme or megasas) will create the coroutine in the VCPU thread, and destroy it in the iothread. The result is that coroutines cannot be reused.

Can you check if this is still the case?

Paolo

#7 lore raw

Am 27.11.2014 um 17:40 schrieb Paolo Bonzini:

Quoted 14 lines · Peter Lieven

qemu-coroutine.c enum { · line 31

Quoted 6 lines
qemu-coroutine.c +5 -0
+static __thread struct CoRoutinePool {
+    Coroutine *ptrs[POOL_MAX_SIZE];
+    unsigned int size;
+    unsigned int nextfree;
+} CoPool;

The per-thread ring unfortunately didn't work well last time it was tested. Devices that do not use ioeventfd (not just the slow ones, even decently performing ones like ahci, nvme or megasas) will create the coroutine in the VCPU thread, and destroy it in the iothread. The result is that coroutines cannot be reused.

Quoted 1 line

Can you check if this is still the case?

I already tested at least for IDE and for ioeventfd=off. The coroutine is created in the vCPU thread and destroyed in the I/O thread.

I also havea more complicated version which sets per therad coroutine pool only for dataplane. Avoiding the lock for dedicated iothreads.

For those who want to take a look:

https://github.com/plieven/qemu/commit/325bc4ef5c7039337fa785744b145e2bdbb7b62e

Peter

A message that is not in the archive (sent off-list or not yet received): [email protected]

#9 lore raw

Am 28.11.2014 um 11:28 schrieb Paolo Bonzini:

Quoted 20 lines · Peter Lieven
Quoted 18 lines

Am 27.11.2014 um 17:40 schrieb Paolo Bonzini:

Quoted 13 lines · Peter Lieven

qemu-coroutine.c enum { · line 31

Quoted 6 lines
qemu-coroutine.c +5 -0
+static __thread struct CoRoutinePool {
+    Coroutine *ptrs[POOL_MAX_SIZE];
+    unsigned int size;
+    unsigned int nextfree;
+} CoPool;

The per-thread ring unfortunately didn't work well last time it was tested. Devices that do not use ioeventfd (not just the slow ones, even decently performing ones like ahci, nvme or megasas) will create the coroutine in the VCPU thread, and destroy it in the iothread. The result is that coroutines cannot be reused.

Quoted 1 line

Can you check if this is still the case?

I already tested at least for IDE and for ioeventfd=off. The coroutine is created in the vCPU thread and destroyed in the I/O thread.

Quoted 8 lines
Quoted 6 lines

I also havea more complicated version which sets per therad coroutine pool only for dataplane. Avoiding the lock for dedicated iothreads.

For those who want to take a look:

https://github.com/plieven/qemu/commit/325bc4ef5c7039337fa785744b145e2bdbb7b62e

Can you test it against the patch I just sent in Kevin's linux-aio coroutine thread?

Was already doing it ;-) At least with test-couroutine.c....

master:
Run operation 40000000 iterations 12.851414 s, 3112K operations/s, 321ns per coroutine

paolo:
Run operation 40000000 iterations 11.951720 s, 3346K operations/s, 298ns per coroutine

plieven/perf_master2:
Run operation 40000000 iterations 9.013785 s, 4437K operations/s, 225ns per coroutine

plieven/perf_master:
Run operation 40000000 iterations 11.072883 s, 3612K operations/s, 276ns per coroutine

However, perf_master and perf_master2 have a regerssion regarding nesting as it seems. @Kevin: Could that be the reason why they performe bad in some szenarios?

Regarding the bypass that is discussed. If it is not just a benchmark thing but really necessary
for some peoples use cases why not add a new aio mode like "bypass" and use it only then.
If the performance is really needed the user he/she might trade it in for lost features like iothrottling, filters etc.

Peter

#10 lore raw
Quoted 6 lines

master:
Run operation 40000000 iterations 12.851414 s, 3112K operations/s, 321ns per coroutine

paolo:
Run operation 40000000 iterations 11.951720 s, 3346K operations/s, 298ns per coroutine

Nice. :)

Can you please try "coroutine: Use __thread … " together, too? I still see 11% time spent in pthread_getspecific, and I get ~10% more indeed if I apply it here (my times are 191/160/145).

Paolo

#11 lore raw

Am 28.11.2014 um 12:14 schrieb Paolo Bonzini:

Quoted 10 lines
Quoted 5 lines

master:
Run operation 40000000 iterations 12.851414 s, 3112K operations/s, 321ns per coroutine

paolo:
Run operation 40000000 iterations 11.951720 s, 3346K operations/s, 298ns per coroutine

Nice. :)

Can you please try "coroutine: Use __thread … " together, too? I still see 11% time spent in pthread_getspecific, and I get ~10% more indeed if I apply it here (my times are 191/160/145).

indeed:

Run operation 40000000 iterations 10.138684 s, 3945K operations/s, 253ns per coroutine

#12 lore raw
Quoted 8 lines · Peter Lieven

Am 28.11.2014 um 12:14 schrieb Paolo Bonzini:

Quoted 7 lines
Quoted 5 lines

master:
Run operation 40000000 iterations 12.851414 s, 3112K operations/s, 321ns per coroutine

paolo:
Run operation 40000000 iterations 11.951720 s, 3346K operations/s, 298ns per coroutine

Nice. :)

Quoted 7 lines
Quoted 3 lines

Can you please try "coroutine: Use __thread … " together, too? I still see 11% time spent in pthread_getspecific, and I get ~10% more indeed if I apply it here (my times are 191/160/145).

indeed:

Run operation 40000000 iterations 10.138684 s, 3945K operations/s, 253ns per coroutine

Your perf_master2 uses the ring buffer unconditionally, right? I wonder if we can use a similar algorithm but with arrays instead of lists...

Paolo

#13 lore raw

Am 28.11.2014 um 12:23 schrieb Paolo Bonzini:

Quoted 10 lines · Peter Lieven
Quoted 8 lines

Am 28.11.2014 um 12:14 schrieb Paolo Bonzini:

Quoted 7 lines
Quoted 5 lines

master:
Run operation 40000000 iterations 12.851414 s, 3112K operations/s, 321ns per coroutine

paolo:
Run operation 40000000 iterations 11.951720 s, 3346K operations/s, 298ns per coroutine

Nice. :)

Quoted 8 lines
Quoted 6 lines
Quoted 3 lines

Can you please try "coroutine: Use __thread … " together, too? I still see 11% time spent in pthread_getspecific, and I get ~10% more indeed if I apply it here (my times are 191/160/145).

indeed:

Run operation 40000000 iterations 10.138684 s, 3945K operations/s, 253ns per coroutine

Your perf_master2 uses the ring buffer unconditionally, right? I wonder if we can use a similar algorithm but with arrays instead of lists...

You mean an algorithm similar to perf_master2 or to the current implementation?

The ring buffer seems to have a drawback when it comes to excessive coroutine nesting. My idea was that you do not throw away hot coroutines when the pool is full. However, i do not know if this is really a problem since the pool is only full when there is not much I/O. Or is this assumption to easy?

Peter

#14 lore raw
In reply to Paolo Bonzini

Am 28.11.2014 um 12:23 schrieb Paolo Bonzini:

Quoted 10 lines · Peter Lieven
Quoted 8 lines

Am 28.11.2014 um 12:14 schrieb Paolo Bonzini:

Quoted 7 lines
Quoted 5 lines

master:
Run operation 40000000 iterations 12.851414 s, 3112K operations/s, 321ns per coroutine

paolo:
Run operation 40000000 iterations 11.951720 s, 3346K operations/s, 298ns per coroutine

Nice. :)

Quoted 8 lines
Quoted 6 lines
Quoted 3 lines

Can you please try "coroutine: Use __thread … " together, too? I still see 11% time spent in pthread_getspecific, and I get ~10% more indeed if I apply it here (my times are 191/160/145).

indeed:

Run operation 40000000 iterations 10.138684 s, 3945K operations/s, 253ns per coroutine

Your perf_master2 uses the ring buffer unconditionally, right? I wonder if we can use a similar algorithm but with arrays instead of lists...

Why do you set pool_size = 0 in the create path?

When I do the following:

qemu-coroutine.c +1 -1
index 6bee354..c79ee78 100644
@@ -44,7 +44,7 @@ Coroutine *qemu_coroutine_create(CoroutineEntry *entry)
                  * and the actual size of alloc_pool.  But it is just a heuristic,
                  * it does not need to be perfect.
                  */
-                pool_size = 0;
+                atomic_dec(&pool_size);
                 QSLIST_MOVE_ATOMIC(&alloc_pool, &release_pool);
                 co = QSLIST_FIRST(&alloc_pool);
 

I get:
Run operation 40000000 iterations 9.883958 s, 4046K operations/s, 247ns per coroutine

#15 lore raw

Am 28.11.2014 um 12:32 schrieb Peter Lieven:

Quoted 35 lines

Am 28.11.2014 um 12:23 schrieb Paolo Bonzini:

Quoted 9 lines · Peter Lieven
Quoted 8 lines

Am 28.11.2014 um 12:14 schrieb Paolo Bonzini:

Quoted 7 lines
Quoted 5 lines

master:
Run operation 40000000 iterations 12.851414 s, 3112K operations/s, 321ns per coroutine

paolo:
Run operation 40000000 iterations 11.951720 s, 3346K operations/s, 298ns per coroutine

Nice. :)

Quoted 8 lines
Quoted 6 lines
Quoted 3 lines

Can you please try "coroutine: Use __thread … " together, too? I still see 11% time spent in pthread_getspecific, and I get ~10% more indeed if I apply it here (my times are 191/160/145).

indeed:

Run operation 40000000 iterations 10.138684 s, 3945K operations/s, 253ns per coroutine

Your perf_master2 uses the ring buffer unconditionally, right? I wonder if we can use a similar algorithm but with arrays instead of lists...

Why do you set pool_size = 0 in the create path?

When I do the following:

qemu-coroutine.c +1 -1
index 6bee354..c79ee78 100644
@@ -44,7 +44,7 @@ Coroutine *qemu_coroutine_create(CoroutineEntry *entry)
                  * and the actual size of alloc_pool.  But it is just a heuristic,
                  * it does not need to be perfect.
                  */
-                pool_size = 0;
+                atomic_dec(&pool_size);
                 QSLIST_MOVE_ATOMIC(&alloc_pool, &release_pool);
                 co = QSLIST_FIRST(&alloc_pool);
 
Quoted 3 lines

I get:
Run operation 40000000 iterations 9.883958 s, 4046K operations/s, 247ns per coroutine

Ok, understood, it "steals" the whole pool, right? Isn't that bad if we have more than one thread in need of a lot of coroutines?

Peter

#16 lore raw
Quoted 5 lines · Peter Lieven
Quoted 2 lines

I get:
Run operation 40000000 iterations 9.883958 s, 4046K operations/s, 247ns per coroutine

Ok, understood, it "steals" the whole pool, right? Isn't that bad if we have more than one thread in need of a lot of coroutines?

Overall the algorithm is expected to adapt. The N threads contribute to the global release pool, so the pool will fill up N times faster than if you had only one thread. There can be some variance, which is why the maximum size of the pool is twice the threshold (and probably could be tuned better).

Benchmarks are needed on real I/O too, of course, especially with high queue depth.

Paolo

#17 lore raw

Am 28.11.2014 um 13:26 schrieb Paolo Bonzini:

Quoted 6 lines · Peter Lieven
Quoted 4 lines
Quoted 2 lines

I get:
Run operation 40000000 iterations 9.883958 s, 4046K operations/s, 247ns per coroutine

Ok, understood, it "steals" the whole pool, right? Isn't that bad if we have more than one thread in need of a lot of coroutines?

Quoted 8 lines

Overall the algorithm is expected to adapt. The N threads contribute to the global release pool, so the pool will fill up N times faster than if you had only one thread. There can be some variance, which is why the maximum size of the pool is twice the threshold (and probably could be tuned better).

Benchmarks are needed on real I/O too, of course, especially with high queue depth.

Yes, cool. The atomic operations are a bit tricky at the first glance ;-)

Question:
Why is the pool_size increment atomic and the set to zero not?

Idea:
If the release_pool is full why not put the coroutine in the thread alloc_pool instead of throwing it away? :-)

Run operation 40000000 iterations 9.057805 s, 4416K operations/s, 226ns per coroutine

qemu-coroutine.c +14 -6
index 6bee354..edea162 100644
@@ -25,8 +25,9 @@ enum {
 
 /** Free list to speed up creation */
 static QSLIST_HEAD(, Coroutine) release_pool = QSLIST_HEAD_INITIALIZER(pool);
-static unsigned int pool_size;
+static unsigned int release_pool_size;
 static __thread QSLIST_HEAD(, Coroutine) alloc_pool = QSLIST_HEAD_INITIALIZER(pool);
+static __thread unsigned int alloc_pool_size;
 
 /* The GPrivate is only used to invoke coroutine_pool_cleanup.  */
 static void coroutine_pool_cleanup(void *value);
@@ -39,12 +40,12 @@ Coroutine *qemu_coroutine_create(CoroutineEntry *entry)
     if (CONFIG_COROUTINE_POOL) {
         co = QSLIST_FIRST(&alloc_pool);
         if (!co) {
-            if (pool_size > POOL_BATCH_SIZE) {
-                /* This is not exact; there could be a little skew between pool_size
+            if (release_pool_size > POOL_BATCH_SIZE) {
+                /* This is not exact; there could be a little skew between release_pool_size
                  * and the actual size of alloc_pool.  But it is just a heuristic,
                  * it does not need to be perfect.
                  */
-                pool_size = 0;
+                alloc_pool_size = atomic_fetch_and(&release_pool_size, 0);
                 QSLIST_MOVE_ATOMIC(&alloc_pool, &release_pool);
                 co = QSLIST_FIRST(&alloc_pool);
 
@@ -53,6 +54,8 @@ Coroutine *qemu_coroutine_create(CoroutineEntry *entry)
                  */
                 g_private_set(&dummy_key, &dummy_key);
             }
+        } else {
+            alloc_pool_size--;
         }
         if (co) {
             QSLIST_REMOVE_HEAD(&alloc_pool, pool_next);
@@ -71,10 +74,15 @@ Coroutine *qemu_coroutine_create(CoroutineEntry *entry)
 static void coroutine_delete(Coroutine *co)
 {
     if (CONFIG_COROUTINE_POOL) {
-        if (pool_size < POOL_BATCH_SIZE * 2) {
+        if (release_pool_size < POOL_BATCH_SIZE * 2) {
             co->caller = NULL;
             QSLIST_INSERT_HEAD_ATOMIC(&release_pool, co, pool_next);
-            atomic_inc(&pool_size);
+            atomic_inc(&release_pool_size);
+            return;
+        } else if (alloc_pool_size < POOL_BATCH_SIZE) {
+            co->caller = NULL;
+            QSLIST_INSERT_HEAD(&alloc_pool, co, pool_next);
+            alloc_pool_size++;
             return;
         }
     }

Bug?:
The release_pool is not cleanup up on termination I think.

Peter

#18 lore raw
Quoted 13 lines · Peter Lieven

Am 28.11.2014 um 13:26 schrieb Paolo Bonzini:

Quoted 12 lines · Peter Lieven
Quoted 4 lines
Quoted 2 lines

I get:
Run operation 40000000 iterations 9.883958 s, 4046K operations/s, 247ns per coroutine

Ok, understood, it "steals" the whole pool, right? Isn't that bad if we have more than one thread in need of a lot of coroutines?

Overall the algorithm is expected to adapt. The N threads contribute to the global release pool, so the pool will fill up N times faster than if you had only one thread. There can be some variance, which is why the maximum size of the pool is twice the threshold (and probably could be tuned better).

Quoted 7 lines
Quoted 2 lines

Benchmarks are needed on real I/O too, of course, especially with high queue depth.

Yes, cool. The atomic operations are a bit tricky at the first glance ;-)

Question:
Why is the pool_size increment atomic and the set to zero not?

Because the set to zero is not a read-modify-write operation, so it is always atomic. It's just not sequentially-consistent (see docs/atomics.txt for some info on what that means).

Quoted 2 lines

Idea:
If the release_pool is full why not put the coroutine in the thread alloc_pool instead of throwing it away? :-)

Because you can only waste 64 coroutines per thread. But numbers cannot be sneezed at, so it's worth doing it as a separate patch.

Quoted 62 lines

Run operation 40000000 iterations 9.057805 s, 4416K operations/s, 226ns per coroutine

qemu-coroutine.c +14 -6
index 6bee354..edea162 100644
@@ -25,8 +25,9 @@ enum {
 
 /** Free list to speed up creation */
 static QSLIST_HEAD(, Coroutine) release_pool = QSLIST_HEAD_INITIALIZER(pool);
-static unsigned int pool_size;
+static unsigned int release_pool_size;
 static __thread QSLIST_HEAD(, Coroutine) alloc_pool = QSLIST_HEAD_INITIALIZER(pool);
+static __thread unsigned int alloc_pool_size;
 
 /* The GPrivate is only used to invoke coroutine_pool_cleanup.  */
 static void coroutine_pool_cleanup(void *value);
@@ -39,12 +40,12 @@ Coroutine *qemu_coroutine_create(CoroutineEntry *entry)
     if (CONFIG_COROUTINE_POOL) {
         co = QSLIST_FIRST(&alloc_pool);
         if (!co) {
-            if (pool_size > POOL_BATCH_SIZE) {
-                /* This is not exact; there could be a little skew between pool_size
+            if (release_pool_size > POOL_BATCH_SIZE) {
+                /* This is not exact; there could be a little skew between release_pool_size
                  * and the actual size of alloc_pool.  But it is just a heuristic,
                  * it does not need to be perfect.
                  */
-                pool_size = 0;
+                alloc_pool_size = atomic_fetch_and(&release_pool_size, 0);
                 QSLIST_MOVE_ATOMIC(&alloc_pool, &release_pool);
                 co = QSLIST_FIRST(&alloc_pool);
 
@@ -53,6 +54,8 @@ Coroutine *qemu_coroutine_create(CoroutineEntry *entry)
                  */
                 g_private_set(&dummy_key, &dummy_key);
             }
+        } else {
+            alloc_pool_size--;
         }
         if (co) {
             QSLIST_REMOVE_HEAD(&alloc_pool, pool_next);
@@ -71,10 +74,15 @@ Coroutine *qemu_coroutine_create(CoroutineEntry *entry)
 static void coroutine_delete(Coroutine *co)
 {
     if (CONFIG_COROUTINE_POOL) {
-        if (pool_size < POOL_BATCH_SIZE * 2) {
+        if (release_pool_size < POOL_BATCH_SIZE * 2) {
             co->caller = NULL;
             QSLIST_INSERT_HEAD_ATOMIC(&release_pool, co, pool_next);
-            atomic_inc(&pool_size);
+            atomic_inc(&release_pool_size);
+            return;
+        } else if (alloc_pool_size < POOL_BATCH_SIZE) {
+            co->caller = NULL;
+            QSLIST_INSERT_HEAD(&alloc_pool, co, pool_next);
+            alloc_pool_size++;
             return;
         }
     }
Quoted 2 lines

Bug?:
The release_pool is not cleanup up on termination I think.

That's not necessary, it is global.

Paolo

#19 lore raw

Am 28.11.2014 um 13:45 schrieb Paolo Bonzini:

Quoted 20 lines · Peter Lieven
Quoted 12 lines

Am 28.11.2014 um 13:26 schrieb Paolo Bonzini:

Quoted 11 lines · Peter Lieven
Quoted 4 lines
Quoted 2 lines

I get:
Run operation 40000000 iterations 9.883958 s, 4046K operations/s, 247ns per coroutine

Ok, understood, it "steals" the whole pool, right? Isn't that bad if we have more than one thread in need of a lot of coroutines?

Overall the algorithm is expected to adapt. The N threads contribute to the global release pool, so the pool will fill up N times faster than if you had only one thread. There can be some variance, which is why the maximum size of the pool is twice the threshold (and probably could be tuned better).

Quoted 6 lines
Quoted 2 lines

Benchmarks are needed on real I/O too, of course, especially with high queue depth.

Yes, cool. The atomic operations are a bit tricky at the first glance ;-)

Question:
Why is the pool_size increment atomic and the set to zero not?

Quoted 8 lines

Because the set to zero is not a read-modify-write operation, so it is always atomic. It's just not sequentially-consistent (see docs/atomics.txt for some info on what that means).

Quoted 2 lines

Idea:
If the release_pool is full why not put the coroutine in the thread alloc_pool instead of throwing it away? :-)

Because you can only waste 64 coroutines per thread. But numbers cannot be sneezed at, so it's worth doing it as a separate patch.

What do you mean by that? If I use dataplane I will fill the global pool and never use it okay, but then I use thread local storage only. So I get the same numbers as in my thread local storage only version.

Maybe it is an idea to tweak the POOL_BATCH_SIZE * 2 according to what is really attached. If we have only dataplane or ioeventfd it can be POOL_BATCH_SIZE * 0 and we even won't waste those coroutines oxidating in the global pool.

Peter

#20 lore raw
Quoted 3 lines · Peter Lieven
Quoted 3 lines
Quoted 3 lines

Idea:

Quoted 1 line
Quoted 1 line

If the release_pool is full why not put the coroutine in the thread alloc_pool instead of throwing it away? :-)

Because you can only waste 64 coroutines per thread. But numbers cannot

s/only//

Quoted 3 lines
Quoted 1 line
Quoted 1 line

be sneezed at, so it's worth doing it as a separate patch.

What do you mean by that? If I use dataplane I will fill the global pool and never use it okay, but then I use thread local storage only. So I get the same numbers as in my thread local storage only version.

Right. I didn't want to waste the coroutines. But it's not 64 coroutines per VCPU thread, it's just 64 coroutines for the global iothread because all the dataplane threads are guaranteed to use ioeventfd. Let's do it. :)

Can I add your Signed-off-by to the patch?

Paolo

Quoted 3 lines

Maybe it is an idea to tweak the POOL_BATCH_SIZE * 2 according to what is really attached. If we have only dataplane or ioeventfd it can be POOL_BATCH_SIZE * 0 and we even won't waste those coroutines oxidating in the global pool.

#21 lore raw
In reply to Paolo Bonzini

Am 28.11.2014 um 13:45 schrieb Paolo Bonzini:

Quoted 91 lines · Peter Lieven
Quoted 12 lines

Am 28.11.2014 um 13:26 schrieb Paolo Bonzini:

Quoted 11 lines · Peter Lieven
Quoted 4 lines
Quoted 2 lines

I get:
Run operation 40000000 iterations 9.883958 s, 4046K operations/s, 247ns per coroutine

Ok, understood, it "steals" the whole pool, right? Isn't that bad if we have more than one thread in need of a lot of coroutines?

Overall the algorithm is expected to adapt. The N threads contribute to the global release pool, so the pool will fill up N times faster than if you had only one thread. There can be some variance, which is why the maximum size of the pool is twice the threshold (and probably could be tuned better).

Quoted 6 lines
Quoted 2 lines

Benchmarks are needed on real I/O too, of course, especially with high queue depth.

Yes, cool. The atomic operations are a bit tricky at the first glance ;-)

Question:
Why is the pool_size increment atomic and the set to zero not?

Because the set to zero is not a read-modify-write operation, so it is always atomic. It's just not sequentially-consistent (see docs/atomics.txt for some info on what that means).

Quoted 2 lines

Idea:
If the release_pool is full why not put the coroutine in the thread alloc_pool instead of throwing it away? :-)

Because you can only waste 64 coroutines per thread. But numbers cannot be sneezed at, so it's worth doing it as a separate patch.

Quoted 62 lines

Run operation 40000000 iterations 9.057805 s, 4416K operations/s, 226ns per coroutine

qemu-coroutine.c +14 -6
index 6bee354..edea162 100644
@@ -25,8 +25,9 @@ enum {
 
 /** Free list to speed up creation */
 static QSLIST_HEAD(, Coroutine) release_pool = QSLIST_HEAD_INITIALIZER(pool);
-static unsigned int pool_size;
+static unsigned int release_pool_size;
 static __thread QSLIST_HEAD(, Coroutine) alloc_pool = QSLIST_HEAD_INITIALIZER(pool);
+static __thread unsigned int alloc_pool_size;
 
 /* The GPrivate is only used to invoke coroutine_pool_cleanup.  */
 static void coroutine_pool_cleanup(void *value);
@@ -39,12 +40,12 @@ Coroutine *qemu_coroutine_create(CoroutineEntry *entry)
     if (CONFIG_COROUTINE_POOL) {
         co = QSLIST_FIRST(&alloc_pool);
         if (!co) {
-            if (pool_size > POOL_BATCH_SIZE) {
-                /* This is not exact; there could be a little skew between pool_size
+            if (release_pool_size > POOL_BATCH_SIZE) {
+                /* This is not exact; there could be a little skew between release_pool_size
                  * and the actual size of alloc_pool.  But it is just a heuristic,
                  * it does not need to be perfect.
                  */
-                pool_size = 0;
+                alloc_pool_size = atomic_fetch_and(&release_pool_size, 0);
                 QSLIST_MOVE_ATOMIC(&alloc_pool, &release_pool);
                 co = QSLIST_FIRST(&alloc_pool);
 
@@ -53,6 +54,8 @@ Coroutine *qemu_coroutine_create(CoroutineEntry *entry)
                  */
                 g_private_set(&dummy_key, &dummy_key);
             }
+        } else {
+            alloc_pool_size--;
         }
         if (co) {
             QSLIST_REMOVE_HEAD(&alloc_pool, pool_next);
@@ -71,10 +74,15 @@ Coroutine *qemu_coroutine_create(CoroutineEntry *entry)
 static void coroutine_delete(Coroutine *co)
 {
     if (CONFIG_COROUTINE_POOL) {
-        if (pool_size < POOL_BATCH_SIZE * 2) {
+        if (release_pool_size < POOL_BATCH_SIZE * 2) {
             co->caller = NULL;
             QSLIST_INSERT_HEAD_ATOMIC(&release_pool, co, pool_next);
-            atomic_inc(&pool_size);
+            atomic_inc(&release_pool_size);
+            return;
+        } else if (alloc_pool_size < POOL_BATCH_SIZE) {
+            co->caller = NULL;
+            QSLIST_INSERT_HEAD(&alloc_pool, co, pool_next);
+            alloc_pool_size++;
             return;
         }
     }
Quoted 3 lines
Quoted 2 lines

Bug?:
The release_pool is not cleanup up on termination I think.

That's not necessary, it is global.

I don't see where you iterate over release_pool and destroy all coroutines?

Maybe just add back the old destructor with s/pool/release_pool/g

Peter

Peter

#22 lore raw
Quoted 3 lines · Peter Lieven
Quoted 2 lines
Quoted 2 lines
Quoted 1 line
Quoted 1 line

The release_pool is not cleanup up on termination I think.

That's not necessary, it is global.

I don't see where you iterate over release_pool and destroy all coroutines?

The OS does that for us when we exit.

Paolo

#23 lore raw

Am 28.11.2014 um 15:17 schrieb Paolo Bonzini:

Quoted 6 lines · Peter Lieven
Quoted 3 lines
Quoted 2 lines
Quoted 2 lines
Quoted 1 line
Quoted 1 line

The release_pool is not cleanup up on termination I think.

That's not necessary, it is global.

I don't see where you iterate over release_pool and destroy all coroutines?

The OS does that for us when we exit.

Sure, but isn't that considered bad practice? Before this patch the destructured freed the coroutines.

Peter

#24 lore raw
In reply to Peter Lieven

Am 28.11.2014 um 13:39 schrieb Peter Lieven:

Quoted 72 lines

Am 28.11.2014 um 13:26 schrieb Paolo Bonzini:

Quoted 5 lines · Peter Lieven
Quoted 4 lines
Quoted 2 lines

I get:
Run operation 40000000 iterations 9.883958 s, 4046K operations/s, 247ns per coroutine

Ok, understood, it "steals" the whole pool, right? Isn't that bad if we have more than one thread in need of a lot of coroutines?

Quoted 8 lines

Overall the algorithm is expected to adapt. The N threads contribute to the global release pool, so the pool will fill up N times faster than if you had only one thread. There can be some variance, which is why the maximum size of the pool is twice the threshold (and probably could be tuned better).

Benchmarks are needed on real I/O too, of course, especially with high queue depth.

Yes, cool. The atomic operations are a bit tricky at the first glance ;-)

Question:
Why is the pool_size increment atomic and the set to zero not?

Idea:
If the release_pool is full why not put the coroutine in the thread alloc_pool instead of throwing it away? :-)

Run operation 40000000 iterations 9.057805 s, 4416K operations/s, 226ns per coroutine

qemu-coroutine.c +8 -5
index 6bee354..edea162 100644
@@ -25,8 +25,9 @@ enum {
 
 /** Free list to speed up creation */
 static QSLIST_HEAD(, Coroutine) release_pool = QSLIST_HEAD_INITIALIZER(pool);
-static unsigned int pool_size;
+static unsigned int release_pool_size;
 static __thread QSLIST_HEAD(, Coroutine) alloc_pool = QSLIST_HEAD_INITIALIZER(pool);
+static __thread unsigned int alloc_pool_size;
 
 /* The GPrivate is only used to invoke coroutine_pool_cleanup.  */
 static void coroutine_pool_cleanup(void *value);
@@ -39,12 +40,12 @@ Coroutine *qemu_coroutine_create(CoroutineEntry *entry)
     if (CONFIG_COROUTINE_POOL) {
         co = QSLIST_FIRST(&alloc_pool);
         if (!co) {
-            if (pool_size > POOL_BATCH_SIZE) {
-                /* This is not exact; there could be a little skew between pool_size
+            if (release_pool_size > POOL_BATCH_SIZE) {
+                /* This is not exact; there could be a little skew between release_pool_size
                  * and the actual size of alloc_pool.  But it is just a heuristic,
                  * it does not need to be perfect.
                  */
-                pool_size = 0;
+                alloc_pool_size = atomic_fetch_and(&release_pool_size, 0);
                 QSLIST_MOVE_ATOMIC(&alloc_pool, &release_pool);
                 co = QSLIST_FIRST(&alloc_pool);
 
@@ -53,6 +54,8 @@ Coroutine *qemu_coroutine_create(CoroutineEntry *entry)
                  */
                 g_private_set(&dummy_key, &dummy_key);
             }
+        } else {
+            alloc_pool_size--;
         }
         if (co) {
             QSLIST_REMOVE_HEAD(&alloc_pool, pool_next);
@@ -71,10 +74,15 @@ Coroutine *qemu_coroutine_create(CoroutineEntry *entry)
 static void coroutine_delete(Coroutine *co)
 {
     if (CONFIG_COROUTINE_POOL) {
-        if (pool_size < POOL_BATCH_SIZE * 2) {
+        if (release_pool_size < POOL_BATCH_SIZE * 2) {
             co->caller = NULL;
             QSLIST_INSERT_HEAD_ATOMIC(&release_pool, co, pool_next);

qemu-coroutine.c Coroutine *qemu_coroutine_create(CoroutineEntry *entry) · line 88

Quoted 10 lines
qemu-coroutine.c +6 -1
-            atomic_inc(&pool_size);
+            atomic_inc(&release_pool_size);
+            return;
+        } else if (alloc_pool_size < POOL_BATCH_SIZE) {
+            co->caller = NULL;
+            QSLIST_INSERT_HEAD(&alloc_pool, co, pool_next);
+            alloc_pool_size++;
             return;
         }
     }
#25 lore raw
In reply to Peter Lieven
Quoted 37 lines · Peter Lieven

Am 28.11.2014 um 12:23 schrieb Paolo Bonzini:

Quoted 10 lines · Peter Lieven
Quoted 8 lines

Am 28.11.2014 um 12:14 schrieb Paolo Bonzini:

Quoted 7 lines
Quoted 5 lines

master:
Run operation 40000000 iterations 12.851414 s, 3112K operations/s, 321ns per coroutine

paolo:
Run operation 40000000 iterations 11.951720 s, 3346K operations/s, 298ns per coroutine

Nice. :)

Quoted 8 lines
Quoted 6 lines
Quoted 3 lines

Can you please try "coroutine: Use __thread … " together, too? I still see 11% time spent in pthread_getspecific, and I get ~10% more indeed if I apply it here (my times are 191/160/145).

indeed:

Run operation 40000000 iterations 10.138684 s, 3945K operations/s, 253ns per coroutine

Your perf_master2 uses the ring buffer unconditionally, right? I wonder if we can use a similar algorithm but with arrays instead of lists...

Why do you set pool_size = 0 in the create path?

When I do the following:

qemu-coroutine.c +1 -1
index 6bee354..c79ee78 100644
@@ -44,7 +44,7 @@ Coroutine *qemu_coroutine_create(CoroutineEntry *entry)
                  * and the actual size of alloc_pool.  But it is just a heuristic,
                  * it does not need to be perfect.
                  */
-                pool_size = 0;
+                atomic_dec(&pool_size);
                 QSLIST_MOVE_ATOMIC(&alloc_pool, &release_pool);
                 co = QSLIST_FIRST(&alloc_pool);
 
Quoted 2 lines

I get:
Run operation 40000000 iterations 9.883958 s, 4046K operations/s, 247ns per coroutine

Because pool_size is the (approximate) number of coroutines in the pool.
It is zero after QSLIST_MOVE_ATOMIC has NULL-ed out release_pool.slh_first.

Paolo

#26 lore raw

Am 28.11.2014 um 13:21 schrieb Paolo Bonzini:

Quoted 37 lines · Peter Lieven
Quoted 35 lines

Am 28.11.2014 um 12:23 schrieb Paolo Bonzini:

Quoted 9 lines · Peter Lieven
Quoted 8 lines

Am 28.11.2014 um 12:14 schrieb Paolo Bonzini:

Quoted 7 lines
Quoted 5 lines

master:
Run operation 40000000 iterations 12.851414 s, 3112K operations/s, 321ns per coroutine

paolo:
Run operation 40000000 iterations 11.951720 s, 3346K operations/s, 298ns per coroutine

Nice. :)

Quoted 8 lines
Quoted 6 lines
Quoted 3 lines

Can you please try "coroutine: Use __thread … " together, too? I still see 11% time spent in pthread_getspecific, and I get ~10% more indeed if I apply it here (my times are 191/160/145).

indeed:

Run operation 40000000 iterations 10.138684 s, 3945K operations/s, 253ns per coroutine

Your perf_master2 uses the ring buffer unconditionally, right? I wonder if we can use a similar algorithm but with arrays instead of lists...

Why do you set pool_size = 0 in the create path?

When I do the following:

qemu-coroutine.c +1 -1
index 6bee354..c79ee78 100644
@@ -44,7 +44,7 @@ Coroutine *qemu_coroutine_create(CoroutineEntry *entry)
                  * and the actual size of alloc_pool.  But it is just a heuristic,
                  * it does not need to be perfect.
                  */
-                pool_size = 0;
+                atomic_dec(&pool_size);
                 QSLIST_MOVE_ATOMIC(&alloc_pool, &release_pool);
                 co = QSLIST_FIRST(&alloc_pool);
 
Quoted 4 lines
Quoted 2 lines

I get:
Run operation 40000000 iterations 9.883958 s, 4046K operations/s, 247ns per coroutine

Because pool_size is the (approximate) number of coroutines in the pool.
It is zero after QSLIST_MOVE_ATOMIC has NULL-ed out release_pool.slh_first.

got it meanwhile. and its not as bad as i thought since you only steal the release_pool if your alloc_pool is empty. Right?

Peter

#27 lore raw
In reply to Peter Lieven
Quoted 13 lines · Peter Lieven
iothread.c +1 -0
index 342a23f..b53529b 100644
@@ -15,6 +15,7 @@
 #include "qom/object_interfaces.h"
 #include "qemu/module.h"
 #include "block/aio.h"
+#include "block/coroutine.h"
 #include "sysemu/iothread.h"
 #include "qmp-commands.h"
 #include "qemu/error-report.h"

iothread.c static void *iothread_run(void *opaque) · line 54

Quoted 7 lines
iothread.c +2 -0
@@ -47,6 +48,8 @@ static void *iothread_run(void *opaque)
         }
         aio_context_release(iothread->ctx);
     }
+
+    coroutine_pool_cleanup();
     return NULL;
 }

The assumption here is that iothread_run() is the only thread function that uses coroutines.

If another thread uses coroutines the pool will leak :(. This is one of the challenges of thread-local storage.