[PATCH] Dynamic growable arrays for internal use

Florian Weimer fweimer@redhat.com
Tue May 30 12:48:00 GMT 2017


On 05/20/2017 04:01 AM, Carlos O'Donell wrote:

> Why would someone choose to use dynarray instead of a scratch buffer?

Scratch buffers hard-coded to 1000-something bytes of on-stack storage.
This can be a problem if you need several (usually small) arrays.  The
on-stack allocation is configurable for dynamic arrays.

Scratch buffers are geared towards untyped byte sequences, not arrays of
a single type.

Scratch buffers are generally not expected to preserve their contents
when resized.

Some existing scratch buffer users probably should switch to dynarray
once we have that functionality.  scratch_buffer_set_array_size is a bit
of a hack because I didn't want to create the full array functionality
at that time.  I expect that this function can eventually go away (and
so should scratch_buffer_grow_preserve), so that scratch buffers are
strictly for NSS-style buffers.  At that point, we can use a more
aggressive resizing policy (because retries are so expensive).

> Can dynarray guarantee it is only heap allocated to make it easier to use
> in certain stack-limited cases? While scratch-buffers are designed to be stack
> allocated with heap fallback?

By default, at least two array elements are stack-allocated, and up to
128 bytes of stack space are used.  This can be configured through
DYNARRAY_INITIAL_SIZE.

> The tests while thorough need much more comments explaining the purpose of each
> test and the iterations being done by the tests. For example for each sequence
> of nested for loops we need an outer comment to describe exactly what is being
> tested and why that is important.

I added more comments in the attached patch.

> The initial dynarray size makes the API feel like a variant of scratch buffer.
> Why have an initial inline size that can be allocated on the stack? I'd like to
> avoid stack allocations for dynarray's and make them completely heap-based.
> Does that make sense from a design perspective? It would make them an easy
> choice when you want to avoid stack based allocations.

Most arrays are small.  A small stack-allocated array will allow us to
create the final array with the correct size for such small arrays.  We
already need three words for the dynarray header.  I could reduce the
default stack allocation size to something in the same ballpark (say
five words) if on-stack size is a concern.

> The dynamic array growth factor appears to be a value of 2, and that doesn't
> match best practice nor academic literature. I would be happier with a growth
> factor closer to 1.5.

I used Paul's suggestion of (1.5 * N + 1).

I could not find any relevant discussion of growth factors.

> Review of low level details:
> 
> tst-dynarray-fail - Takes on average 40s on an "average" machine.
> 		    The timeout needs to be bumped up to 50s. I see ~93% of the
> 		    time in _dl_addr because the free hook uses that to
> 		    lookup information and that's very expensive.

I set a timeout of 90 seconds.  For me, it completes much, much faster
for some reason.

> malloc/dynarray_at_failure.c - Must use __snprintf to avoid PLT. Has slight
> 			       formatting issue.

Fixed.

>> +#ifdef DYNARRAY_INITIAL_SIZE
>> +# if DYNARRAY_INITIAL_SIZE < 0
>> +#  error "DYNARRAY_INITIAL_SIZE must be non-negative"
>> +# endif
>> +# if DYNARRAY_INITIAL_SIZE > 0
>> +#  define DYNARRAY_HAVE_SCRATCH 1
>> +# else
>> +#  define DYNARRAY_HAVE_SCRATCH 0
>> +# endif
>> +#else
>> +/* Provide a reasonable default which limits the size of
>> +   DYNARRAY_STRUCT.  */
>> +# define DYNARRAY_INITIAL_SIZE \
>> +  (sizeof (DYNARRAY_ELEMENT) > 64 ? 2 : 128 / sizeof (DYNARRAY_ELEMENT))
>> +# define DYNARRAY_HAVE_SCRATCH 1
> 
> Not OK.
> 
> I dont like defaults for this interface. I think the user should
> explicitly request a size or this should fail. It avoids macro API typos
> which intend to define DYNARRAY_INITIAL_SIZE but don't, and then a default
> applies.
> 
> Why have a default at all though?

Convenience.  This has to be a preprocessor constant, so that we can
make the presence of the scratch array inside the dynarray struct
conditional on the initial size value.

I do think we need some defaults here because without them, new features
we add will eventually need patching all over the place.  This would be
very hostile to backporting in particular.

> Can we make the choice to have dynarray always heap allocated?

#define DYNARRAY_INITIAL_SIZE 0

does that.  I clarified that in the file comment.

> It would make this simpler, you just never have an initial scratch.
> 
> If you need scratch space use growable scratch buffers?

See above why scratch buffers can be a poor fit.

