1. ホーム
  2. c

[解決済み】標準ライブラリを使ってアライメントされたメモリのみを割り当てるには?

2022-03-24 08:01:55

質問

面接のためのテストを終えたところですが、ある質問でGoogleを参考にしてもつまずきました。StackOverflowのスタッフがこの問題で何ができるか見てみたいです。

<ブロッククオート

その memset_16aligned 関数は 16 バイトのアラインされたポインタを渡す必要があり、さもなければクラッシュします。

a) どのように1024バイトのメモリを確保し、16バイトの境界線に揃えるか?

b) memset_16aligned が実行されました。

{    
   void *mem;
   void *ptr;

   // answer a) here

   memset_16aligned(ptr, 0, 1024);

   // answer b) here    
}

解決方法は?

オリジナルの回答

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

回答確定

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

ご要望に応じた説明

まず、万が一に備えて十分な予備領域を確保することです。 メモリは16バイトアライメントでなければならないので(先頭バイトのアドレスが16の倍数である必要があるという意味)、16バイト余分に追加することで十分なスペースを確保することができるのです。 最初の16バイトのどこかに、16バイトにアライメントされたポインタがあるのです。 (ただし malloc() のために十分に整列されたポインタを返すことになっています。 任意の を目的としています。 しかし、'any' の意味は主に基本的な型などのためのものである - 。 long , double , long double , long long と、オブジェクトへのポインタと関数へのポインタがあります。 グラフィックシステムで遊ぶような、より専門的なことをする場合、システムの他の部分よりも厳しいアライメントが必要になることがあります - そのため、このような質問と回答があります)。

次のステップは void ポインタを char ポインタに変換することです。GCC とは関係なく、void ポインタに対してポインタ演算を行うことは想定されていません(GCC にはそれを乱用したときに教えてくれる警告オプションがあります)。 それから、開始ポインタに16を加えてください。 仮に malloc() は、ありえないほどアライメントが悪いポインタを返しました。0x800001. 16を足すと0x800011になります。ここで、16バイト境界に切り詰めたいので、最後の4ビットを0にリセットしたいのですが、0x0Fは最後の4ビットが1にセットされています。 ~0x0F は、最後の4ビットを除くすべてのビットが1に設定されています。 これを0x800011とすると0x800010となります。 他のオフセットも繰り返し計算して、同じように動作することを確認できます。

最後のステップです。 free() に戻るだけです。 free() のいずれかが malloc() , calloc() または realloc() を返します。それ以外のものは大失敗です。 あなたは正しく mem を、その値を保持するために使用することができます - ありがとうございます。 フリーはそれを解放します。

最後に、もしあなたがシステムの内部について知っているのであれば malloc パッケージの場合、16バイトアラインのデータを返す可能性が高いことが推測できます(8バイトアラインの場合もあります)。 もし16バイトアライメントなら、値をいじくりまわす必要はないだろう。 しかし、これは危険であり、移植性がありません。 malloc パッケージの最小アライメントが異なるため、あることを想定して別のことをするとコアダンプが発生します。 大まかな制限の範囲内であれば、この解決策は移植可能です。

他の人が posix_memalign() アライメントされたメモリを取得する別の方法として、これはどこでも利用できるわけではありませんが、これを基本として実装できることが多いでしょう。 アライメントが2のべき乗であることが便利であることに注意してください。

もう一つコメントです。このコードでは、アロケーションが成功したかどうかをチェックしていません。

修正

Windowsプログラマー ポインターに対してビットマスク操作ができないという指摘がありましたが、確かにGCC(3.4.6と4.3.1テスト済み)はそのような文句を言いますね。 そこで、基本的なコードを修正し、メインプログラムに変換したものを以下に示します。 また、ご指摘の通り、勝手に16ではなく15だけ追加しています。 私が使っているのは uintptr_t C99は長い歴史があるので、ほとんどのプラットフォームでアクセス可能です。 もし PRIXPTR の中に printf() ステートメントを使用すれば十分でしょう。 #include <stdint.h> を使用する代わりに #include <inttypes.h> . [で指摘された修正を含んでいます。 C.R. が最初に指摘したことを繰り返したものです。 ビル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バイトの間ですが、どのサイズを指定するかのメカニズムが利用できませんでした)ので、あまり有用ではありません。
  • もうひとつの可能性は、完全なメモリアロケータを書き、返す1024バイトのブロックが適切にアラインされていることを確認することを期待されていることです。 その場合、おそらく提案された解決策とよく似た操作をすることになりますが、それをアロケータの中に隠してしまうのです。

しかし、もし面接官がそのどちらかの回答を期待していたのなら、この解決策が密接に関連した質問に答えていることを認識し、質問を組み替えて正しい方向に話を向けることを期待します。 (さらに、もし面接官が本当に険悪になったら、私はその仕事をしたくありません。もし、十分に正確でない要求に対する答えを修正せずに炎上させるなら、その面接官は安全に仕事ができる相手ではありません)。

世界は動き出す

最近、質問のタイトルが変わりましたね。 それは C言語でのメモリ・アライメントを解決する インタビュー質問でつまずいたこと . 修正後のタイトル( 標準ライブラリを使ってアライメントされたメモリのみを確保する方法とは? この補遺がその答えです。

C11 (ISO/IEC 9899:2011) 関数追加 aligned_alloc() :

<ブロッククオート

7.22.3.1項 aligned_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() 関数は size で指定された境界でアライメントされたバイトを指定します。 alignment に割り当てられたメモリへのポインタを返すものとする。 memptr . の値は alignment の2の累乗でなければならない。 sizeof(void *) .

正常に終了すると memptr の倍数でなければならない。 alignment .

要求された空間のサイズが0である場合、動作は実装定義であり、以下のように返される。 memptr は,ヌルポインタ又はユニークポインタのいずれかでなければならない。

free() 関数によって割り当てられたメモリを解放しなければならない。 posix_memalign() .

戻り値

正常に終了した場合。 posix_memalign() そうでない場合は、エラーを示すエラー番号を返すものとする。

現在はこれらのどちらか、または両方を使用して質問に答えることができますが、この質問が最初に回答されたときは、POSIX関数だけが選択肢にありました。

裏側では、新しいアラインドメモリ関数は、より簡単にアライメントを強制する機能を持ち、アラインドメモリの開始位置を内部で追跡して、コードが特別に処理する必要がないように、質問に概説されているのとほとんど同じ仕事をします。