diff mbox series

[FFmpeg-devel,v2,11/17] avutil/mem: Add av_fast_realloc_array()

Message ID 20200101132758.4452-1-andreas.rheinhardt@gmail.com
State New
Headers show
Series None | expand

Commit Message

Andreas Rheinhardt Jan. 1, 2020, 1:27 p.m. UTC
This is an array-equivalent of av_fast_realloc(). Its advantages
compared to using av_fast_realloc() for allocating arrays are as
follows:

a) It performs its own overflow checks for the multiplication that is
implicit in array allocations. (And it only needs to perform these
checks (as well as the multiplication itself) in case the array needs to
be reallocated.)
b) It allows to limit the number of elements to an upper bound given
by the caller. This allows to restrict the number of allocated elements
to fit into an int and therefore makes this function usable with
counters of this type. It can also be used to avoid overflow checks in
the caller: E.g. setting it to UINT_MAX - 1 elements makes it safe to
increase the desired number of elements in steps of one. And it avoids
overallocations in situations where one already has an upper bound.
c) Should the caller have increased max_alloc_size, av_fast_realloc()
could come in a situation where it allocates more than what fits into an
unsigned. In this case the variable containing the allocated size (an
unsigned) won't accurately reflect how much has been allocated. After
this point, lots of reallocations will happen despite the buffer
actually being big enough.
d) av_fast_realloc_array() will always allocate in multiples of array
elements; no memory is wasted with partial elements.
e) By returning an int, av_fast_realloc_array() can distinguish between
ordinary allocation failures (meriting AVERROR(ENOMEM)) and failures
because of allocation limits (by returning AVERROR(ERANGE)).
f) It is no longer possible for the user to accidentally lose the
pointer by using ptr = av_fast_realloc(ptr, ...).

Because of f) there is no need to set the number of allocated elements
to zero on failure.

av_fast_realloc() usually allocates size + size / 16 + 32 bytes if size
bytes are desired and if the already existing buffer isn't big enough.
av_fast_realloc_array() instead allocates nb + (nb + 14) / 16. Rounding
up is done in order not to reallocate in steps of one if the current
number is < 16; adding 14 instead of 15 has the effect of only
allocating one element if one element is desired. This is done with an
eye towards applications where arrays might commonly only contain one
element (as happens with the Matroska CueTrackPositions).

Which of the two functions allocates faster depends upon the size of
the elements. E.g. if the elements have a size of 32B and the desired
size is incremented in steps of one, allocations happen at
1, 3, 5, 7, 9, 11, 13, 15, 17, 20, 23, 26 ... for av_fast_realloc(),
1, 2, 4, 6, 8, 10, 12, 14, 16, 18, 21, 24 ... for
av_fast_realloc_array(). For element sizes of 96B, the numbers are
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 21, 23, 25, 27, 30 ...
for av_fast_realloc() whereas the pattern for av_fast_realloc_array() is
unchanged.

Signed-off-by: Andreas Rheinhardt <andreas.rheinhardt@gmail.com>
---
Switched to returning an int and added the max_nb parameter. What has
not been done is switching to size_t. This function can still be turned
into a wrapper for a size_t function if the need for such a function
arises.

Furthermore, I have found that several reallocation functions don't
abide by their documented behaviour: "If `size` is zero, free the memory
block pointed to by `ptr`." says the documentation of av_realloc,
av_reallocp, av_realloc_f (implicitly); av_realloc_array and
av_reallocp_array claim to free the memory block in case the number of
elements to allocate is zero.

Yet realloc allocates size + !size bytes. av_realloc_f (calling
av_realloc) does also not free its array in case zero elements should be
allocated (or if the size of an element is zero). av_reallocp does what
its documentation says; av_realloc_array (relying on av_realloc) and
av_reallocp_array (relying on av_realloc_f) don't.

Changing the behaviour of av_realloc to match its documentation leads to
lots of failing FATE-tests, so I suggest updating the documentation to
match actual behaviour.

Finally, there is no check in av_max_alloc that the new max is actually
bigger than 32; this is a problem given that max_alloc_size - 32 is the
real limit. Maybe the 32 should simply be dropped (and the default value
be set to INT_MAX - 32 if it is deemed important)? (And why 32? I would
have expected AV_INPUT_BUFFER_PADDING_SIZE or so.)

 doc/APIchanges      |  3 +++
 libavutil/mem.c     | 32 ++++++++++++++++++++++++++++++++
 libavutil/mem.h     | 28 ++++++++++++++++++++++++++++
 libavutil/version.h |  2 +-
 4 files changed, 64 insertions(+), 1 deletion(-)

Comments

Nicolas George Jan. 1, 2020, 2:07 p.m. UTC | #1
Andreas Rheinhardt (12020-01-01):
> What has
> not been done is switching to size_t. This function can still be turned
> into a wrapper for a size_t function if the need for such a function
> arises.

I do not agree with that. size_t is the correct type for that use, and
therefore should be the choice for all new code. I remember trouble
about that, let us not make some more.