>> +/* Remove the last element of LIST if it is present.  */
>> +__attribute__ ((unused))
>> +static void
>> +DYNARRAY_NAME (remove_last) (struct DYNARRAY_STRUCT *list)
>> +{
>> +  /* We deliberately skip error checking here.  */
> 
> Why do you deliberatly skip error checking?

I updated the comment.

>> +/* Remove all elements from the list.  The elements are freed, but the
>> +   list itself is not.  */
>> +__attribute__ ((unused))
>> +static void
>> +DYNARRAY_NAME (clear) (struct DYNARRAY_STRUCT *list)
>> +{
>> +  /* We deliberately skip error checking here.  */
> 
> Why?

Likewise.

>> +#ifdef DYNARRAY_FINAL_TYPE
>> +/* Transfer the dynamic array to a permanent location at *RESULT.
>> +   Returns true on success on false on allocation failure.  In either
>> +   case, *LIST is re-initialized and can be reused.  A NULL pointer is
>> +   stored in *RESULT if LIST refers to an empty list.  On success, the
>> +   pointer in *RESULT is heap-allocated and must be deallocated using
>> +   free.  */
>> +__attribute__ ((unused, warn_unused_result))
>> +static bool
>> +DYNARRAY_NAME (finalize) (struct DYNARRAY_STRUCT *list,
>> +                          DYNARRAY_FINAL_TYPE *result)
>> +{
>> +  struct dynarray_finalize_result res;
>> +  if (__libc_dynarray_finalize (&list->dynarray_abstract,
>> +                                DYNARRAY_SCRATCH (list),
>> +                                sizeof (DYNARRAY_ELEMENT), &res))
>> +    {
>> +      /* On success, the result owns all the data.  */
>> +      DYNARRAY_NAME (init) (list);
>> +      *result = (DYNARRAY_FINAL_TYPE) { res.array, res.length };
> 
> Can we do a named structure member assignment here just for clarity and safety?

This would force specific names for this user-supplied structure.  I did
not want to do that.

>> +   A minimal example which provides a growing list of integers can be
>> +   defined like this:
>> +
>> +     struct int_array
>> +     {
>> +       int *array;
>> +       size_t length;
>> +     };
> 
> Please enhance this example to explain tha the layout of the final
> type has to have ceratain members in a particular order.

I added a comment.

>> +    /* Double the allocated size.  */
>> +    {
>> +      new_allocated = 2 * list->allocated;
> 
> Not OK. Please review academic literature on growth factor values.

See above.

>> +  size_t allocation_size = used * element_size;
>> +  void *heap_array = malloc (allocation_size);
>> +  if (heap_array != NULL)
>> +    {
>> +      /* The new array takes ownership of the strings.  */
>> +      if (list->array != NULL)
>> +        memcpy (heap_array, list->array, allocation_size);
>> +      if (list->array != scratch)
>> +        free (list->array);
>> +      *result = (struct dynarray_finalize_result) { heap_array, used };
> 
> Can we use named structure member assignment?

Fixed.

>> +static void
>> +check_stream (const char *what, const struct xmemstream *stream,
>> +              const char *expected)
>> +{
>> +  if (strcmp (stream->buffer, expected) != 0)
>> +    {
>> +      support_record_failure ();
>> +      printf ("error: captured %s data incorrect\n"
>> +              "  expected: %s\n"
>> +              "  actual:   %s\n",
>> +              what, expected, stream->buffer);
>> +    }
>> +  if (stream->length != strlen (expected))
>> +    {
>> +      support_record_failure ();
>> +      printf ("error: captured %s data length incorrect\n"
>> +              "  expected: %zu\n"
>> +              "  actual:   %zu\n",
>> +              what, strlen (expected), stream->length);
>> +    }
>> +}
>> +
>> +static int
>> +do_test (void)
>> +{
>> +  const int lengths[] = {0, 1, 17, 512, 20000, -1};
>> +
>> +  for (int i = 0; lengths[i] >= 0; ++i)
>> +    for (int j = 0; lengths[j] >= 0; ++j)
>> +      for (int write_mode = 0; write_mode < write_mode_last; ++write_mode)
>> +        for (int signal = 0; signal < 2; ++signal)
>> +          for (int status = 0; status < 2; ++status)
> 
> This needs detailed comments.

Fixed.

The attached patch adds a new add function for arrays, which is
convenient to construct arrays with simple elements (such as
NUL-terminated strings).  For arrays which have sentinels when completed
(such as NUL-terminated strings and argv lists), the existing finalize
interface with the type definition is overkill, so I added a default
implementation which directly returns the heap-allocated pointer.

In addition, I added a few nonnull attributes and told the compiler to
inline the fast path for emplace (and the new add function).

Thanks,
Florian
-------------- next part --------------
A non-text attachment was scrubbed...
Name: dynarray.patch
Type: text/x-patch
Size: 95284 bytes
Desc: not available
URL: <http://sourceware.org/pipermail/libc-alpha/attachments/20170530/de49c379/attachment.bin>


More information about the Libc-alpha mailing list