DPDK-dev Archive on lore.kernel.org
 help / color / mirror / Atom feed
From: Stephen Hemminger <stephen@networkplumber.org>
To: Konstantin Ananyev <konstantin.ananyev@huawei.com>
Cc: <dev@dpdk.org>, <mattias.ronnblom@ericsson.com>
Subject: Re: [RFC v2] memtank: add memtank library
Date: Wed, 5 Aug 2026 08:37:04 -0700	[thread overview]
Message-ID: <20260805083704.005d1c76@phoenix.local> (raw)
In-Reply-To: <20260805123305.90059-1-konstantin.ananyev@huawei.com>

On Wed, 5 Aug 2026 13:33:01 +0100
Konstantin Ananyev <konstantin.ananyev@huawei.com> wrote:

> v2:
> - Addressed majority of AI review comments (except 11, 12,14):
>   https://patchwork.dpdk.org/project/dpdk/patch/20260610103918.96857-1-konstantin.ananyev@huawei.com/#185014
> - Still missing integration/common tests with fastmem RFC:
>   https://patchwork.dpdk.org/project/dpdk/list/?series=38273
> 
> Introduce memtank, highly customizable fixed sized object allocator
> for DPDK applications. It offers close to the mempool level performance
> on the fast path.
> Main difference with the mempool is the ability to grow/shrink at runtime
> with user provided grow/shirnk threshold values, plus some extra
> features for higher flexibility.
> 
> Key properties:
> 
> - relies on user to provide callbacks for actual memory reservations.
>   User is free to choose whatever is most suitable way for his scenario,
>   i.e: via malloc/rte_malloc/mmap/some custom memory allocator.
> - user defined constructor callback for newly allocated objects.
> - bulk alloc and free APIs.
> - different alloc/free policies (specified by user via flags parameter):
>   * lightweight as possible, but can fail
>   * more robust, but heavyweight - causes call to user-provided backing
>     memory allocator.
> - backing memory grows/shrinks on demand, special API extensions
>   to allow user control grow/shrink size, frequency and when/where it
>   is going to happen (DP, CP, both, etc.).
> - ability to pre-allocate all objects at memtank creation time
>   (mempool like behavior).
> - custom object size and alignment.
> - per object runtime statistics and sanity-checks (boundary violation,
>   double free, etc.) can be enabled/disabled at memtank creation time.
> 
> Known limitations (subject for further improvements):
> 
> - scalability:
>   after 8+ lcores conventional mempool (with FIFO) starts to outperform
>   memtank (which uses LIFO inside).
> - mempool_cache integration is not part of the library and right now
>   has to be implemented by used manually on top of memtank API.
> 
> Envisioned usage scenarios within DPDK-based apps:
> various flow/session control structures (TCP PCB, CT, NAT sessions, etc.)
> that needs to be allocated/freed at the data-path.
> Also can be used by 'semi-fastpath' allocations:
> TBL-8 blocks for LPM, hash buckets, etc.
> 
> Initial idea is inspired by Linux/Solaris SLAB allocators.
> Also re-used some ideas from my previous work for TLDK project:
> https://github.com/FDio/tldk
> Signed-off-by: Konstantin Ananyev <konstantin.ananyev@huawei.com>
> ---

More detailed AI review (Claude Opus 5):

Review of [RFC v2] memtank: add memtank library

Errors

lib/memtank/memtank.h
  TAILQ_ENTRY/TAILQ_HEAD and friends are used without including
  <sys/queue.h>.  This only compiles because rte_common.h pulls in
  rte_os.h, and only the Linux and FreeBSD rte_os.h include <sys/queue.h>.
  The Windows rte_os.h does not; it only defines the RTE_TAILQ_* wrappers.
  As written the library will not build on Windows, and nothing in
  lib/memtank/meson.build excludes it there.
  Use RTE_TAILQ_HEAD(), RTE_TAILQ_ENTRY(), RTE_TAILQ_FIRST() and
  RTE_TAILQ_NEXT() as the rest of DPDK does, and add an explicit
  #include <sys/queue.h> for the macros that have no RTE_ wrapper
  (TAILQ_INIT, TAILQ_REMOVE, TAILQ_INSERT_*, TAILQ_CONCAT, TAILQ_LAST).