Regards,
diff mbox series

Patch

diff --git a/doc/APIchanges b/doc/APIchanges
index 3c24dc6fbc..7feca58e98 100644
--- a/doc/APIchanges
+++ b/doc/APIchanges
@@ -15,6 +15,9 @@  libavutil:     2017-10-21
 
 API changes, most recent first:
 
+2020-01-01 - xxxxxxxxxx - lavu 56.39.100 - mem.h
+  Add av_fast_realloc_array().
+
 2019-12-27 - xxxxxxxxxx - lavu 56.38.100 - eval.h
   Add av_expr_count_func().
 
diff --git a/libavutil/mem.c b/libavutil/mem.c
index 88fe09b179..56c82fcee7 100644
--- a/libavutil/mem.c
+++ b/libavutil/mem.c
@@ -497,6 +497,38 @@  void *av_fast_realloc(void *ptr, unsigned int *size, size_t min_size)
     return ptr;
 }
 
+int av_fast_realloc_array(void *ptr, unsigned *nb_allocated,
+                          unsigned min_nb, unsigned max_nb, size_t elsize)
+{
+    void *array;
+    unsigned nb;
+
+    if (min_nb <= *nb_allocated)
+        return 0;
+
+    max_nb = FFMIN(max_nb, (max_alloc_size - 32) / elsize);
+
+    if (min_nb > max_nb)
+        return AVERROR(ERANGE);
+
+    nb = min_nb + (min_nb + 14) / 16;
+
+    /* If min_nb is so big that the above calculation overflowed,
+     * just allocate as much as we are allowed to. */
+    nb = nb < min_nb ? max_nb : FFMIN(nb, max_nb);
+
+    memcpy(&array, ptr, sizeof(array));
+
+    array = av_realloc(array, nb * elsize);
+    if (!array)
+        return AVERROR(ENOMEM);
+
+    memcpy(ptr, &array, sizeof(array));
+    *nb_allocated = nb;
+
+    return 0;
+}
+
 void av_fast_malloc(void *ptr, unsigned int *size, size_t min_size)
 {
     ff_fast_malloc(ptr, size, min_size, 0);
diff --git a/libavutil/mem.h b/libavutil/mem.h
index 5fb1a02dd9..e05550f363 100644
--- a/libavutil/mem.h
+++ b/libavutil/mem.h
@@ -370,11 +370,39 @@  int av_reallocp_array(void *ptr, size_t nmemb, size_t size);
  * @return `ptr` if the buffer is large enough, a pointer to newly reallocated
  *         buffer if the buffer was not large enough, or `NULL` in case of
  *         error
+ * @see av_fast_realloc_array()
  * @see av_realloc()
  * @see av_fast_malloc()
  */
 void *av_fast_realloc(void *ptr, unsigned int *size, size_t min_size);
 
+/**
+ * Reallocate the given array if it is not large enough, otherwise do nothing.
+ *
+ * If `ptr` points to `NULL`, then a new uninitialized array is allocated.
+ *
+ * @param[in,out] ptr           Pointer to `NULL` or an already allocated array.
+ *                              `*ptr` will be set to point to the new array.
+ * @param[in,out] nb_allocated  Pointer to the number of elements of the array
+ *                              `*ptr`. `*nb_allocated` is updated to the new
+ *                              number of allocated elements.
+ * @param[in]     min_nb        Desired minimal number of elements in array `*ptr`
+ * @param[in]     max_nb        Maximal number of elements to allocate.
+ * @param[in]     elsize        Size of a single element of the array.
+ *                              Must not be zero.
+ * @return 0 on success, < 0 on failure. On failure, `*ptr` is not freed and
+ *         `*ptr` as well as `*nb_allocated` are unchanged.
+ * @note `max_nb` can be used to limit allocations and make this function usable
+ *       with counters of type int. It can also be used to avoid overflow checks
+ *       in callers: E.g. setting it to `UINT_MAX - 1` means that incrementing
+ *       an unsigned in steps of one need not be checked for overflow.
+ * @see av_fast_realloc()
+ * @see av_realloc()
+ * @see av_fast_malloc()
+ */
+int av_fast_realloc_array(void *ptr, unsigned *nb_allocated,
+                          unsigned min_nb, unsigned max_nb, size_t elsize);
+
 /**
  * Allocate a buffer, reusing the given one if large enough.
  *
diff --git a/libavutil/version.h b/libavutil/version.h
index af8f614aff..2bc1b98615 100644
--- a/libavutil/version.h
+++ b/libavutil/version.h
@@ -79,7 +79,7 @@ 
  */
 
 #define LIBAVUTIL_VERSION_MAJOR  56
-#define LIBAVUTIL_VERSION_MINOR  38
+#define LIBAVUTIL_VERSION_MINOR  39
 #define LIBAVUTIL_VERSION_MICRO 100
 
 #define LIBAVUTIL_VERSION_INT   AV_VERSION_INT(LIBAVUTIL_VERSION_MAJOR, \