From: "Christian König" <christian.koenig@amd.com>
To: Matthew Brost <matthew.brost@intel.com>,
intel-xe@lists.freedesktop.org, dri-devel@lists.freedesktop.org
Cc: "Thomas Hellström" <thomas.hellstrom@linux.intel.com>,
"Maarten Lankhorst" <maarten.lankhorst@linux.intel.com>,
"Maxime Ripard" <mripard@kernel.org>,
"Thomas Zimmermann" <tzimmermann@suse.de>,
"David Airlie" <airlied@gmail.com>,
"Simona Vetter" <simona@ffwll.ch>,
linux-kernel@vger.kernel.org
Subject: Re: [RFC PATCH] drm/exec: track the first few locked objects inline
Date: Fri, 2 Oct 2026 13:35:35 +0200 [thread overview]
Message-ID: <9b363165-9e1e-43be-bd54-b1c420e047ca@amd.com> (raw)
In-Reply-To: <20261002003816.3233277-1-matthew.brost@intel.com>
On 10/2/26 02:38, Matthew Brost wrote:
> drm_exec_init() always allocates the table of locked objects, a whole
> page of it unless the caller asks for a specific size. Many users only
> ever lock a handful of objects, often just a VM's common dma-resv and
> one BO, yet still pay for an allocation, and any of them can fail with
> -ENOMEM. That is awkward on paths which can't fail, such as dropping
> what may be the last reference of a drm_gpuvm_bo, which needs both the
> VM's and the BO's dma-resv held; drivers end up open coding the ww_mutex
> dance instead.
>
> Keep room for DRM_EXEC_INLINE_OBJECTS (8) objects within struct drm_exec
> itself and track the first locked objects there. Only once more are
> locked does the table move to an allocation, which then grows as before.
> A caller passing nr larger than that still gets an allocated table of
> that size up front, falling back to the inline one should that fail.
>
> As a result, locking up to DRM_EXEC_INLINE_OBJECTS objects never
> allocates memory and cannot fail with -ENOMEM, and callers passing
> nr == 0 no longer allocate a page they mostly don't need. The cost is
> 64 bytes more of struct drm_exec, which typically lives on the stack.
>
> Since objects may now point into the struct itself, a struct drm_exec
> must not be moved or copied after drm_exec_init(); no user does so.
>
> Add a KUnit test which locks one object more than fits inline, checking
> that the table moves out of the struct only then, that every object is
> tracked across the move, and that a large nr skips the inline table.
>
> Cc: Christian König <christian.koenig@amd.com>
> Cc: Thomas Hellström <thomas.hellstrom@linux.intel.com>
> Cc: Maarten Lankhorst <maarten.lankhorst@linux.intel.com>
> Cc: Maxime Ripard <mripard@kernel.org>
> Cc: Thomas Zimmermann <tzimmermann@suse.de>
> Cc: David Airlie <airlied@gmail.com>
> Cc: Simona Vetter <simona@ffwll.ch>
> Cc: dri-devel@lists.freedesktop.org
> Cc: linux-kernel@vger.kernel.org
> Signed-off-by: Matthew Brost <matthew.brost@intel.com>
> Assisted-by: LLM
I Was considering that as well at some point when I found that using an xarray is actually quite a bit slower than just pre-allocating using kmalloc.
No time to review that in deep, but feel free to add Acked-by: Christian König <christian.koenig@amd.com>.
Regards,
Christian.
>
> ---
> This came out of a Sashiko review of the two-pass GPUVM series [1],
> which flagged a nouveau cleanup path dropping a drm_gpuvm_bo with
> drm_exec and silently carrying on if locking failed with -ENOMEM.
> Grabbing a small number of locks on paths which cannot fail is quite
> common, and drm_exec should provide a ww transaction wrapper for that
> which can't fail, rather than drivers open coding the ww_mutex dance.
>
> [1] https://sashiko.dev/#/patchset/20261001220632.3190896-1-matthew.brost%40intel.com
> ---
> drivers/gpu/drm/drm_exec.c | 39 +++++++++++++------
> drivers/gpu/drm/tests/drm_exec_test.c | 55 +++++++++++++++++++++++++++
> include/drm/drm_exec.h | 18 ++++++++-
> 3 files changed, 99 insertions(+), 13 deletions(-)
>
> diff --git a/drivers/gpu/drm/drm_exec.c b/drivers/gpu/drm/drm_exec.c
> index 2453ec41360f..bdc9c79359b3 100644
> --- a/drivers/gpu/drm/drm_exec.c
> +++ b/drivers/gpu/drm/drm_exec.c
> @@ -70,19 +70,26 @@ static void drm_exec_unlock_all(struct drm_exec *exec)
> *
> * Initialize the object and make sure that we can track locked objects.
> *
> - * If nr is non-zero then it is used as the initial objects table size.
> - * In either case, the table will grow (be re-allocated) on demand.
> + * The first %DRM_EXEC_INLINE_OBJECTS locked objects are tracked within @exec
> + * itself, so locking no more than that never allocates memory. If nr is larger
> + * than that, it is used as the initial size of an allocated objects table
> + * instead. In either case, the table will grow (be re-allocated) on demand.
> */
> void drm_exec_init(struct drm_exec *exec, u32 flags, unsigned nr)
> {
> - if (!nr)
> - nr = PAGE_SIZE / sizeof(void *);
> + exec->objects = NULL;
> + if (nr > DRM_EXEC_INLINE_OBJECTS)
> + exec->objects = kvmalloc_objs(*exec->objects, nr);
>
> - exec->flags = flags;
> - exec->objects = kvmalloc_objs(*exec->objects, nr);
> + /* If allocation here fails, just delay that till it is needed */
> + if (exec->objects) {
> + exec->max_objects = nr;
> + } else {
> + exec->objects = exec->inline_objects;
> + exec->max_objects = DRM_EXEC_INLINE_OBJECTS;
> + }
>
> - /* If allocation here fails, just delay that till the first use */
> - exec->max_objects = exec->objects ? nr : 0;
> + exec->flags = flags;
> exec->num_objects = 0;
> exec->contended = DRM_EXEC_DUMMY;
> exec->prelocked = NULL;
> @@ -99,7 +106,8 @@ EXPORT_SYMBOL(drm_exec_init);
> void drm_exec_fini(struct drm_exec *exec)
> {
> drm_exec_unlock_all(exec);
> - kvfree(exec->objects);
> + if (exec->objects != exec->inline_objects)
> + kvfree(exec->objects);
> if (exec->contended != DRM_EXEC_DUMMY) {
> drm_gem_object_put(exec->contended);
> ww_acquire_fini(&exec->ticket);
> @@ -140,9 +148,16 @@ static int drm_exec_obj_locked(struct drm_exec *exec,
> {
> if (unlikely(exec->num_objects == exec->max_objects)) {
> size_t size = exec->max_objects * sizeof(void *);
> - void *tmp;
> -
> - tmp = kvrealloc(exec->objects, size + PAGE_SIZE, GFP_KERNEL);
> + struct drm_gem_object **tmp;
> +
> + if (exec->objects == exec->inline_objects) {
> + tmp = kvmalloc(size + PAGE_SIZE, GFP_KERNEL);
> + if (tmp)
> + memcpy(tmp, exec->objects, size);
> + } else {
> + tmp = kvrealloc(exec->objects, size + PAGE_SIZE,
> + GFP_KERNEL);
> + }
> if (!tmp)
> return -ENOMEM;
>
> diff --git a/drivers/gpu/drm/tests/drm_exec_test.c b/drivers/gpu/drm/tests/drm_exec_test.c
> index 7a374e462348..434ab9c59878 100644
> --- a/drivers/gpu/drm/tests/drm_exec_test.c
> +++ b/drivers/gpu/drm/tests/drm_exec_test.c
> @@ -204,6 +204,60 @@ static void test_multiple_loops(struct kunit *test)
> KUNIT_SUCCEED(test);
> }
>
> +static void test_inline_objects(struct kunit *test)
> +{
> + struct drm_exec_priv *priv = test->priv;
> + const unsigned int count = DRM_EXEC_INLINE_OBJECTS + 1;
> + DECLARE_BITMAP(seen, DRM_EXEC_INLINE_OBJECTS + 1) = {};
> + struct drm_gem_object *gobj, *obj;
> + struct drm_exec exec;
> + unsigned int i;
> + int ret = 0;
> +
> + gobj = kunit_kcalloc(test, count, sizeof(*gobj), GFP_KERNEL);
> + KUNIT_ASSERT_NOT_NULL(test, gobj);
> +
> + for (i = 0; i < count; i++)
> + drm_gem_private_object_init(priv->drm, &gobj[i], PAGE_SIZE);
> +
> + drm_exec_init(&exec, DRM_EXEC_INTERRUPTIBLE_WAIT, 0);
> + KUNIT_EXPECT_PTR_EQ(test, exec.objects, &exec.inline_objects[0]);
> + drm_exec_until_all_locked(&exec) {
> + for (i = 0; i < count; i++) {
> + ret = drm_exec_lock_obj(&exec, &gobj[i]);
> + drm_exec_retry_on_contention(&exec);
> + if (ret)
> + break;
> +
> + /* Only spills out of the struct past the inline ones */
> + if (exec.num_objects <= DRM_EXEC_INLINE_OBJECTS)
> + KUNIT_EXPECT_PTR_EQ(test, exec.objects,
> + &exec.inline_objects[0]);
> + else
> + KUNIT_EXPECT_PTR_NE(test, exec.objects,
> + &exec.inline_objects[0]);
> + }
> + }
> + KUNIT_EXPECT_EQ(test, ret, 0);
> + KUNIT_EXPECT_EQ(test, exec.num_objects, count);
> + /* Contention may reorder them, but each must be tracked once */
> + drm_exec_for_each_locked_object(&exec, obj) {
> + KUNIT_ASSERT_TRUE(test, obj >= gobj && obj < gobj + count);
> + KUNIT_EXPECT_FALSE(test, test_and_set_bit(obj - gobj, seen));
> + }
> + KUNIT_EXPECT_TRUE(test, bitmap_full(seen, count));
> + drm_exec_fini(&exec);
> +
> + /* A larger initial size skips the inline objects altogether */
> + drm_exec_init(&exec, DRM_EXEC_INTERRUPTIBLE_WAIT, count);
> + KUNIT_EXPECT_PTR_NE(test, exec.objects, &exec.inline_objects[0]);
> + KUNIT_EXPECT_EQ(test, exec.max_objects, count);
> + drm_exec_fini(&exec);
> +
> + for (i = 0; i < count; i++)
> + drm_gem_private_object_fini(&gobj[i]);
> +}
> +
> static struct kunit_case drm_exec_tests[] = {
> KUNIT_CASE(sanitycheck),
> KUNIT_CASE(test_lock),
> @@ -212,6 +266,7 @@ static struct kunit_case drm_exec_tests[] = {
> KUNIT_CASE(test_prepare),
> KUNIT_CASE(test_prepare_array),
> KUNIT_CASE(test_multiple_loops),
> + KUNIT_CASE(test_inline_objects),
> {}
> };
>
> diff --git a/include/drm/drm_exec.h b/include/drm/drm_exec.h
> index cc2937185a9f..ddbdd745efd9 100644
> --- a/include/drm/drm_exec.h
> +++ b/include/drm/drm_exec.h
> @@ -15,6 +15,12 @@
> */
> #define DRM_EXEC_DUMMY ((void *)~0)
>
> +/*
> + * Number of locked objects tracked inside &struct drm_exec itself. Locking at
> + * most this many objects never allocates memory for tracking them.
> + */
> +#define DRM_EXEC_INLINE_OBJECTS 8
> +
> struct drm_gem_object;
>
> /**
> @@ -42,7 +48,8 @@ struct drm_exec {
> unsigned int max_objects;
>
> /**
> - * @objects: array of the locked objects
> + * @objects: array of the locked objects, either @inline_objects or
> + * allocated once more objects need tracking than it can hold
> */
> struct drm_gem_object **objects;
>
> @@ -55,6 +62,15 @@ struct drm_exec {
> * @prelocked: already locked GEM object due to contention
> */
> struct drm_gem_object *prelocked;
> +
> + /**
> + * @inline_objects: storage for the first %DRM_EXEC_INLINE_OBJECTS
> + * locked objects, so that locking only a few objects does not need to
> + * allocate memory, and therefore cannot fail with -ENOMEM. As @objects
> + * may point here, a &struct drm_exec must not be moved or copied after
> + * drm_exec_init().
> + */
> + struct drm_gem_object *inline_objects[DRM_EXEC_INLINE_OBJECTS];
> };
>
> /**
prev parent reply other threads:[~2026-10-02 11:35 UTC|newest]
Thread overview: 2+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-02 0:38 Matthew Brost
2026-10-02 11:35 ` Christian König [this message]
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=9b363165-9e1e-43be-bd54-b1c420e047ca@amd.com \
--to=christian.koenig@amd.com \
--cc=airlied@gmail.com \
--cc=dri-devel@lists.freedesktop.org \
--cc=intel-xe@lists.freedesktop.org \
--cc=linux-kernel@vger.kernel.org \
--cc=maarten.lankhorst@linux.intel.com \
--cc=matthew.brost@intel.com \
--cc=mripard@kernel.org \
--cc=simona@ffwll.ch \
--cc=thomas.hellstrom@linux.intel.com \
--cc=tzimmermann@suse.de \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®