web-dev-qa-db-ja.com

C ++で未定義の動作なしにマ​​ップされたメモリに適切にアクセスする方法

未定義の動作を呼び出さずに、C++ 17からマップされたバッファーにアクセスする方法を理解しようとしています。この例では、Vulkanの vkMapMemory によって返されるバッファを使用します。

したがって、 N4659 (最終C++ 17ワーキングドラフト)、セクション [intro.object] (強調を追加)に従って、

C++プログラムの構造は、オブジェクトの作成、破棄、参照、アクセス、および操作を行います。オブジェクトはdefinition(6.1)によって、new-expression(8.3.4)、共用体のアクティブメンバーを暗黙的に変更する場合(12.3)、または一時オブジェクトが作成されます(7.4、15.2)。

これらは明らかに、C++オブジェクトを作成する唯一の有効な方法です。したがって、ホストから見える(そしてコヒーレントな)デバイスメモリのマップされた領域へのvoid*ポインタを取得したとします(もちろん、必要なすべての引数に有効な値があり、呼び出しが成功し、返されたブロックがメモリは十分なサイズであり、適切に整列されています):

void* ptr{};
vkMapMemory(device, memory, offset, size, flags, &ptr);
assert(ptr != nullptr);

さて、このメモリにfloat配列としてアクセスしたいと思います。明白なことは、ポインタをstatic_castして、次のように陽気に進むことです。

volatile float* float_array = static_cast<volatile float*>(ptr);

volatilecoherentメモリとしてマップされるため、含まれており、GPUによって任意の時点で書き込まれる可能性があります)。ただし、float配列はそのメモリの場所に技術的に存在しない、少なくとも引用された抜粋の意味では存在しないため、そのようなメモリを通じてメモリにアクセスするポインタは未定義の動作になります。したがって、私の理解によると、私には2つのオプションが残されています。

1. memcpyデータ

ローカルバッファーを常に使用できるようにして、それをstd::byte*およびmemcpyにキャストし、表現をマップされた領域にキャストします。 GPUはそれをシェーダーで指示されたものとして解釈し(この場合は32ビットfloatの配列として)、したがって問題は解決されます。ただし、これには追加のメモリと追加のコピーが必要になるため、これは避けたいと思います。

2.配置-new配列

セクション [new.delete.placement] は、配置アドレスの取得方法に制限を課していないようです(これは 安全である必要はありません)派生ポインター 実装のポインターの安全性に関係なく)。したがって、次のように配置-newを使用して有効なフロート配列を作成できるはずです。

volatile float* float_array = new (ptr) volatile float[sizeInFloats];

ポインタfloat_arrayは、安全にアクセスできるはずです(配列の境界内、または過去)。


だから、私の質問は次のとおりです:

  1. 単純なstatic_castは本当に未定義の動作ですか?
  2. この配置-newの使用法は明確ですか?
  3. この手法は メモリマップハードウェアへのアクセス などの同様の状況に適用できますか?

ちなみに、返されたポインタをキャストするだけでは問題が発生したことはありません。これを行うにはproperの方法を理解しようとしているだけです。標準の手紙に。

23
Socrates Zouras

短い答え

標準に従って、ハードウェアにマップされたメモリに関係するものはすべて抽象マシンには存在しないため、未定義の動作です。実装マニュアルを参照してください。


長い答え

ハードウェアにマップされたメモリは標準では未定義の動作ですが、一般的なルールに準拠している健全な実装を想像できます。その場合、一部の構成要素はmore他の構成要素よりも未定義の動作です(それが何であれ)。

単純な_static_cast_は本当に未定義の動作ですか?

_volatile float* float_array = static_cast<volatile float*>(ptr);
_

はい、 これは未定義の動作 であり、StackOverflowで何度も議論されてきました。

この配置-新しい使用法は明確ですか?

_volatile float* float_array = new (ptr) volatile float[N];
_

いいえ、これは明確に見えますがこれは実装に依存します。たまたま、_operator ::new[]_はいくつかのオーバーヘッドを予約できます 1、2 であり、ツールチェーンのドキュメントを確認しない限り、その量を知ることはできません。その結果、::new (dst) T[N]は_N*sizeof T_以上の未知の量のメモリを必要とし、割り当てたdstは小さすぎて、バッファオーバーフローを伴う可能性があります。

次にどうすればいいですか?

解決策は、フロートのシーケンスを手動で構築することです:

_auto p = static_cast<volatile float*>(ptr);
for (std::size_t n = 0 ; n < N; ++n) {
    ::new (p+n) volatile float;
}
_

または同等に、標準ライブラリに依存します:

_#include <memory>
auto p = static_cast<volatile float*>(ptr);
std::uninitialized_default_construct(p, p+N);
_

これは、Nが指すメモリにptrの初期化されていない_volatile float_オブジェクトを連続的に構築します。つまり、それらを読み取る前に初期化する必要があります。初期化されていないオブジェクトを読み取ることは未定義の動作です。

この手法は、メモリマップされたハードウェアへのアクセスなど、同様の状況に適用できますか?

いいえ、再びこれは実際に実装定義です。私たちはあなたの実装が合理的な選択をしたと仮定することができるだけですが、あなたはそのドキュメントが言っていることをチェックするべきです。

9
YSC

C++仕様にはマップされたメモリの概念がないため、C-++仕様に関する限り、everythingとの関係は未定義の動作です。そのため、使用している特定の実装(コンパイラとオペレーティングシステム)を調べて、何が定義されており、安全に何ができるかを確認する必要があります。

ほとんどのシステムでは、マッピングは別の場所からのメモリを返し、特定のタイプと互換性のある方法で初期化されている場合とされていない場合があります。一般に、メモリが元々正しいサポートされている形式のfloat値として書き込まれていた場合は、ポインタをfloat *に安全にキャストして、そのようにアクセスできます。ただし、マップされているメモリが最初にどのように書き込まれたかを知る必要があります。

3
Chris Dodd