shared: add nm_utils_get_next_realloc_size() helper

When growing a buffer by appending a previously unknown number
of elements, the often preferable strategy is growing it exponentially,
so that the amortized runtime and re-allocation costs scale linearly.
GString just always increases the buffer length to the next power of
two. That works.

I think there is value in trying to find an optimal next size. Because
while it doesn't matter in terms of asymptotic behavior, in practice
a better choice should make a difference. This is inspired by what QT
does ([1]), to take more care when growing the buffers:

  - QString allocates 4 characters at a time until it reaches size 20.
  - From 20 to 4084, it advances by doubling the size each time. More
    precisely, it advances to the next power of two, minus 12. (Some memory
    allocators perform worst when requested exact powers of two, because
    they use a few bytes per block for book-keeping.)
  - From 4084 on, it advances by blocks of 2048 characters (4096 bytes).
    This makes sense because modern operating systems don't copy the entire
    data when reallocating a buffer; the physical memory pages are simply
    reordered, and only the data on the first and last pages actually needs
    to be copied.

Note that a QT is talking about 12 characters, so we use 24 bytes
head room.

[1] https://doc.qt.io/qt-5/containers.html#growth-strategies
This commit is contained in:
Thomas Haller 2020-02-25 10:46:45 +01:00
parent d51be7e963
commit 04d0d1bbe5
3 changed files with 222 additions and 0 deletions

View file

