How to allocate aligned memory only using the standard library? Ask Question

How to allocate aligned memory only using the standard library? Ask Question

I just finished a test as part of a job interview, and one question stumped me, even using Google for reference. I'd like to see what the StackOverflow crew can do with it:

The memset_16aligned function requires a 16-byte aligned pointer passed to it, or it will crash.

a) How would you allocate 1024 bytes of memory, and align it to a 16 byte boundary?
b) Free the memory after the memset_16aligned has executed.

{    
   void *mem;
   void *ptr;

   // answer a) here

   memset_16aligned(ptr, 0, 1024);

   // answer b) here    
}

ベストアンサー1

Original answer

{
    void *mem = malloc(1024+16);
    void *ptr = ((char *)mem+16) & ~ 0x0F;
    memset_16aligned(ptr, 0, 1024);
    free(mem);
}

Fixed answer

{
    void *mem = malloc(1024+15);
    void *ptr = ((uintptr_t)mem+15) & ~ (uintptr_t)0x0F;
    memset_16aligned(ptr, 0, 1024);
    free(mem);
}

Explanation as requested

The first step is to allocate enough spare space, just in case. Since the memory must be 16-byte aligned (meaning that the leading byte address needs to be a multiple of 16), adding 16 extra bytes guarantees that we have enough space. Somewhere in the first 16 bytes, there is a 16-byte aligned pointer. (Note that malloc() is supposed to return a pointer that is sufficiently well aligned for any purpose. However, the meaning of 'any' is primarily for things like basic types — long, double, long double, long long, and pointers to objects and pointers to functions. When you are doing more specialized things, like playing with graphics systems, they can need more stringent alignment than the rest of the system — hence questions and answers like this.)

The next step is to convert the void pointer to a char pointer; GCC notwithstanding, you are not supposed to do pointer arithmetic on void pointers (and GCC has warning options to tell you when you abuse it). Then add 16 to the start pointer. Suppose malloc() returned you an impossibly badly aligned pointer: 0x800001. Adding the 16 gives 0x800011. Now I want to round down to the 16-byte boundary — so I want to reset the last 4 bits to 0. 0x0F has the last 4 bits set to one; therefore, ~0x0F has all bits set to one except the last four. Anding that with 0x800011 gives 0x800010. You can iterate over the other offsets and see that the same arithmetic works.

The last step, free(), is easy: you always, and only, return to free() a value that one of malloc(), calloc() or realloc() returned to you — anything else is a disaster. You correctly provided mem to hold that value — thank you. The free releases it.

Finally, if you know about the internals of your system's malloc package, you could guess that it might well return 16-byte aligned data (or it might be 8-byte aligned). If it was 16-byte aligned, then you'd not need to dink with the values. However, this is dodgy and non-portable — other malloc packages have different minimum alignments, and therefore assuming one thing when it does something different would lead to core dumps. Within broad limits, this solution is portable.

誰かが、posix_memalign()アラインメントされたメモリを取得する別の方法として言及していました。これはどこでも利用できるわけではありませんが、これをベースとして使用して実装できる場合がよくあります。アラインメントが 2 の累乗であることが便利だったことに注意してください。他のアラインメントはより複雑です。

もう 1 つコメントがあります。このコードでは割り当てが成功したかどうかはチェックされません。

修正