lib/memtank/memtank.h
  struct memtank_free ends in a flexible array member and is then embedded
  as struct rte_memtank::mtf.  C11 6.7.2.1p3 forbids a structure containing
  a flexible array member from being a member of another structure.  GCC and
  clang accept it as an extension, MSVC warns (C4200).  Either move the
  free[] array out of the struct and store a pointer plus the trailing
  allocation, or make memtank_free the outer object.

lib/memtank/rte_memtank.h
  The object constructor callback returns void:

      void (*init)(void *obj[], uint32_t num, void *udata);

  Object construction can fail (sub-allocation, lock init, device handle),
  and there is no way for the user to report that.  init() is also called
  from init_chunk() with no error path back to grow_chunk().  Give it an
  int return and propagate the failure out of rte_memtank_grow() /
  rte_memtank_chunk_alloc().

  Related: there is no destructor callback to pair with init().  Objects
  constructed in init_chunk() are silently discarded when _shrink_chunk()
  or rte_memtank_destroy() hands the raw chunk back to prm.free(), so any
  per-object resource the constructor acquired is leaked.  For a library
  whose selling point is runtime shrink, this needs an answer.

lib/memtank/memtank.c
  rte_memtank_grow() and rte_memtank_shrink() read mt->mtf.nb_free without
  holding mtf.lock:

      k = t->min_free - t->nb_free;
      ...
      if (t->nb_free < t->max_free)

  nb_free is written under that lock in get_free()/put_free().  This is a
  data race; the effect is benign (a threshold heuristic) but it is
  undefined behaviour and TSan will flag it.  Either take the lock for the
  read or make nb_free an RTE_ATOMIC() with a relaxed load.

lib/memtank/memtank.c
  rte_memtank_alloc() gates the chunk path on "flags != 0" rather than on
  RTE_MTANK_ALLOC_CHUNK:

      if (n != num && flags != 0)

  RTE_MTANK_ALLOC_CHUNK is defined in the public header and never
  referenced anywhere in lib/.  Any undefined flag bit therefore behaves as
  ALLOC_CHUNK.  Test the flag explicitly, and reject unknown bits.

app/test/test_memtank_stress.c, test_memtank_cleanup()
  The accumulated worker exit codes are discarded:

      rc = 0;
      RTE_LCORE_FOREACH_WORKER(lc) {
              rc |= rte_eal_wait_lcore(lc);
              ...
      }
      ...
      rc = rte_memtank_sanity_check(mt, 0);   /* overwrites */

  A worker that failed its content check during cleanup is reported as
  success.  Should be "rc |=".

app/test/test_memtank_stress.c, test_memtank_worker()
  rc is declared uninitialized and only assigned inside the two
  "if (num != 0)" blocks.  num is rte_rand() % BULK_NUM and can be zero for
  both alloc and free on a given iteration.  If the loop exits after such
  an iteration, the function returns an uninitialized value.  Initialize
  rc = 0.

doc/api/doxy-api-index.md
  [memcpy](@ref rte_memcpy.h) was the last entry in the memory group and
  has no trailing comma.  The new line is appended after it without adding
  one, so the two entries run together in the rendered index.  Add the
  comma to the memcpy line.