@ -84,6 +84,75 @@ nm_ip_addr_set_from_untrusted (int addr_family,
/*****************************************************************************/
gsize
nm_utils_get_next_realloc_size (gboolean true_realloc, gsize requested)
{
gsize n, x;
/* https://doc.qt.io/qt-5/containers.html#growth-strategies */
if (requested <= 40) {
/* small allocations. Increase in small steps of 8 bytes.
*
* We get thus sizes of 8, 16, 32, 40. */
if (requested <= 8)
return 8;
if (requested <= 16)
return 16;
if (requested <= 32)
return 32;
/* The return values for < 104 are essentially hard-coded, and the choice here is
* made without very strong reasons.
*
* We want to stay 24 bytes below the power-of-two border 64. Hence, return 40 here.
* However, the next step then is already 104 (128 - 24). It's a larger gap than in
* the steps before.
*
* It's not clear whether some of the steps should be adjusted (or how exactly). */
return 40;
}
if ( requested <= 0x2000u - 24u
|| G_UNLIKELY (!true_realloc)) {
/* mid sized allocations. Return next power of two, minus 24 bytes extra space
* at the beginning.
* That means, we double the size as we grow.
*
* With !true_realloc, it means that the caller does not intend to call
* realloc() but instead clone the buffer. This is for example the case, when we
* want to nm_explicit_bzero() the old buffer. In that case we really want to grow
* the buffer exponentially every time and not increment in page sizes of 4K (below).
*
* We get thus sizes of 104, 232, 488, 1000, 2024, 4072, 8168... */
if (G_UNLIKELY (requested > G_MAXSIZE / 2u - 24u))
return G_MAXSIZE;
x = requested + 24u;
n = 128u;
while (n < x) {
n <<= 1;
nm_assert (n > 128u);
}
nm_assert (n > 24u && n - 24u >= requested);
return n - 24u;
}
if (G_UNLIKELY (requested > G_MAXSIZE - 0x1000u - 24u))
return G_MAXSIZE;
/* For large allocations (with !true_realloc) we allocate memory in chunks of
* 4K (- 24 bytes extra), assuming that the memory gets mmapped and thus
* realloc() is efficient by just reordering pages. */
n = ((requested + (0x0FFFu + 24u)) & ~((gsize) 0x0FFFu)) - 24u;
nm_assert (n >= requested);
return n;
}
/*****************************************************************************/
pid_t
nm_utils_gettid (void)
{

View file

@ -1696,6 +1696,10 @@ nm_utils_strdup_reset (char **dst, const char *src)
/*****************************************************************************/
gsize nm_utils_get_next_realloc_size (gboolean true_realloc, gsize requested);
/*****************************************************************************/
typedef enum {
NMU_IFACE_ANY,
NMU_IFACE_KERNEL,

View file

@ -584,6 +584,154 @@ test_string_table_lookup (void)
/*****************************************************************************/
static void
test_nm_utils_get_next_realloc_size (void)
{
static const struct {
gsize requested;
gsize reserved_true;
gsize reserved_false;
} test_data[] = {
{ 0, 8, 8 },
{ 1, 8, 8 },
{ 8, 8, 8 },
{ 9, 16, 16 },
{ 16, 16, 16 },
{ 17, 32, 32 },
{ 32, 32, 32 },
{ 33, 40, 40 },
{ 40, 40, 40 },
{ 41, 104, 104 },
{ 104, 104, 104 },
{ 105, 232, 232 },
{ 232, 232, 232 },
{ 233, 488, 488 },
{ 488, 488, 488 },
{ 489, 1000, 1000 },
{ 1000, 1000, 1000 },
{ 1001, 2024, 2024 },
{ 2024, 2024, 2024 },
{ 2025, 4072, 4072 },
{ 4072, 4072, 4072 },
{ 4073, 8168, 8168 },
{ 8168, 8168, 8168 },
{ 8169, 12264, 16360 },
{ 12263, 12264, 16360 },
{ 12264, 12264, 16360 },
{ 12265, 16360, 16360 },
{ 16360, 16360, 16360 },
{ 16361, 20456, 32744 },
{ 20456, 20456, 32744 },
{ 20457, 24552, 32744 },
{ 24552, 24552, 32744 },
{ 24553, 28648, 32744 },
{ 28648, 28648, 32744 },
{ 28649, 32744, 32744 },
{ 32744, 32744, 32744 },
{ 32745, 36840, 65512 },
{ 36840, 36840, 65512 },
{ G_MAXSIZE - 0x1000u, G_MAXSIZE, G_MAXSIZE },
{ G_MAXSIZE - 25u, G_MAXSIZE, G_MAXSIZE },
{ G_MAXSIZE - 24u, G_MAXSIZE, G_MAXSIZE },
{ G_MAXSIZE - 1u, G_MAXSIZE, G_MAXSIZE },
{ G_MAXSIZE, G_MAXSIZE, G_MAXSIZE },
};
guint i;
for (i = 0; i < G_N_ELEMENTS (test_data) + 5000u; i++) {
gsize requested0;
if (i < G_N_ELEMENTS (test_data))
requested0 = test_data[i].requested;
else {
/* find some interesting random values for testing. */
switch (nmtst_get_rand_uint32 () % 5) {
case 0:
requested0 = nmtst_get_rand_size ();
break;
case 1:
/* values close to G_MAXSIZE. */
requested0 = G_MAXSIZE - (nmtst_get_rand_uint32 () % 12000u);
break;
case 2:
/* values around G_MAXSIZE/2. */
requested0 = (G_MAXSIZE / 2u) + 6000u - (nmtst_get_rand_uint32 () % 12000u);
break;
case 3:
/* values around powers of 2. */
requested0 = (((gsize) 1) << (nmtst_get_rand_uint32 () % 64)) + 6000u - (nmtst_get_rand_uint32 () % 12000u);
break;
case 4:
/* values around 4k borders. */
requested0 = (nmtst_get_rand_size () & ~((gsize) 0xFFFu)) + 30u - (nmtst_get_rand_uint32 () % 60u);
break;
default: g_assert_not_reached ();
}
}
{
const gsize requested = requested0;
const gsize reserved_true = nm_utils_get_next_realloc_size (TRUE, requested);
const gsize reserved_false = nm_utils_get_next_realloc_size (FALSE, requested);
g_assert_cmpuint (reserved_true, >, 0);
g_assert_cmpuint (reserved_false, >, 0);
g_assert_cmpuint (reserved_true, >=, requested);
g_assert_cmpuint (reserved_false, >=, requested);
g_assert_cmpuint (reserved_false, >=, reserved_true);
if (i < G_N_ELEMENTS (test_data)) {
g_assert_cmpuint (reserved_true, ==, test_data[i].reserved_true);
g_assert_cmpuint (reserved_false, ==, test_data[i].reserved_false);
}
/* reserved_false is generally the next power of two - 24. */
if (reserved_false == G_MAXSIZE)
g_assert_cmpuint (requested, >, G_MAXSIZE / 2u - 24u);
else {
g_assert_cmpuint (reserved_false, <=, G_MAXSIZE - 24u);
if (reserved_false >= 40) {
const gsize _pow2 = reserved_false + 24u;
/* reserved_false must always be a power of two minus 24. */
g_assert_cmpuint (_pow2, >=, 64u);
g_assert_cmpuint (_pow2, >, requested);
g_assert (nm_utils_is_power_of_two (_pow2));
/* but _pow2/2 must also be smaller than what we requested. */
g_assert_cmpuint (_pow2 / 2u - 24u, <, requested);
} else {
/* smaller values are hard-coded. */
}
}
/* reserved_true is generally the next 4k border - 24. */
if (reserved_true == G_MAXSIZE)
g_assert_cmpuint (requested, >, G_MAXSIZE - 0x1000u - 24u);
else {
g_assert_cmpuint (reserved_true, <=, G_MAXSIZE - 24u);
if (reserved_true > 8168u) {
const gsize page_border = reserved_true + 24u;
/* reserved_true must always be aligned to 4k (minus 24). */
g_assert_cmpuint (page_border % 0x1000u, ==, 0);
if (requested > 0x1000u - 24u) {
/* page_border not be more than 4k above requested. */
g_assert_cmpuint (page_border, >=, 0x1000u - 24u);
g_assert_cmpuint (page_border - 0x1000u - 24u, <, requested);
}
} else {
/* for smaller sizes, reserved_true and reserved_false are the same. */
g_assert_cmpuint (reserved_true, ==, reserved_false);
}
}
}
}
}
/*****************************************************************************/
NMTST_DEFINE ();
int main (int argc, char **argv)
@ -603,6 +751,7 @@ int main (int argc, char **argv)
g_test_add_func ("/general/test_nm_utils_bin2hexstr", test_nm_utils_bin2hexstr);
g_test_add_func ("/general/test_nm_ref_string", test_nm_ref_string);
g_test_add_func ("/general/test_string_table_lookup", test_string_table_lookup);
g_test_add_func ("/general/test_nm_utils_get_next_realloc_size", test_nm_utils_get_next_realloc_size);
return g_test_run ();
}