From 475d57444070280a2deba108d86470228d81f2ca Mon Sep 17 00:00:00 2001 From: Emmanuele Bassi Date: Thu, 6 Jan 2022 18:03:16 +0000 Subject: [PATCH 1/3] Add aligned memory allocators When working with storage (especially GInputStream or GOutputStream) it is preferred to use page-aligned buffers so that the operating system can do page-mapping tricks as the operation passes through the kernel. Another use case is allocating memory used for vectorised operations, which must be aligned to specific boundaries. POSIX and Windows, as well as the C11 specification, provide this kind of allocator functions, and GLib already makes use of it inside GSlice. It would be convenient to have a public, portable wrapper that other projects can use. Fixes: #2574 --- docs/reference/glib/glib-sections.txt | 5 + glib/gmem.c | 149 ++++++++++++++++++++++++++ glib/gmem.h | 11 ++ meson.build | 15 ++- 4 files changed, 178 insertions(+), 2 deletions(-) diff --git a/docs/reference/glib/glib-sections.txt b/docs/reference/glib/glib-sections.txt index c8681fa2f..0d534c295 100644 --- a/docs/reference/glib/glib-sections.txt +++ b/docs/reference/glib/glib-sections.txt @@ -1393,6 +1393,11 @@ g_alloca0 g_newa g_newa0 + +g_aligned_alloc +g_aligned_alloc0 +g_aligned_free + g_memmove g_memdup diff --git a/glib/gmem.c b/glib/gmem.c index 605eac1ff..67e283575 100644 --- a/glib/gmem.c +++ b/glib/gmem.c @@ -30,6 +30,25 @@ #include "gmem.h" +#if defined(HAVE_POSIX_MEMALIGN) && !defined(_XOPEN_SOURCE) +# define _XOPEN_SOURCE 600 +#endif + +#if defined(HAVE_MEMALIGN) || defined(HAVE__ALIGNED_MALLOC) +/* Required for _aligned_malloc() and _aligned_free() on Windows */ +#include +#endif + +#ifdef HAVE__ALIGNED_MALLOC +/* _aligned_malloc() takes parameters of aligned_malloc() in reverse order */ +# define aligned_alloc(alignment, size) _aligned_malloc (size, alignment) + +/* _aligned_malloc()'ed memory must be freed by _align_free() on MSVC */ +# define aligned_free(x) _aligned_free (x) +#else +# define aligned_free(x) free (x) +#endif + #include #include #include @@ -522,3 +541,133 @@ g_mem_profile (void) { g_warning (G_STRLOC ": memory profiling not supported"); } + +/** + * g_aligned_alloc: + * @n_blocks: the number of blocks to allocate + * @n_block_bytes: the size of each block in bytes + * @alignment: the alignment to be enforced, which must be a positive power of 2 + * and a multiple of `sizeof(void*)` + * + * This function is similar to g_malloc(), allocating (@n_blocks * @n_block_bytes) + * bytes, but care is taken to align the allocated memory to with the given + * alignment value. Additionally, it will detect possible overflow during + * multiplication. + * + * Aligned memory allocations returned by this function can only be + * freed using g_aligned_free(). + * + * Returns: (transfer full): the allocated memory + * + * Since: 2.72 + */ +gpointer +g_aligned_alloc (gsize n_blocks, + gsize n_block_bytes, + gsize alignment) +{ + gpointer res = NULL; + gsize real_size; + + if (G_UNLIKELY ((alignment == 0) || (alignment & (alignment - 1)) != 0)) + { + g_error ("%s: alignment %"G_GSIZE_FORMAT" must be a positive power of two", + G_STRLOC, alignment); + } + + if (G_UNLIKELY ((alignment % sizeof (void *)) != 0)) + { + g_error ("%s: alignment %"G_GSIZE_FORMAT" must be a multiple of %"G_GSIZE_FORMAT, + G_STRLOC, alignment, sizeof (void *)); + } + + if (SIZE_OVERFLOWS (n_blocks, n_block_bytes)) + { + g_error ("%s: overflow allocating %"G_GSIZE_FORMAT"*%"G_GSIZE_FORMAT" bytes", + G_STRLOC, n_blocks, n_block_bytes); + } + + real_size = n_blocks * n_block_bytes; + + if (G_UNLIKELY (real_size == 0)) + { + TRACE(GLIB_MEM_ALLOC((void*) NULL, (int) real_size, 0, 0)); + return NULL; + } + + errno = 0; + +#if defined(HAVE_POSIX_MEMALIGN) + errno = posix_memalign (&res, alignment, real_size); +#elif defined(HAVE_ALIGNED_ALLOC) || defined(HAVE__ALIGNED_MALLOC) + /* real_size must be a multiple of alignment */ + if (real_size % alignment != 0) + { + gsize offset = real_size % alignment; + + if (G_MAXSIZE - real_size < (alignment - offset)) + { + g_error ("%s: overflow allocating %"G_GSIZE_FORMAT"+%"G_GSIZE_FORMAT" bytes", + G_STRLOC, real_size, (alignment - offset)); + } + + real_size += (alignment - offset); + } + + res = aligned_alloc (alignment, real_size); +#elif defined(HAVE_MEMALIGN) + res = memalign (alignment, real_size); +#else +# error "This platform does not have an aligned memory allocator." +#endif + + TRACE (GLIB_MEM_ALLOC((void*) res, (unsigned int) real_size, 0, 0)); + if (res) + return res; + + g_error ("%s: failed to allocate %"G_GSIZE_FORMAT" bytes", + G_STRLOC, real_size); + + return NULL; +} + +/** + * g_aligned_alloc0: + * @n_blocks: the number of blocks to allocate + * @n_block_bytes: the size of each block in bytes + * @alignment: the alignment to be enforced, which must be a positive power of 2 + * and a multiple of `sizeof(void*)` + * + * This function is similar to g_aligned_alloc(), but it will + * also clear the allocated memory before returning it. + * + * Returns: (transfer full): the allocated, cleared memory + * + * Since: 2.72 + */ +gpointer +g_aligned_alloc0 (gsize n_blocks, + gsize n_block_bytes, + gsize alignment) +{ + gpointer res = g_aligned_alloc (n_blocks, n_block_bytes, alignment); + + if (G_LIKELY (res != NULL)) + memset (res, 0, n_blocks * n_block_bytes); + + return res; +} + +/** + * g_aligned_free: + * @mem: (nullable): the memory to deallocate + * + * Frees the memory allocated by g_aligned_alloc(). + * + * Since: 2.72 + */ +void +g_aligned_free (gpointer mem) +{ + aligned_free (mem); +} diff --git a/glib/gmem.h b/glib/gmem.h index 47c4735ac..d29907a67 100644 --- a/glib/gmem.h +++ b/glib/gmem.h @@ -111,6 +111,17 @@ gpointer g_try_realloc_n (gpointer mem, gsize n_blocks, gsize n_block_bytes) G_GNUC_WARN_UNUSED_RESULT; +GLIB_AVAILABLE_IN_2_72 +gpointer g_aligned_alloc (gsize n_blocks, + gsize n_block_bytes, + gsize alignment) G_GNUC_WARN_UNUSED_RESULT G_GNUC_ALLOC_SIZE2(1,2); +GLIB_AVAILABLE_IN_2_72 +gpointer g_aligned_alloc0 (gsize n_blocks, + gsize n_block_bytes, + gsize alignment) G_GNUC_WARN_UNUSED_RESULT G_GNUC_ALLOC_SIZE2(1,2); +GLIB_AVAILABLE_IN_2_72 +void g_aligned_free (gpointer mem); + #if defined(glib_typeof) && GLIB_VERSION_MAX_ALLOWED >= GLIB_VERSION_2_58 #define g_clear_pointer(pp, destroy) \ G_STMT_START \ diff --git a/meson.build b/meson.build index 6e566d483..d7b770ace 100644 --- a/meson.build +++ b/meson.build @@ -648,8 +648,19 @@ if host_system == 'android' endif -# Check that posix_memalign() is usable; must use header -if host_system != 'windows' and cc.has_function('posix_memalign', prefix : '#include ') +if cc.has_function('memalign', prefix: '#include \n#include ') + glib_conf.set('HAVE_MEMALIGN', 1) +endif + +if cc.has_function('_aligned_malloc', prefix: '#include ') + glib_conf.set('HAVE__ALIGNED_MALLOC', 1) +endif + +if host_system != 'windows' and cc.has_function('aligned_alloc', prefix: '#include ') + glib_conf.set('HAVE_ALIGNED_ALLOC', 1) +endif + +if host_system != 'windows' and cc.has_function('posix_memalign', prefix: '#include ') glib_conf.set('HAVE_POSIX_MEMALIGN', 1) endif From b37c99c44e44bea670663fcabb49c50b3e014259 Mon Sep 17 00:00:00 2001 From: Emmanuele Bassi Date: Fri, 7 Jan 2022 17:24:03 +0000 Subject: [PATCH 2/3] Add tests for g_aligned_alloc() We want to test the API contract, by checking the return value for zero-sized allocations, invalid alignments, and overflows. --- glib/tests/mem-overflow.c | 21 +++++++- glib/tests/utils.c | 100 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 119 insertions(+), 2 deletions(-) diff --git a/glib/tests/mem-overflow.c b/glib/tests/mem-overflow.c index 1654ab7fc..fd926854f 100644 --- a/glib/tests/mem-overflow.c +++ b/glib/tests/mem-overflow.c @@ -34,13 +34,14 @@ static gsize a = G_MAXSIZE / 10 + 10; static gsize b = 10; typedef char X[10]; -#define MEM_OVERFLOW_TEST(name, code) \ +#define MEM_OVERFLOW_TEST(name, code) MEM_OVERFLOW_TEST_FULL(name, code, g_free) +#define MEM_OVERFLOW_TEST_FULL(name, code, free_func) \ static void \ mem_overflow_ ## name (void) \ { \ gpointer p; \ code; \ - g_free (p); \ + free_func (p); \ exit (0); \ } @@ -68,6 +69,12 @@ MEM_OVERFLOW_TEST (new0_b, p = g_new0 (X, b)) MEM_OVERFLOW_TEST (renew_a, p = g_malloc (1); p = g_renew (X, p, a)) MEM_OVERFLOW_TEST (renew_b, p = g_malloc (1); p = g_renew (X, p, b)) +MEM_OVERFLOW_TEST_FULL (aligned_alloc_a, p = g_aligned_alloc (sizeof(X), a, 16), g_aligned_free) +MEM_OVERFLOW_TEST_FULL (aligned_alloc_b, p = g_aligned_alloc (sizeof(X), b, 16), g_aligned_free) + +MEM_OVERFLOW_TEST_FULL (aligned_alloc0_a, p = g_aligned_alloc0 (sizeof(X), a, 16), g_aligned_free) +MEM_OVERFLOW_TEST_FULL (aligned_alloc0_b, p = g_aligned_alloc0 (sizeof(X), b, 16), g_aligned_free) + static void mem_overflow_malloc_0 (void) { @@ -171,6 +178,12 @@ mem_overflow (void) CHECK_SUBPROCESS_PASS (malloc_0); CHECK_SUBPROCESS_PASS (realloc_0); + + CHECK_SUBPROCESS_FAIL (aligned_alloc_a); + CHECK_SUBPROCESS_PASS (aligned_alloc_b); + + CHECK_SUBPROCESS_FAIL (aligned_alloc0_a); + CHECK_SUBPROCESS_PASS (aligned_alloc0_b); } #ifdef __GNUC__ @@ -231,6 +244,10 @@ main (int argc, g_test_add_func ("/mem/overflow/subprocess/renew_b", mem_overflow_renew_b); g_test_add_func ("/mem/overflow/subprocess/malloc_0", mem_overflow_malloc_0); g_test_add_func ("/mem/overflow/subprocess/realloc_0", mem_overflow_realloc_0); + g_test_add_func ("/mem/overflow/subprocess/aligned_alloc_a", mem_overflow_aligned_alloc_a); + g_test_add_func ("/mem/overflow/subprocess/aligned_alloc_b", mem_overflow_aligned_alloc_b); + g_test_add_func ("/mem/overflow/subprocess/aligned_alloc0_a", mem_overflow_aligned_alloc0_a); + g_test_add_func ("/mem/overflow/subprocess/aligned_alloc0_b", mem_overflow_aligned_alloc0_b); #ifdef __GNUC__ g_test_add_func ("/mem/empty-alloc", empty_alloc); diff --git a/glib/tests/utils.c b/glib/tests/utils.c index 643978149..9dbdd2c8c 100644 --- a/glib/tests/utils.c +++ b/glib/tests/utils.c @@ -915,6 +915,100 @@ test_misc_mem (void) g_assert (a == NULL); } +static void +aligned_alloc_nz (void) +{ + gpointer a; + + /* Test an alignment that’s zero */ + a = g_aligned_alloc (16, sizeof(char), 0); + g_assert_null (a); + exit (0); +} + +static void +aligned_alloc_npot (void) +{ + gpointer a; + + /* Test an alignment that’s not a power of two */ + a = g_aligned_alloc (16, sizeof(char), 15); + g_assert_null (a); + exit (0); +} + +static void +aligned_alloc_nmov (void) +{ + gpointer a; + + /* Test an alignment that’s not a multiple of sizeof(void*) */ + a = g_aligned_alloc (16, sizeof(char), 4); + g_assert_null (a); + exit (0); +} + +static void +test_aligned_mem (void) +{ + gpointer a; + + g_test_summary ("Aligned memory allocator"); + + a = g_aligned_alloc (0, sizeof(int), 8); + g_assert_null (a); + + a = g_aligned_alloc0 (0, sizeof(int), 8); + g_assert_null (a); + + a = g_aligned_alloc (16, 0, 8); + g_assert_null (a); + +#define CHECK_SUBPROCESS_FAIL(name,msg) do { \ + { \ + g_test_message (msg); \ + g_test_trap_subprocess ("/utils/aligned-mem/subprocess/" #name, 0, 0); \ + g_test_trap_assert_failed (); \ + } \ + } while (0) + + CHECK_SUBPROCESS_FAIL (aligned_alloc_nz, "Alignment must not be zero"); + CHECK_SUBPROCESS_FAIL (aligned_alloc_npot, "Alignment must be a power of two"); + CHECK_SUBPROCESS_FAIL (aligned_alloc_nmov, "Alignment must be a multiple of sizeof(void*)"); +} + +static void +test_aligned_mem_alignment (void) +{ + gchar *p; + + g_test_summary ("Check that g_aligned_alloc() returns a correctly aligned pointer"); + + p = g_aligned_alloc (5, sizeof (*p), 256); + g_assert_nonnull (p); + g_assert_cmpuint (((guintptr) p) % 256, ==, 0); + + g_aligned_free (p); +} + +static void +test_aligned_mem_zeroed (void) +{ + gsize n_blocks = 10; + guint *p; + gsize i; + + g_test_summary ("Check that g_aligned_alloc0() zeroes out its allocation"); + + p = g_aligned_alloc0 (n_blocks, sizeof (*p), 16); + g_assert_nonnull (p); + + for (i = 0; i < n_blocks; i++) + g_assert_cmpuint (p[i], ==, 0); + + g_aligned_free (p); +} + static void test_nullify (void) { @@ -1084,6 +1178,12 @@ main (int argc, g_test_add_func ("/utils/take-pointer", test_take_pointer); g_test_add_func ("/utils/clear-source", test_clear_source); g_test_add_func ("/utils/misc-mem", test_misc_mem); + g_test_add_func ("/utils/aligned-mem", test_aligned_mem); + g_test_add_func ("/utils/aligned-mem/subprocess/aligned_alloc_nz", aligned_alloc_nz); + g_test_add_func ("/utils/aligned-mem/subprocess/aligned_alloc_npot", aligned_alloc_npot); + g_test_add_func ("/utils/aligned-mem/subprocess/aligned_alloc_nmov", aligned_alloc_nmov); + g_test_add_func ("/utils/aligned-mem/alignment", test_aligned_mem_alignment); + g_test_add_func ("/utils/aligned-mem/zeroed", test_aligned_mem_zeroed); g_test_add_func ("/utils/nullify", test_nullify); g_test_add_func ("/utils/atexit", test_atexit); g_test_add_func ("/utils/check-setuid", test_check_setuid); From 36449ef5f969bc21d5b9900209e661354026ee99 Mon Sep 17 00:00:00 2001 From: Emmanuele Bassi Date: Fri, 7 Jan 2022 17:29:06 +0000 Subject: [PATCH 3/3] Explain the errno use in g_aligned_alloc() --- glib/gmem.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/glib/gmem.c b/glib/gmem.c index 67e283575..060e91af9 100644 --- a/glib/gmem.c +++ b/glib/gmem.c @@ -595,6 +595,14 @@ g_aligned_alloc (gsize n_blocks, return NULL; } + /* We need to clear errno because posix_memalign() will use its return + * value in the same way memalign() and aligned_alloc() will set errno. + * Additionally, posix_memalign() will warn if its return value is left + * unassigned. + * + * We handle all possible return values (ENOMEM and EINVAL) with either + * precondition or postcondition checking. + */ errno = 0; #if defined(HAVE_POSIX_MEMALIGN)