Windows プログラマーは、ポインタに対してビットマスク演算ができないことを指摘しており、実際、GCC (3.4.6 および 4.3.1 でテスト済み) は、そのようにエラーを発しています。そこで、基本コードの修正版 (メイン プログラムに変換) を以下に示します。また、指摘されているように、16 ではなく 15 だけ追加しました。C99 はほとんどのプラットフォームで利用できるほど長い間使用されてきたため、 を使用しています。ステートメントでuintptr_tを使用していない場合、の代わりに を使用すれば十分です。[このコードには、 が指摘した修正が含まれています。PRIXPTRprintf()#include <stdint.h>#include <inttypes.h>CRは、最初に述べた点を繰り返していた。ビル・K数年前に起こったことですが、今まで見過ごしていました。

#include <assert.h>
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

static void memset_16aligned(void *space, char byte, size_t nbytes)
{
    assert((nbytes & 0x0F) == 0);
    assert(((uintptr_t)space & 0x0F) == 0);
    memset(space, byte, nbytes);  // Not a custom implementation of memset()
}

int main(void)
{
    void *mem = malloc(1024+15);
    void *ptr = (void *)(((uintptr_t)mem+15) & ~ (uintptr_t)0x0F);
    printf("0x%08" PRIXPTR ", 0x%08" PRIXPTR "\n", (uintptr_t)mem, (uintptr_t)ptr);
    memset_16aligned(ptr, 0, 1024);
    free(mem);
    return(0);
}

以下は、2 の累乗のサイズに機能する、わずかに一般化されたバージョンです。

#include <assert.h>
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

static void memset_16aligned(void *space, char byte, size_t nbytes)
{
    assert((nbytes & 0x0F) == 0);
    assert(((uintptr_t)space & 0x0F) == 0);
    memset(space, byte, nbytes);  // Not a custom implementation of memset()
}

static void test_mask(size_t align)
{
    uintptr_t mask = ~(uintptr_t)(align - 1);
    void *mem = malloc(1024+align-1);
    void *ptr = (void *)(((uintptr_t)mem+align-1) & mask);
    assert((align & (align - 1)) == 0);
    printf("0x%08" PRIXPTR ", 0x%08" PRIXPTR "\n", (uintptr_t)mem, (uintptr_t)ptr);
    memset_16aligned(ptr, 0, 1024);
    free(mem);
}

int main(void)
{
    test_mask(16);
    test_mask(32);
    test_mask(64);
    test_mask(128);
    return(0);
}

test_mask()汎用割り当て関数に変換するには、何人かの回答者が指摘しているように、アロケータからの単一の戻り値でリリース アドレスをエンコードする必要があります。

面接官の問題

ウリコメント: 今朝は読解力に問題があるのか​​もしれませんが、面接の質問に「1024 バイトのメモリをどのように割り当てますか」と具体的に書かれていて、明らかにそれ以上のメモリを割り当てた場合、面接官は自動的に不合格と判断するのではないでしょうか。

私の返答は300文字のコメントには収まりきりません...

状況によって異なると思います。ほとんどの人 (私を含む) は、この質問を「1024 バイトのデータを格納でき、ベース アドレスが 16 バイトの倍数であるスペースをどのように割り当てるか」という意味だと解釈したと思います。インタビュアーが本当に「1024 バイト (のみ) を割り当てて、それを 16 バイトに揃えるにはどうすればよいか」と尋ねていた場合、選択肢はさらに限られます。

  • 明らかに、1024 バイトを割り当ててから、そのアドレスに「アライメント処理」を施すという方法があります。この方法の問題点は、実際に使用可能なスペースが適切に決定されないことです (使用可能なスペースは 1008 バイトから 1024 バイトの間ですが、どのサイズを指定するためのメカニズムがありませんでした)。そのため、あまり役に立ちません。
  • もう 1 つの可能性は、完全なメモリ アロケータを作成し、返される 1024 バイトのブロックが適切にアラインされていることを確認する必要があることです。その場合、提案されたソリューションとほぼ同じ操作を実行することになりますが、その操作はアロケータ内に隠されます。

しかし、面接官がどちらかの回答を期待していた場合、私は、面接官がこの解決策が密接に関連する質問への回答であることを認識し、会話を正しい方向に向けるために質問を再構成することを期待します。(さらに、面接官が本当に不機嫌になった場合、私はその仕事は受けたくありません。不十分な正確さの要件に対する回答が修正なしに激しく否定された場合、面接官は安心して働ける人ではありません。)

世界は動き続ける

最近、質問のタイトルが変更されました。私を困惑させたのは、C でのメモリ アラインメントを解決するという面接の質問でした。変更されたタイトル (標準ライブラリのみを使用してアラインメントされたメモリを割り当てる方法は? ) では、わずかに変更された回答が求められます。この補足資料でそれを提供します。

C11 (ISO/IEC 9899:2011) 追加機能aligned_alloc():

7.22.3.1aligned_alloc関数

概要

#include <stdlib.h>
void *aligned_alloc(size_t alignment, size_t size);

説明
このaligned_alloc関数は、配置が で指定されalignment、サイズが で指定されsize、値が不定であるオブジェクトにスペースを割り当てます。 の値はalignment実装によってサポートされている有効な配置でなければならず、 の値はsizeの整数倍でなければなりませんalignment

戻り値
このaligned_alloc関数は、null ポインターまたは割り当てられた領域へのポインターを返します。

そしてPOSIXは定義しているposix_memalign():

#include <stdlib.h>

int posix_memalign(void **memptr, size_t alignment, size_t size);

説明

この関数は、 で指定された境界に揃えられたバイトposix_memalign()を割り当て、 に割り当てられたメモリへのポインタを返します。 の値は、の 2 の累乗倍数になります。sizealignmentmemptralignmentsizeof(void *)

正常に完了すると、 が指す値はmemptrの倍数になりますalignment

要求されたスペースのサイズが 0 の場合、動作は実装定義であり、返される値はmemptrnull ポインターまたは一意のポインターのいずれかになります。

このfree()関数は、以前に によって割り当てられたメモリを解放しますposix_memalign()

戻り値

正常に完了した場合はposix_memalign()ゼロを返します。それ以外の場合は、エラーを示すエラー番号を返します。

現在では、これらのいずれかまたは両方を使用して質問に答えることができますが、質問が最初に回答されたときは、POSIX 関数のみがオプションでした。

舞台裏では、新しいアラインメントされたメモリ関数は、質問で概説されているのとほぼ同じ仕事を行いますが、アラインメントをより簡単に強制し、アラインメントされたメモリの開始を内部的に追跡する機能があるため、コードが特別に処理する必要はなく、使用された割り当て関数によって返されたメモリを解放するだけです。

おすすめ記事