doc/guides/prog_guide/memtank_lib.rst
  Markdown triple backticks are used throughout instead of RST inline
  literals, e.g. ```rte_memtank_create()``` and one instance of
  ```RTE_MTANK_FREE_SHRINK````.  RST wants double backticks; as written
  Sphinx emits inline-markup warnings and the extra backticks appear in the
  output.  With -Dwerror this fails the docs build.

  Several bullet continuations are not indented, which terminates the list
  and produces a block quote instead:

      * ```rte_memtank_create()```/... are responsible for
      creation/destroying the memntank.

  Same at the rte_memtank_grow() and rte_memtank_shrink() bullets.

doc/guides/prog_guide/memtank_lib.rst, Create/Destroy example
  The example does not compile and would not work if it did:
   - "sruct user_defined_type;"
   - .obj_size and .obj_align initializers are terminated with ';' instead
     of ','
   - .free = user_define_free, but the function is user_defined_free
   - .max_obj is never set, so it is 0, while .max_free is 1024 * 1024.
     check_param() rejects max_free > max_obj, so rte_memtank_create()
     returns NULL with EINVAL.

Warnings

Process
  No entry added to doc/guides/rel_notes/release_26_11.rst.  A new library
  needs a "New Features" item.

  No MAINTAINERS entry for lib/memtank; devtools/check-maintainers.sh will
  report it as unmaintained.

lib/memtank/memtank.c, misc.c
  The advertised double-free and boundary detection does not cover the
  fast path.  obj_dbg_alloc()/obj_dbg_free() are only called from
  rte_memtank_chunk_alloc()/rte_memtank_chunk_free().  Objects that go out
  through get_free() and come back through put_free() never touch the
  nb_alloc/nb_free counters, so an application that frees the same object
  twice simply gets it into mtf.free[] twice, and both copies still satisfy
  memobj_verify(mo, 1).  The commit message and the doc both claim double
  free detection without qualification.  Either extend the checks to the
  cache path or scope the claim.

lib/memtank/memtank.h
  struct memobj is 32 bytes and is appended to every object unconditionally.
  With RTE_MTANK_OBJ_DBG clear, only the 8-byte chunk pointer is used; the
  two red zones and the dbg counters are pure overhead.  The stated use case
  is "relatively small objects" and flow/session structures, where 32 bytes
  per object is a large tax.  Consider making the layout depend on the flag.

lib/memtank/memtank.c
  nb_chunks uses rte_memory_order_acq_rel on both fetch_add and fetch_sub.
  It is a plain counter with no data published through it; relaxed is
  sufficient and is what rte_memtank_dump()/sanity_check() already use for
  the load.

lib/memtank/memtank.c, put_chunk()
  The only bound on the write into ch->free[] is RTE_ASSERT(), which is
  compiled out unless RTE_ENABLE_ASSERT.  In a normal build a double free,
  or a pointer that is not from this memtank, overruns the chunk's free
  array into object memory with no diagnostic.  Since OBJ_DBG does not
  catch the cache path either (above), there is no configuration in which
  this is reliably detected.

lib/memtank/memtank.c, check_param()
  prm->obj_size is not validated.  A memtank with obj_size 0 is accepted;
  test_memtank.c's create_invalid case relies on that.  Reject 0, or
  document that it is meaningful.

  Also, obj_align must be a non-zero power of two, but rte_memtank_prm's
  Doxygen does not say so; 0 is a natural "don't care" value for a caller
  and is rejected.

app/test/test_memtank_stress.c, update_global_cfg()
  max_obj = wrk_max_obj * rte_lcore_count() = 64 * nb_lcores by default,
  while mtnk_prm.max_free is 32 * BULK_NUM = 1024.  check_param() rejects
  max_free > max_obj, so rte_memtank_create() fails and the stress test
  reports -ENOMEM on any host with fewer than 16 lcores.  Derive max_obj
  from max_free, or clamp.

app/test/test_memtank_stress.c
  - struct memtank_arg arg[RTE_MAX_LCORE] is on the stack; 256 bytes per
    entry, and RTE_MAX_LCORE is configurable well above the default 128.
  - rte_ring_enqueue_bulk() and rte_ring_dequeue_bulk() return values are
    ignored in test_memtank_worker() and test_worker_cleanup().
  - test_memtank_mt() returns early when fill_worker_args() fails partway
    through the loop, leaking the rings created for earlier lcores.
  - parse_opt() lets a later successful option overwrite an earlier
    parse failure, so bad input is silently accepted.

app/test/test_memtank.c
  memtank_alloc_test() and test_memtank_create_invalid() return via
  RTE_TEST_ASSERT_* without calling rte_memtank_destroy(), leaking the
  memtank (and its chunks) on every failure path.

lib/memtank/meson.build
  extra_flags is empty and the foreach over it is dead code.  Drop both.
  sources and headers are short lists; DPDK style puts those on one line.

doc/guides/prog_guide/memtank_lib.rst
  - ".. figure:: img/memtank_internal.svg" should use the wildcard form
    "img/memtank_internal.*".
  - The figure has no caption, and the _figure_memtank-internals label is
    never referenced.
  - The doc calls the two chunk lists USED and FREE; the code calls them
    MC_USED and MC_FULL.  Pick one.
  - The min_free / max_free bullets and the alloc-flag bullets are
    term/description pairs and read better as RST definition lists.
  - Non-ASCII en-dash on the "built-in per object runtime verify" line.
  - Typos: "Same a s mempool", "memntank" (x4), "rte_memetank_destroy",
    "ret_memtank_free", "pooll", "miscelanneous", "cheking", "Aled public
    API", "relased", "shrinked", "statisitics", "maximimum", "thershold",
    "theshold", "udnerlying", "susbystem", "It's internals", "how many
    memory", "by used manually", "As used needs".

doc/guides/prog_guide/img/memtank_internal.svg
  No newline at end of file.  It is also a single 9.8 KB line exported from
  Office, referencing Calibri and Wingdings with private-use glyphs that
  will not render off Windows.

lib/memtank/misc.c
  %zx is used to print uintptr_t values (p[i], align) in mobj_bulk_check().
  Use PRIxPTR.

lib/memtank/rte_memtank.h, memtank.c
  Include ordering: rte_memtank.h has <stdio.h> after the rte_ headers, and
  memtank.c puts the local "memtank.h" before the rte_ headers.  DPDK order
  is system, EAL, DPDK libs, local.

lib/memtank/rte_memtank.h
  Doxygen typos in the public header: "Same a s mempool", "intitialize",
  "insteance", "initialiaze", "A pinter to the file", "concurently"
  (x2), "inconsitency".  These land in the published API docs.
  The commit message has "shirnk".

Info

lib/memtank/memtank.c
  memtank_meta_size(), memchunk_meta_size(), memobj_size() and
  memchunk_size() each declare an unused "static const struct X *p" purely
  to feed sizeof/alignof.  Use the type directly and drop the statics.

  ALIGN_MUL_CEIL(v, mul) is a ceiling divide, but RTE_ALIGN_MUL_CEIL in
  rte_common.h rounds up to a multiple.  Same name, different meaning, in
  the same translation unit.  Rename it, e.g. CEIL_DIV().

  rte_memtank_grow(): "k = t->min_free - t->nb_free; if ((int32_t)k <= 0)"
  is an unsigned subtract cast back to signed.  "if (t->nb_free >=
  t->min_free) return 0;" says the same thing.

  rte_memtank_shrink() only reclaims chunks from the MC_FULL list and never
  drains the free cache, so a memtank whose objects are all parked in
  mtf.free[] will sit at nb_free == max_free and return 0 forever.  The
  limitation is noted in the doc; worth also noting on the API.

  max_chunk = ceil(max_obj / nb_obj_chunk), so the actual object ceiling is
  max_obj rounded up to a chunk boundary.  Document that max_obj is
  approximate.

  rte_memtank_alloc()/free() do not check for a NULL tank while dump() and
  sanity_check() do.  Fine either way, but be consistent and say which in
  the Doxygen.

lib/memtank/misc.c
  mobj_bulk_check(): "k = ((mt->flags & RTE_MTANK_OBJ_DBG) != 0) & fmsk;"
  uses bitwise & on two logical values.

  rte_memtank_dump() prints "\t[USED]={,\n" - stray comma.

  mfree_stat_collect() returns silently when malloc() fails, and the caller
  then prints an all-zero free_stat block as if it were real.

  ptr_cmp() is correct but obscure; qsort on uintptr_t is clearer as
  "(v1 > v2) - (v1 < v2)".

lib/memtank/rte_memtank.h
  RTE_MTANK_DUMP_END is an implementation detail ("first not used power of
  two") exposed in a public enum.  Compute RTE_MTANK_DUMP_ALL from the
  named flags instead.

app/test/test_memtank_stress.c
  check_fill_objs() passes the lcore id into a uint8_t fill pattern, so
  lcores above 255 collide.  Its static rte_spinlock_t dump_lock is never
  rte_spinlock_init()'d; it works because zero is unlocked, but say so or
  initialize it.

app/test/test_memtank.c
  test_free() does "return free(buf);" from a void function.  The comment
  "min_obj is 0 so this is expected to fail" refers to a field that does
  not exist.

No Reviewed-by: this is an RFC and there are open errors above.

General comment on direction: the design is sound and the chunk/LIFO split
is clean, but the two things that most need settling before a non-RFC
posting are (1) the init callback contract - a void constructor with no
destructor cannot support the flow/session use case the cover letter aims
at, and (2) what the OBJ_DBG mode actually guarantees, given that the fast
path bypasses it entirely.

      reply	other threads:[~2026-08-05 15:37 UTC|newest]

Thread overview: 2+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
     [not found] <0260610103918.96857-1-konstantin.ananyev@huawei.com>
2026-08-05 12:33 ` [RFC v2] memtank: add memtank library Konstantin Ananyev
2026-08-05 15:37   ` Stephen Hemminger [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=20260805083704.005d1c76@phoenix.local \
    --to=stephen@networkplumber.org \
    --cc=dev@dpdk.org \
    --cc=konstantin.ananyev@huawei.com \
    --cc=mattias.ronnblom@ericsson.com \
    /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