mm: zswap: reduce request contention on loads
Subsystems (3)
v3 cover mm: zswap: reduce request contention on loads
Stores and loads share a per-CPU acomp request and mutex. A low-priority store can be preempted right after the compressor drops its stream lock, while it still holds the zswap mutex, and a higher-priority load on that CPU then waits for the store to run again. This follows the work from Sergey Senozhatsky's zram series which splits it for the same reason [1].
Patch 1 gives compression and decompression separate requests, waits and mutexes, so loads no longer wait for stores, though they can still wait for each other. Patch 2, authored by Herbert Xu, enforces MAX_SYNC_COMP_REQSIZE when synchronous Crypto ACOMP transforms are initialized. Patch 3 uses an on-stack request for synchronous decompression, so those loads take no zswap lock. This covers all in-tree software compressors. Asynchronous algorithms keep the per-CPU request and mutex. For software compressors the series allocates the same number of requests as before; each per-CPU context grows by 72 bytes, and the load path is about 270 bytes deeper on x86-64.
The series does not fix two related cases:
- Stores still serialize on the compression mutex, so a high-priority task that reclaims (direct reclaim, MADV_PAGEOUT) can still wait for a preempted store.
- On PREEMPT_RT the codec stream locks are preemptible, so a load can still wait for a preempted store inside the codec.
The numbers below are the slowest read per run, as a median (min-max) of 5 runs. Each run is 12 seconds in a zstd VM with lazy preemption, vm.page-cluster=0 and swap on /dev/ram0. With 1 vCPU, four nice +10 workers page memory out and read it back while a nice 0 task spins. A nice -19 reader pages out its own buffer and measures how long each read of it takes. With 8 vCPUs there are 16 workers, 8 spinning tasks and 8 readers.
Before series (ms) With series (ms) 1 vCPU 22.3 (21.6-22.6) 0.97 (0.72-1.4) 8 vCPUs 314 (97-2542) 7.0 (5.0-98)
Reads over 10 ms fell from 26-35 per run to none with 1 vCPU, and from 3-18 per run to at most one with 8 vCPUs. The benchmark and test programs were written with the help of an LLM.
[1] https://lore.kernel.org/all/[email protected]/
v2 -> v3:
- Include Herbert Xu's Crypto API patch enforcing MAX_SYNC_COMP_REQSIZE when synchronous ACOMP transforms are initialized.
- Use acomp_is_async() in the stack-request patch and remove the redundant acomp_can_use_stack_req() helper (Herbert).
v1 -> v2:
- Group the compression request and output buffer in zswap_comp_ctx, documenting that the compression mutex protects the buffer (Yosry).
- Move the existing synchronous and request context size checks into the Crypto API helper acomp_can_use_stack_req() (Yosry).
- Share callback setup through zswap_set_acomp_req_callback() and explain the two decompression paths (Yosry).
- Clarify in patch 1's commit message that codecs and drivers synchronize shared transform state internally (Yosry).
- Add Nhat Pham's Acked-by on patch 2.
- Add Nhat Pham's Acked-by to the first zswap patch.
Herbert Xu (1):
crypto: acomp - enforce MAX_SYNC_COMP_REQSIZE
Usama Arif (2):
mm: zswap: use separate compression and decompression requests
mm: zswap: use stack requests for synchronous decompression
crypto/acompress.c | 18 ++--- mm/zswap.c | 161 +++++++++++++++++++++++++++++---------------- 2 files changed, 115 insertions(+), 64 deletions(-)
Signature · 1 line
2.53.0-Meta
PATCH v3 1/3 mm: zswap: use separate compression and decompression requests
Stores and loads serialize on the same per-CPU acomp request and mutex. A low-priority store can be preempted as soon as the compressor drops its stream lock, while it still holds the mutex. A higher-priority load on that CPU then waits until the store runs again, which can take a long time when other tasks are runnable.
Give compression and decompression their own request, completion wait and mutex. Since commit e2c3b6b21c77f ("mm: zswap: use SG list decompression APIs from zsmalloc"), the per-CPU buffer is only used for compression, so group it with the compression request. Independent requests can share the per-CPU transform; the codecs and drivers synchronize their shared state internally. Loads can still wait for each other on the decompression mutex, and stores still serialize on the compression mutex.
acomp_request_free() ignores NULL requests, so drop the check in acomp_ctx_free().
This follows the proposal from Sergey Senozhatsky for the same split for zram [1].
[1] https://lore.kernel.org/all/[email protected]/
--- mm/zswap.c | 108 +++++++++++++++++++++++++++++++---------------------- 1 file changed, 64 insertions(+), 44 deletions(-)
mm/zswap.c +64 -44
@@ -137,14 +137,25 @@ bool zswap_never_enabled(void)* data structures **********************************/ -struct crypto_acomp_ctx { -struct crypto_acomp *acomp; +struct zswap_acomp_req {struct acomp_req *req;struct crypto_wait wait; -u8 *buffer;struct mutex mutex; }; +/* The compression mutex also protects the output buffer. */ +struct zswap_comp_ctx { +struct zswap_acomp_req areq; +u8 *buffer; +}; + +/* Separate requests, so that decompression does not wait for compression. */ +struct crypto_acomp_ctx { +struct crypto_acomp *acomp; +struct zswap_comp_ctx comp; +struct zswap_acomp_req decomp; +}; +/* * The lock ordering is zswap_tree.lock -> zswap_pool.lru_lock. * The only case where lru_lock is not acquired while holding tree.lock is
@@ -270,14 +281,10 @@ static void acomp_ctx_free(struct crypto_acomp_ctx *acomp_ctx)if (!acomp_ctx)return ; -/* -* If there was an error in allocating @acomp_ctx->req, it -* would be set to NULL. -*/ -if (acomp_ctx->req) -acomp_request_free (acomp_ctx->req); - - acomp_ctx->req =NULL ; +acomp_request_free (acomp_ctx->comp.areq.req); + acomp_ctx->comp.areq.req =NULL ; +acomp_request_free (acomp_ctx->decomp.req); + acomp_ctx->decomp.req =NULL ;/* * We have to handle both cases here: an error pointer return from
@@ -289,8 +296,8 @@ static void acomp_ctx_free(struct crypto_acomp_ctx *acomp_ctx) acomp_ctx->acomp =NULL ; -kfree (acomp_ctx->buffer); - acomp_ctx->buffer =NULL ; +kfree (acomp_ctx->comp.buffer); + acomp_ctx->comp.buffer =NULL ; }static struct zswap_pool *zswap_pool_create (char *compressor)
@@ -796,6 +803,28 @@ static void zswap_entry_free(struct zswap_entry *entry)/********************************* * compressed storage functions **********************************/ +static int zswap_acomp_req_init (struct zswap_acomp_req *areq, +struct crypto_acomp *acomp) +{ +/* acomp_request_alloc() returns NULL in case of an error. */ + areq->req =acomp_request_alloc (acomp); +if (!areq->req) +return -ENOMEM ; + +crypto_init_wait (&areq->wait); + +/* +* if the backend of acomp is async zip, crypto_req_done() will wakeup +* crypto_wait_req(); if the backend of acomp is scomp, the callback +* won't be called, crypto_wait_req() will return without blocking. +*/ +acomp_request_set_callback (areq->req,CRYPTO_TFM_REQ_MAY_BACKLOG , + crypto_req_done, &areq->wait); + +mutex_init (&areq->mutex); +return 0 ; +} +static int zswap_cpu_comp_prepare (unsigned int cpu,struct hlist_node *node) { struct zswap_pool *pool = hlist_entry(node, struct zswap_pool, node);
@@ -811,8 +840,8 @@ static int zswap_cpu_comp_prepare(unsigned int cpu, struct hlist_node *node)return 0 ; } - acomp_ctx->buffer =kmalloc_node (PAGE_SIZE ,GFP_KERNEL ,cpu_to_node (cpu)); -if (!acomp_ctx->buffer) + acomp_ctx->comp.buffer =kmalloc_node (PAGE_SIZE ,GFP_KERNEL ,cpu_to_node (cpu)); +if (!acomp_ctx->comp.buffer)return ret;/*
@@ -827,25 +856,13 @@ static int zswap_cpu_comp_prepare(unsigned int cpu, struct hlist_node *node) goto fail; } -/* acomp_request_alloc() returns NULL in case of an error. */ - acomp_ctx->req =acomp_request_alloc (acomp_ctx->acomp); -if (!acomp_ctx->req) { +if (zswap_acomp_req_init (&acomp_ctx->comp.areq, acomp_ctx->acomp) || +zswap_acomp_req_init (&acomp_ctx->decomp, acomp_ctx->acomp)) {pr_err ("could not alloc crypto acomp_request %s\n" , pool->tfm_name); goto fail; } -crypto_init_wait (&acomp_ctx->wait); - -/* -* if the backend of acomp is async zip, crypto_req_done() will wakeup -* crypto_wait_req(); if the backend of acomp is scomp, the callback -* won't be called, crypto_wait_req() will return without blocking. -*/ -acomp_request_set_callback (acomp_ctx->req,CRYPTO_TFM_REQ_MAY_BACKLOG , - crypto_req_done, &acomp_ctx->wait); - -mutex_init (&acomp_ctx->mutex);return 0 ; fail:
@@ -856,7 +873,7 @@ static int zswap_cpu_comp_prepare(unsigned int cpu, struct hlist_node *node)static bool zswap_compress (struct folio *folio,long index,struct zswap_entry *entry,struct zswap_pool *pool) { -struct crypto_acomp_ctx *acomp_ctx; +struct zswap_comp_ctx *comp_ctx;struct scatterlist input, output;int comp_ret =0 , alloc_ret =0 ;unsigned int dlen =PAGE_SIZE ;
@@ -865,15 +882,16 @@ static bool zswap_compress(struct folio *folio, long index,u8 *dst;bool mapped = false; - acomp_ctx =raw_cpu_ptr (pool->acomp_ctx); -mutex_lock (&acomp_ctx->mutex); + comp_ctx = &raw_cpu_ptr (pool->acomp_ctx)->comp; +mutex_lock (&comp_ctx->areq.mutex); - dst = acomp_ctx->buffer; + dst = comp_ctx->buffer;sg_init_table (&input,1 );sg_set_folio (&input, folio,PAGE_SIZE , index *PAGE_SIZE );sg_init_one (&output, dst,PAGE_SIZE ); -acomp_request_set_params (acomp_ctx->req, &input, &output,PAGE_SIZE , dlen); +acomp_request_set_params (comp_ctx->areq.req, &input, &output, +PAGE_SIZE , dlen);/* * it maybe looks a little bit silly that we send an asynchronous request,
@@ -885,10 +903,12 @@ static bool zswap_compress(struct folio *folio, long index,* existing method to send the second page before the first page is done * in one thread doing zswap. * but in different threads running on different cpu, we have different -* acomp instance, so multiple threads can do (de)compression in parallel. +* acomp instance, and compression and decompression use separate +* requests, so multiple threads can do (de)compression in parallel. */ - comp_ret =crypto_wait_req (crypto_acomp_compress (acomp_ctx->req), &acomp_ctx->wait); - dlen = acomp_ctx->req->dlen; + comp_ret =crypto_wait_req (crypto_acomp_compress (comp_ctx->areq.req), + &comp_ctx->areq.wait); + dlen = comp_ctx->areq.req->dlen;/* * If a page cannot be compressed into a size smaller than PAGE_SIZE,
@@ -932,7 +952,7 @@ static bool zswap_compress(struct folio *folio, long index,else if (alloc_ret) zswap_reject_alloc_fail++; -mutex_unlock (&acomp_ctx->mutex); +mutex_unlock (&comp_ctx->areq.mutex);return comp_ret ==0 && alloc_ret ==0 ; }
@@ -948,7 +968,7 @@ static bool zswap_decompress(struct zswap_entry *entry, struct folio *folio)return false; acomp_ctx =raw_cpu_ptr (pool->acomp_ctx); -mutex_lock (&acomp_ctx->mutex); +mutex_lock (&acomp_ctx->decomp.mutex);zs_obj_read_sg_begin (pool->zs_pool, entry->handle, input, entry->length);/* zswap entries of length PAGE_SIZE are not compressed. */
@@ -965,15 +985,15 @@ static bool zswap_decompress(struct zswap_entry *entry, struct folio *folio) }else {sg_init_table (&output,1 );sg_set_folio (&output, folio,PAGE_SIZE ,0 ); -acomp_request_set_params (acomp_ctx->req, input, &output, +acomp_request_set_params (acomp_ctx->decomp.req, input, &output, entry->length,PAGE_SIZE ); - ret =crypto_acomp_decompress (acomp_ctx->req); - ret =crypto_wait_req (ret, &acomp_ctx->wait); - dlen = acomp_ctx->req->dlen; + ret =crypto_acomp_decompress (acomp_ctx->decomp.req); + ret =crypto_wait_req (ret, &acomp_ctx->decomp.wait); + dlen = acomp_ctx->decomp.req->dlen; }zs_obj_read_sg_end (pool->zs_pool, entry->handle); -mutex_unlock (&acomp_ctx->mutex); +mutex_unlock (&acomp_ctx->decomp.mutex);if (!ret && dlen ==PAGE_SIZE )return true;
Signature · 1 line
2.53.0-Meta
Re: [PATCH v3 1/3] mm: zswap: use separate compression and decompression requests
Quoted 19 lines · Usama Arif
Stores and loads serialize on the same per-CPU acomp request and mutex. A low-priority store can be preempted as soon as the compressor drops its stream lock, while it still holds the mutex. A higher-priority load on that CPU then waits until the store runs again, which can take a long time when other tasks are runnable.
Give compression and decompression their own request, completion wait and mutex. Since commit
e2c3b6b21c77f("mm: zswap: use SG list decompression APIs from zsmalloc"), the per-CPU buffer is only used for compression, so group it with the compression request. Independent requests can share the per-CPU transform; the codecs and drivers synchronize their shared state internally. Loads can still wait for each other on the decompression mutex, and stores still serialize on the compression mutex.
acomp_request_free()ignores NULL requests, so drop the check inacomp_ctx_free().
Quoted 7 lines
This follows the proposal from Sergey Senozhatsky for the same split for zram [1].
[1] https://lore.kernel.org/all/[email protected]/
Acked-by Nhat Pham <[email protected]>Signed-off-by Usama Arif <[email protected]>
Why didn't you take Herbert's Ack?
LGTM with one nit below:
Quoted 108 lines
--- mm/zswap.c | 108 +++++++++++++++++++++++++++++++---------------------- 1 file changed, 64 insertions(+), 44 deletions(-)mm/zswap.c +44
-15@@ -137,14 +137,25 @@ bool zswap_never_enabled(void)* data structures **********************************/ -struct crypto_acomp_ctx { -struct crypto_acomp *acomp; +struct zswap_acomp_req {struct acomp_req *req;struct crypto_wait wait; -u8 *buffer;struct mutex mutex; }; +/* The compression mutex also protects the output buffer. */ +struct zswap_comp_ctx { +struct zswap_acomp_req areq; +u8 *buffer; +}; + +/* Separate requests, so that decompression does not wait for compression. */ +struct crypto_acomp_ctx { +struct crypto_acomp *acomp; +struct zswap_comp_ctx comp; +struct zswap_acomp_req decomp; +}; +/* * The lock ordering is zswap_tree.lock -> zswap_pool.lru_lock. * The only case where lru_lock is not acquired while holding tree.lock is @@ -270,14 +281,10 @@ static void acomp_ctx_free(struct crypto_acomp_ctx *acomp_ctx)if (!acomp_ctx)return ; -/* -* If there was an error in allocating @acomp_ctx->req, it -* would be set to NULL. -*/ -if (acomp_ctx->req) -acomp_request_free (acomp_ctx->req); - - acomp_ctx->req =NULL ; +acomp_request_free (acomp_ctx->comp.areq.req); + acomp_ctx->comp.areq.req =NULL ; +acomp_request_free (acomp_ctx->decomp.req); + acomp_ctx->decomp.req =NULL ;/* * We have to handle both cases here: an error pointer return from @@ -289,8 +296,8 @@ static void acomp_ctx_free(struct crypto_acomp_ctx *acomp_ctx) acomp_ctx->acomp =NULL ; -kfree (acomp_ctx->buffer); - acomp_ctx->buffer =NULL ; +kfree (acomp_ctx->comp.buffer); + acomp_ctx->comp.buffer =NULL ; }static struct zswap_pool *zswap_pool_create (char *compressor)@@ -796,6 +803,28 @@ static void zswap_entry_free(struct zswap_entry *entry)/********************************* * compressed storage functions **********************************/ +static int zswap_acomp_req_init (struct zswap_acomp_req *areq, +struct crypto_acomp *acomp) +{ +/* acomp_request_alloc() returns NULL in case of an error. */ + areq->req =acomp_request_alloc (acomp); +if (!areq->req) +return -ENOMEM ; + +crypto_init_wait (&areq->wait); + +/* +* if the backend of acomp is async zip, crypto_req_done() will wakeup +* crypto_wait_req(); if the backend of acomp is scomp, the callback +* won't be called, crypto_wait_req() will return without blocking. +*/ +acomp_request_set_callback (areq->req,CRYPTO_TFM_REQ_MAY_BACKLOG , + crypto_req_done, &areq->wait); + +mutex_init (&areq->mutex); +return 0 ; +} +static int zswap_cpu_comp_prepare (unsigned int cpu,struct hlist_node *node) { struct zswap_pool *pool = hlist_entry(node, struct zswap_pool, node);@@ -811,8 +840,8 @@ static int zswap_cpu_comp_prepare(unsigned int cpu, struct hlist_node *node)return 0 ; } - acomp_ctx->buffer =kmalloc_node (PAGE_SIZE ,GFP_KERNEL ,cpu_to_node (cpu)); -if (!acomp_ctx->buffer) + acomp_ctx->comp.buffer =kmalloc_node (PAGE_SIZE ,GFP_KERNEL ,cpu_to_node (cpu)); +if (!acomp_ctx->comp.buffer)return ret;/*
mm/zswap.c static int zswap_cpu_comp_prepare(unsigned int cpu, struct hlist_node *node) · line 860
Quoted 8 lines
mm/zswap.c +2
-3@@ -827,25 +856,13 @@ static int zswap_cpu_comp_prepare(unsigned int cpu, struct hlist_node *node) goto fail; } -/* acomp_request_alloc() returns NULL in case of an error. */ - acomp_ctx->req =acomp_request_alloc (acomp_ctx->acomp); -if (!acomp_ctx->req) { +if (zswap_acomp_req_init (&acomp_ctx->comp.areq, acomp_ctx->acomp) || +zswap_acomp_req_init (&acomp_ctx->decomp, acomp_ctx->acomp)) {
We stop propagating the output of these functions to the caller here. It still works because ret is initialized as -ENOMEM, and that's the only possible failure of zswap_acomp_req_init(), but that's really subtle.
I'd rather either propagate the return value as-is on failure, or make zswap_acomp_req_init() a boolean so that it's obvious there's one failure mode and future changes can't just return a new error code that would be silently thrown away.
Quoted 19 lines
mm/zswap.c +0
-11pr_err ("could not alloc crypto acomp_request %s\n" , pool->tfm_name); goto fail; } -crypto_init_wait (&acomp_ctx->wait); - -/* -* if the backend of acomp is async zip, crypto_req_done() will wakeup -* crypto_wait_req(); if the backend of acomp is scomp, the callback -* won't be called, crypto_wait_req() will return without blocking. -*/ -acomp_request_set_callback (acomp_ctx->req,CRYPTO_TFM_REQ_MAY_BACKLOG , - crypto_req_done, &acomp_ctx->wait); - -mutex_init (&acomp_ctx->mutex);return 0 ; fail:
Yosry Ahmed
maintainerRe: [PATCH v3 1/3] mm: zswap: use separate compression and decompression requests
Quoted 9 lines · Yosry Ahmed
Quoted 7 lines · Usama Arif
Stores and loads serialize on the same per-CPU acomp request and mutex. A low-priority store can be preempted as soon as the compressor drops its stream lock, while it still holds the mutex. A higher-priority load on that CPU then waits until the store runs again, which can take a long time when other tasks are runnable.
Quoted 8 lines
Quoted 8 lines
Give compression and decompression their own request, completion wait and mutex. Since commit
e2c3b6b21c77f("mm: zswap: use SG list decompression APIs from zsmalloc"), the per-CPU buffer is only used for compression, so group it with the compression request. Independent requests can share the per-CPU transform; the codecs and drivers synchronize their shared state internally. Loads can still wait for each other on the decompression mutex, and stores still serialize on the compression mutex.
Probably worth calling out the extra memory usage pCPU (at least on x86_64).
Re: [PATCH v3 1/3] mm: zswap: use separate compression and decompression requests
In reply to Yosry AhmedQuoted 23 lines · On Tue, 13 Oct 2026 13:57:14 -0700 Yosry Ahmed <[email protected]> wrote:
Quoted 22 lines · Usama Arif
Stores and loads serialize on the same per-CPU acomp request and mutex. A low-priority store can be preempted as soon as the compressor drops its stream lock, while it still holds the mutex. A higher-priority load on that CPU then waits until the store runs again, which can take a long time when other tasks are runnable.
Give compression and decompression their own request, completion wait and mutex. Since commit
e2c3b6b21c77f("mm: zswap: use SG list decompression APIs from zsmalloc"), the per-CPU buffer is only used for compression, so group it with the compression request. Independent requests can share the per-CPU transform; the codecs and drivers synchronize their shared state internally. Loads can still wait for each other on the decompression mutex, and stores still serialize on the compression mutex.
acomp_request_free()ignores NULL requests, so drop the check inacomp_ctx_free().This follows the proposal from Sergey Senozhatsky for the same split for zram [1].
Quoted 6 lines
Quoted 4 lines
[1] https://lore.kernel.org/all/[email protected]/
Acked-by Nhat Pham <[email protected]>Signed-off-by Usama Arif <[email protected]>Why didn't you take Herbert's Ack?
Ah completely forgot, will add it in the next revision and fix the nit below.
Quoted 5 lines
LGTM with one nit below:
Acked-by Yosry Ahmed <[email protected]>
Thanks!
Quoted 147 lines
Quoted 108 lines
--- mm/zswap.c | 108 +++++++++++++++++++++++++++++++---------------------- 1 file changed, 64 insertions(+), 44 deletions(-)mm/zswap.c +44
-15@@ -137,14 +137,25 @@ bool zswap_never_enabled(void)* data structures **********************************/ -struct crypto_acomp_ctx { -struct crypto_acomp *acomp; +struct zswap_acomp_req {struct acomp_req *req;struct crypto_wait wait; -u8 *buffer;struct mutex mutex; }; +/* The compression mutex also protects the output buffer. */ +struct zswap_comp_ctx { +struct zswap_acomp_req areq; +u8 *buffer; +}; + +/* Separate requests, so that decompression does not wait for compression. */ +struct crypto_acomp_ctx { +struct crypto_acomp *acomp; +struct zswap_comp_ctx comp; +struct zswap_acomp_req decomp; +}; +/* * The lock ordering is zswap_tree.lock -> zswap_pool.lru_lock. * The only case where lru_lock is not acquired while holding tree.lock is @@ -270,14 +281,10 @@ static void acomp_ctx_free(struct crypto_acomp_ctx *acomp_ctx)if (!acomp_ctx)return ; -/* -* If there was an error in allocating @acomp_ctx->req, it -* would be set to NULL. -*/ -if (acomp_ctx->req) -acomp_request_free (acomp_ctx->req); - - acomp_ctx->req =NULL ; +acomp_request_free (acomp_ctx->comp.areq.req); + acomp_ctx->comp.areq.req =NULL ; +acomp_request_free (acomp_ctx->decomp.req); + acomp_ctx->decomp.req =NULL ;/* * We have to handle both cases here: an error pointer return from @@ -289,8 +296,8 @@ static void acomp_ctx_free(struct crypto_acomp_ctx *acomp_ctx) acomp_ctx->acomp =NULL ; -kfree (acomp_ctx->buffer); - acomp_ctx->buffer =NULL ; +kfree (acomp_ctx->comp.buffer); + acomp_ctx->comp.buffer =NULL ; }static struct zswap_pool *zswap_pool_create (char *compressor)@@ -796,6 +803,28 @@ static void zswap_entry_free(struct zswap_entry *entry)/********************************* * compressed storage functions **********************************/ +static int zswap_acomp_req_init (struct zswap_acomp_req *areq, +struct crypto_acomp *acomp) +{ +/* acomp_request_alloc() returns NULL in case of an error. */ + areq->req =acomp_request_alloc (acomp); +if (!areq->req) +return -ENOMEM ; + +crypto_init_wait (&areq->wait); + +/* +* if the backend of acomp is async zip, crypto_req_done() will wakeup +* crypto_wait_req(); if the backend of acomp is scomp, the callback +* won't be called, crypto_wait_req() will return without blocking. +*/ +acomp_request_set_callback (areq->req,CRYPTO_TFM_REQ_MAY_BACKLOG , + crypto_req_done, &areq->wait); + +mutex_init (&areq->mutex); +return 0 ; +} +static int zswap_cpu_comp_prepare (unsigned int cpu,struct hlist_node *node) { struct zswap_pool *pool = hlist_entry(node, struct zswap_pool, node);@@ -811,8 +840,8 @@ static int zswap_cpu_comp_prepare(unsigned int cpu, struct hlist_node *node)return 0 ; } - acomp_ctx->buffer =kmalloc_node (PAGE_SIZE ,GFP_KERNEL ,cpu_to_node (cpu)); -if (!acomp_ctx->buffer) + acomp_ctx->comp.buffer =kmalloc_node (PAGE_SIZE ,GFP_KERNEL ,cpu_to_node (cpu)); +if (!acomp_ctx->comp.buffer)return ret;/* mm/zswap.c static int zswap_cpu_comp_prepare(unsigned int cpu, struct hlist_node *node) · line 860
Quoted 8 lines
mm/zswap.c +2
-3@@ -827,25 +856,13 @@ static int zswap_cpu_comp_prepare(unsigned int cpu, struct hlist_node *node) goto fail; } -/* acomp_request_alloc() returns NULL in case of an error. */ - acomp_ctx->req =acomp_request_alloc (acomp_ctx->acomp); -if (!acomp_ctx->req) { +if (zswap_acomp_req_init (&acomp_ctx->comp.areq, acomp_ctx->acomp) || +zswap_acomp_req_init (&acomp_ctx->decomp, acomp_ctx->acomp)) {We stop propagating the output of these functions to the caller here. It still works because ret is initialized as
-ENOMEM, and that's the only possible failure ofzswap_acomp_req_init(), but that's really subtle.I'd rather either propagate the return value as-is on failure, or make
zswap_acomp_req_init()a boolean so that it's obvious there's one failure mode and future changes can't just return a new error code that would be silently thrown away.Quoted 19 lines
mm/zswap.c +0
-11pr_err ("could not alloc crypto acomp_request %s\n" , pool->tfm_name); goto fail; } -crypto_init_wait (&acomp_ctx->wait); - -/* -* if the backend of acomp is async zip, crypto_req_done() will wakeup -* crypto_wait_req(); if the backend of acomp is scomp, the callback -* won't be called, crypto_wait_req() will return without blocking. -*/ -acomp_request_set_callback (acomp_ctx->req,CRYPTO_TFM_REQ_MAY_BACKLOG , - crypto_req_done, &acomp_ctx->wait); - -mutex_init (&acomp_ctx->mutex);return 0 ; fail:
PATCH v3 2/3 crypto: acomp - enforce MAX_SYNC_COMP_REQSIZE
In reply to Usama ArifFrom: Herbert Xu <[email protected]>
Enforce the limit MAX_SYNC_COMP_REQSIZE when an acomp tfm is initialised. All existing algorithms obey this limit.
--- crypto/acompress.c | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-)
crypto/acompress.c +9 -9
@@ -92,10 +92,6 @@ static int crypto_acomp_init_tfm(struct crypto_tfm *tfm)if (IS_ERR (fb))return PTR_ERR (fb); - err = -EINVAL ; -if (crypto_acomp_reqsize (fb) >MAX_SYNC_COMP_REQSIZE ) - goto out_free_fb; - tfm->fb =crypto_acomp_tfm (fb); }
@@ -105,15 +101,19 @@ static int crypto_acomp_init_tfm(struct crypto_tfm *tfm) acomp->base.exit = crypto_acomp_exit_tfm; -if (!alg->init) -return 0 ; - - err = alg->init (acomp); -if (err) +if (alg->init && ((err = alg->init (acomp)))) goto out_free_fb; + err = -EINVAL ; +if (!acomp_is_async (acomp) && +crypto_acomp_reqsize (acomp) >MAX_SYNC_COMP_REQSIZE ) + goto out_exit_tfm; +return 0 ; +out_exit_tfm: +if (alg->exit) + alg->exit (acomp); out_free_fb:crypto_free_acomp (fb);return err;
Signature · 1 line
2.53.0-Meta
PATCH v3 3/3 mm: zswap: use stack requests for synchronous decompression
In reply to Usama ArifWith separate requests for compression and decompression, loads still serialize on the per-CPU decompression mutex. A low-priority load that is preempted after the codec drops its stream lock keeps holding the mutex and stalls every other load on that CPU, including higher-priority ones.
Synchronous algorithms can use an on-stack request, so decompress with one and take no zswap lock. All in-tree software compressors qualify. The Crypto API enforces MAX_SYNC_COMP_REQSIZE during initialization, so checking acomp_is_async() is sufficient. Asynchronous algorithms keep the per-CPU request and mutex, which is still taken before the zsmalloc read lock.
Reading the per-CPU context without the mutex is safe. Since commit ef3c0f6cb798e ("mm: zswap: tie per-CPU acomp_ctx lifetime to the pool"), it is set up before its CPU comes online and is not torn down until the pool is destroyed. The codecs keep their own stream locks, and crypto_acomp_decompress() rejects on-stack requests only for asynchronous transforms, which never take this path.
For software compressors this drops the heap request added for decompression. The on-stack request and wait take 216 bytes, which makes the load path about 270 bytes deeper on x86-64. Asynchronous algorithms pay this too.
--- mm/zswap.c | 87 ++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 59 insertions(+), 28 deletions(-)
mm/zswap.c +59 -28
@@ -153,7 +153,7 @@ struct zswap_comp_ctx {struct crypto_acomp_ctx {struct crypto_acomp *acomp;struct zswap_comp_ctx comp; -struct zswap_acomp_req decomp; +struct zswap_acomp_req decomp;/* unused with on-stack requests */ };/*
@@ -803,6 +803,18 @@ static void zswap_entry_free(struct zswap_entry *entry)/********************************* * compressed storage functions **********************************/ +static void zswap_set_acomp_req_callback (struct acomp_req *req, +struct crypto_wait *wait) +{ +/* +* if the backend of acomp is async zip, crypto_req_done() will wakeup +* crypto_wait_req(); if the backend of acomp is scomp, the callback +* won't be called, crypto_wait_req() will return without blocking. +*/ +acomp_request_set_callback (req,CRYPTO_TFM_REQ_MAY_BACKLOG , + crypto_req_done, wait); +} +static int zswap_acomp_req_init (struct zswap_acomp_req *areq,struct crypto_acomp *acomp) {
@@ -812,14 +824,7 @@ static int zswap_acomp_req_init(struct zswap_acomp_req *areq,return -ENOMEM ;crypto_init_wait (&areq->wait); - -/* -* if the backend of acomp is async zip, crypto_req_done() will wakeup -* crypto_wait_req(); if the backend of acomp is scomp, the callback -* won't be called, crypto_wait_req() will return without blocking. -*/ -acomp_request_set_callback (areq->req,CRYPTO_TFM_REQ_MAY_BACKLOG , - crypto_req_done, &areq->wait); +zswap_set_acomp_req_callback (areq->req, &areq->wait);mutex_init (&areq->mutex);return 0 ;
@@ -856,15 +861,19 @@ static int zswap_cpu_comp_prepare(unsigned int cpu, struct hlist_node *node) goto fail; } -if (zswap_acomp_req_init (&acomp_ctx->comp.areq, acomp_ctx->acomp) || -zswap_acomp_req_init (&acomp_ctx->decomp, acomp_ctx->acomp)) { -pr_err ("could not alloc crypto acomp_request %s\n" , - pool->tfm_name); - goto fail; +if (zswap_acomp_req_init (&acomp_ctx->comp.areq, acomp_ctx->acomp)) + goto req_fail; + +/* Skip the per-CPU request when decompression can use the stack. */ +if (acomp_is_async (acomp_ctx->acomp)) { +if (zswap_acomp_req_init (&acomp_ctx->decomp, acomp_ctx->acomp)) + goto req_fail; }return 0 ; +req_fail: +pr_err ("could not alloc crypto acomp_request %s\n" , pool->tfm_name); fail:acomp_ctx_free (acomp_ctx);return ret;
@@ -956,19 +965,14 @@ static bool zswap_compress(struct folio *folio, long index,return comp_ret ==0 && alloc_ret ==0 ; } -static bool zswap_decompress (struct zswap_entry *entry,struct folio *folio) +static bool __zswap_decompress (struct zswap_entry *entry, +struct zswap_pool *pool,struct acomp_req *req, +struct crypto_wait *wait,struct folio *folio) { -struct zswap_pool *pool =zswap_entry_pool (entry);struct scatterlist input[2 ];/* zsmalloc returns an SG list 1-2 entries */ struct scatterlist output; -struct crypto_acomp_ctx *acomp_ctx;int ret =0 , dlen; -if (WARN_ON_ONCE (!pool)) -return false; - - acomp_ctx =raw_cpu_ptr (pool->acomp_ctx); -mutex_lock (&acomp_ctx->decomp.mutex);zs_obj_read_sg_begin (pool->zs_pool, entry->handle, input, entry->length);/* zswap entries of length PAGE_SIZE are not compressed. */
@@ -985,15 +989,13 @@ static bool zswap_decompress(struct zswap_entry *entry, struct folio *folio) }else {sg_init_table (&output,1 );sg_set_folio (&output, folio,PAGE_SIZE ,0 ); -acomp_request_set_params (acomp_ctx->decomp.req, input, &output, - entry->length,PAGE_SIZE ); - ret =crypto_acomp_decompress (acomp_ctx->decomp.req); - ret =crypto_wait_req (ret, &acomp_ctx->decomp.wait); - dlen = acomp_ctx->decomp.req->dlen; +acomp_request_set_params (req, input, &output, entry->length,PAGE_SIZE ); + ret =crypto_acomp_decompress (req); + ret =crypto_wait_req (ret, wait); + dlen = req->dlen; }zs_obj_read_sg_end (pool->zs_pool, entry->handle); -mutex_unlock (&acomp_ctx->decomp.mutex);if (!ret && dlen ==PAGE_SIZE )return true;
@@ -1007,6 +1009,35 @@ static bool zswap_decompress(struct zswap_entry *entry, struct folio *folio)return false; } +static bool zswap_decompress (struct zswap_entry *entry,struct folio *folio) +{ +struct zswap_pool *pool =zswap_entry_pool (entry); +struct crypto_acomp_ctx *acomp_ctx; +bool ret; + +if (WARN_ON_ONCE (!pool)) +return false; + + acomp_ctx =raw_cpu_ptr (pool->acomp_ctx); +/* +* Use a private on-stack request when the transform supports it. +* Otherwise serialize decompression on the shared per-CPU request. +*/ +if (!acomp_ctx->decomp.req) { +ACOMP_REQUEST_ON_STACK (req, acomp_ctx->acomp); +DECLARE_CRYPTO_WAIT (wait); + +zswap_set_acomp_req_callback (req, &wait); +return __zswap_decompress (entry, pool, req, &wait, folio); + } + +mutex_lock (&acomp_ctx->decomp.mutex); + ret =__zswap_decompress (entry, pool, acomp_ctx->decomp.req, + &acomp_ctx->decomp.wait, folio); +mutex_unlock (&acomp_ctx->decomp.mutex); +return ret; +} +/********************************* * writeback code **********************************/
Signature · 1 line
2.53.0-Meta
Re: [PATCH v3 3/3] mm: zswap: use stack requests for synchronous decompression
Quoted 20 lines · Usama Arif
With separate requests for compression and decompression, loads still serialize on the per-CPU decompression mutex. A low-priority load that is preempted after the codec drops its stream lock keeps holding the mutex and stalls every other load on that CPU, including higher-priority ones.
Synchronous algorithms can use an on-stack request, so decompress with one and take no zswap lock. All in-tree software compressors qualify. The Crypto API enforces
MAX_SYNC_COMP_REQSIZEduring initialization, so checkingacomp_is_async()is sufficient. Asynchronous algorithms keep the per-CPU request and mutex, which is still taken before the zsmalloc read lock.Reading the per-CPU context without the mutex is safe. Since commit
ef3c0f6cb798e("mm: zswap: tie per-CPUacomp_ctxlifetime to the pool"), it is set up before its CPU comes online and is not torn down until the pool is destroyed. The codecs keep their own stream locks, andcrypto_acomp_decompress()rejects on-stack requests only for asynchronous transforms, which never take this path.
Quoted 7 lines
For software compressors this drops the heap request added for decompression. The on-stack request and wait take 216 bytes, which makes the load path about 270 bytes deeper on x86-64. Asynchronous algorithms pay this too.
Acked-by Nhat Pham <[email protected]>Signed-off-by Usama Arif <[email protected]>