未定義の動作を呼び出さずに、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);
(volatile
はcoherentメモリとしてマップされるため、含まれており、GPUによって任意の時点で書き込まれる可能性があります)。ただし、float
配列はそのメモリの場所に技術的に存在しない、少なくとも引用された抜粋の意味では存在しないため、そのようなメモリを通じてメモリにアクセスするポインタは未定義の動作になります。したがって、私の理解によると、私には2つのオプションが残されています。
memcpy
データローカルバッファーを常に使用できるようにして、それをstd::byte*
およびmemcpy
にキャストし、表現をマップされた領域にキャストします。 GPUはそれをシェーダーで指示されたものとして解釈し(この場合は32ビットfloat
の配列として)、したがって問題は解決されます。ただし、これには追加のメモリと追加のコピーが必要になるため、これは避けたいと思います。
new
配列セクション [new.delete.placement] は、配置アドレスの取得方法に制限を課していないようです(これは 安全である必要はありません)派生ポインター 実装のポインターの安全性に関係なく)。したがって、次のように配置-new
を使用して有効なフロート配列を作成できるはずです。
volatile float* float_array = new (ptr) volatile float[sizeInFloats];
ポインタfloat_array
は、安全にアクセスできるはずです(配列の境界内、または過去)。
だから、私の質問は次のとおりです:
static_cast
は本当に未定義の動作ですか?new
の使用法は明確ですか?ちなみに、返されたポインタをキャストするだけでは問題が発生したことはありません。これを行うにはproperの方法を理解しようとしているだけです。標準の手紙に。
標準に従って、ハードウェアにマップされたメモリに関係するものはすべて抽象マシンには存在しないため、未定義の動作です。実装マニュアルを参照してください。
ハードウェアにマップされたメモリは標準では未定義の動作ですが、一般的なルールに準拠している健全な実装を想像できます。その場合、一部の構成要素は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
_オブジェクトを連続的に構築します。つまり、それらを読み取る前に初期化する必要があります。初期化されていないオブジェクトを読み取ることは未定義の動作です。
この手法は、メモリマップされたハードウェアへのアクセスなど、同様の状況に適用できますか?
いいえ、再びこれは実際に実装定義です。私たちはあなたの実装が合理的な選択をしたと仮定することができるだけですが、あなたはそのドキュメントが言っていることをチェックするべきです。
C++仕様にはマップされたメモリの概念がないため、C-++仕様に関する限り、everythingとの関係は未定義の動作です。そのため、使用している特定の実装(コンパイラとオペレーティングシステム)を調べて、何が定義されており、安全に何ができるかを確認する必要があります。
ほとんどのシステムでは、マッピングは別の場所からのメモリを返し、特定のタイプと互換性のある方法で初期化されている場合とされていない場合があります。一般に、メモリが元々正しいサポートされている形式のfloat
値として書き込まれていた場合は、ポインタをfloat *
に安全にキャストして、そのようにアクセスできます。ただし、マップされているメモリが最初にどのように書き込まれたかを知る必要があります。