目次
ご存知のとおり、実行中のプログラムはメモリを占有する必要があり、コーディング時にはスタック上のスペースが連続しており、定義されたすべての変数がスタック上に連続的に分散されていると想定されます。
実際、変数はスタック上に継続的に分散されていますが、コンパイラは最適な状況を達成するために、さまざまな型とアライメントに従って変数を再配置します。
#include <stdio.h>
#define print_position(type, n) \
type n; \
printf(#n ": %p\n", &n);
int main(void) {
print_position(int, a); // a: 0x7ffe84765408
print_position(double, b); // b: 0x7ffe84765410
print_position(char, c); // c: 0x7ffe84765407
print_position(float, d); // d: 0x7ffe8476540c
}
この記事では主に構造の配置に焦点を当てます。
なぜメモリの調整が必要なのか
パフォーマンス
最新のプロセッサには、データが通過する必要がある複数レベルのキャッシュがあり、シングルバイト読み取りのサポートにより、メモリのスループットが実行ユニットのスループット (CPU バウンド、CPU バウンドと呼ばれます) に緊密にバインドされます。これには、 ハードウェア ドライバーの DMA によって PIO がオーバーライドされるのと同様の理由が多数あります 。
CPU は、整列されていないアドレスにアクセスする場合、常にワード サイズ (32 ビット プロセッサでは 4 バイト) を読み取ります。CPU がサポートしている場合、プロセッサは複数のワードを読み取ります。CPU はプログラムによって要求されたアドレスを複数のワードにわたって読み取ります。その結果、要求されたデータの 2 倍のサイズのメモリの読み取りおよび書き込みが行われます。したがって、2 バイトの読み取りは 4 バイトの読み取りよりも遅くなる可能性があります。
2 バイトのデータがワード内で整列していない場合、プロセッサはデータを 1 回読み取ってオフセット計算を実行するだけでよく、通常は 1 サイクルしかかかりません。
さらに、アライメントにより、それらが同じキャッシュ ライン上にあるかどうかをより適切に判断でき、一部の種類のアプリケーションではキャッシュ ラインを最適化してパフォーマンスを向上します。
範囲
任意のアドレス空間が与えられた場合、アーキテクチャが 2 つの最下位ビット (LSB) が常に 0 であるとみなしている場合 (32 ビット マシンのように)、メモリの 4 倍のサイズにアクセスできます (2 ビットで 4 つの異なる状態を表すことができます)。同じサイズのメモリですが、フラグ ビットが 2 つ追加されています。最下位 2 ビットは 4 バイトのアライメントを意味し、アドレスはインクリメント時に 2 番目のビットからのみ変更され、最下位 2 ビットは常に です 00
。
これはプロセッサの物理構造に影響を与える可能性があり、アドレス バス上のビットが 2 つ減ったり、CPU 上のピンが 2 つ減ったり、回路基板上のワイヤが 2 つ減ったりすることになります。
原子性
CPU はアライメントされたワード メモリ上でアトミックに動作できます。つまり、どの命令も動作を中断することはできません。これは、多くのロックフリー データ構造やその他の同時実行パラダイムが正しく動作するために重要です。
結論は
プロセッサのメモリ システムは、ここで説明するものよりもはるかに複雑です。ここでは、 x86 プロセッサが実際にどのようにアドレス指定されるかについて説明します 。これは理解に役立ちます (多くのプロセッサは同様に動作します)。
メモリの調整を行うことには他にも多くの利点があります。それについては、この記事で読むことができます。コンピュータの主な目的はデータを転送することであり、最新のメモリ アーキテクチャとテクノロジは、信頼性の高い方法で、より多くのより高速な実行ユニット間でより多くのデータ入出力の処理を容易にするために、数十年にわたって最適化されてきました。
データ・モデル
C およびその派生言語では、多くの場合、型のサイズがプラットフォームに関連するため、さまざまなプラットフォームでのデータ サイズを定義するためにデータ モデルが使用されます。
データ モデルの定義は非常に明確ですが、クロスプラットフォーム コードを扱う場合、データ型のサイズの処理が頭の痛い問題になります。
幸いなことに、C/C++ は stdint.h
さらに多くの種類の固定長整数も提供しており、長さは主に 8
、16
、32
および bit であり、さまざまな要件を 持つ 64
固定長整数を提供します 。fast
least
- 固定長の整数 (例:
uint8_t
、int16_t
)。固定長整数はコンパイラ オプションであるため、指定された型がない場合があります。固定長整数型で指定されるビット長は、それ以上またはそれ以下にすることはできません。つまり、ビット長の一致は必須です。 - 最も近い固定長整数 (例:
int_least16_t
、 )uint_least16_t
。最も近い固定長整数は、指定されたビット長より長くてもかまいませんが、それより小さくなることはありません。などを使用しますuint_least8_t
が、プラットフォームはサポートしていませんuint16_t
がサポートしているuint32_t
ため、タイプは ですuint32_t
。 - 最速の固定長整数、例
int_fast32_t
:uint_fast32_t
。最速の固定長整数型とは、指定されたビット長以上の整数型を指し、指定されたビット長が満たされる場合に最も高速に実行される整数型が使用されます。を使用するなどuint_fast8_t
、プラットフォームはuint32_t
と を サポートしますuint16_t
が、最も速いのは であるuint32_t
ため、このタイプには前者が使用されます。
最後に、ポインターのサイズはプラットフォームごとに異なるため、ポインター ビット整数を変換するときに、プラットフォーム間の intptr_t
互換性 のために標準ライブラリのオプションの合計を選択できますuintptr_t
。
C++ メモリ アライメント
この章のデータ モデルは LP64 データ モデルです。
署名リクエスト
普通クラス
まず、可平凡复制类型
以下の条件をすべて満たすこと
- 少なくとも 1 つの破棄されていない
复制构造函数
、移动构造函数
、复制赋值运算符
または移动赋值运算符
- すべてのコピー コンストラクターは重要ではないか、削除されています
- すべての移動コンストラクターは重要ではないか、削除されています
- すべてのコピー代入演算子は単純であるか削除されています
- すべての移動代入演算子は単純であるか削除されています
- 削除されていない自明なデストラクタがあります
平凡类
以下の条件をすべて満たすもの1 つ
- 簡単にコピーできる型です
- 1 つ以上のデフォルト コンストラクターがあり、それらはすべて簡単であるか削除されており、少なくとも 1 つは削除されていません
struct A {}; // is trivial
struct B { B(B const&) = delete; }; // is trivial
struct C { C() {} }; // is non-trivial
struct D { ~D() {} }; // is non-trivial
struct E { ~E() = delete; }; // is non-trivial
struct F { private: ~F() = default; } // is non-trivial
struct G { virtual ~G() = default; } // is non-trivial
struct H {
H() = default;
H(const H &) = delete;
H(H &&) noexcept = delete;
H &operator=(H const &) = delete;
H &operator=(H &&) noexcept = delete;
~H() = default;
}; // is non-trivial
struct I { I() = default; I(int) {} }; // is trivial
struct J {
J() = default;
J(const J &) {}
}; // is non-trivial
struct K { int x; }; // is trivial
struct L { int x{0}; }; // is non-trivial
gcc や Clang でコンパイルすると、コンパイラは 通常のクラス である とE
表示します が、標準では通常のクラスではないはずです。gcc や Clang のバグ レポートはbugzilla で確認できます。F
H
さらに、自明にコピー可能なクラスを使用して、 重複する可能性を持たずに 2 つのオブジェクト間でコピー::memcpy
または ::memmove
コピーすることができます。
struct A { int x; };
A a = { .x = 10 }; // C++20
A b = { .x = 20 };
::memcpy(&b, &a, sizeof(A)); // b.x = 10
通常のクラスはリソースを保持していないとみなされるため、リソース リークを引き起こすことなくオブジェクトを直接上書きまたは破棄できます。
template <typename T, size_t N>
void destroy_array_element(
typename ::std::enable_if<::std::is_trivial<T>::value>::type (&/* arr */)[N]) {}
template <typename T, size_t N> void destroy_array_element(T (&arr)[N]) {
for (size_t i = 0; i < N; ++i) {
arr[i].~T();
}
}
標準レイアウトクラス
以下の条件をすべて満たす標準レイアウトクラス
- すべての非静的データ メンバーは標準レイアウト クラス型またはそれらへの参照です
- 仮想関数や仮想基底クラスはありません
- すべての非静的データ メンバーは同じアクセシビリティを持ちます
- 非標準レイアウトの基本クラスはありません
- このクラスとそのすべての基本クラスの非静的データ メンバーとビットフィールドは、最初に同じクラスで宣言されます。
- クラス S が与えられ、基本クラスとしてのセットには
M(S)
要素がありません。ここで、型 X の M(X) は次のように定義されます。- X が (おそらく継承された) 非静的データ メンバーを持たない非共用体クラス型である場合、セット M(X) は空になります。
- X が非共用体クラス型で、その最初の非静的データ メンバー (おそらく匿名共用体) の型が X0 である場合、セット M(X) には X0 と M(X0) の要素が含まれます。
- X が共用体型の場合、セット M(X) は、すべての UiU_{i}Ui と各 M(UiU_{i}Ui) セットを含むセットの和集合です。ここで、各 UiU_{i}Ui はX の i 番目の非静的データ メンバーの型。
- X が要素型が XeX_{e}Xe である配列型の場合、セット M(X) には XeX_{e}Xe と M(XeXeXe) の要素が含まれます。
- X がクラス型または配列型ではない場合、セット M(X) は空になります。
struct A { int a; }; // is standard layout
struct B : public A { double b; }; // isn't standard layout
struct C { A a; double b; }; // is standard layout
struct D {
int a;
double b;
}; // is standard layout
struct E {
public: int a;
private: double b;
}; // isn't standard layout
struct F {
public: int fun() { return 0; }
private: double a;
}; // is standard layout
簡単な標準レイアウト クラスの概要
C 言語のすべての型は標準レイアウトであることは明らかですが、C++ ではこれらの型を C で表現するために POD (Plain Old Data) の概念が導入されています (C++20 ではこの概念が削除されました)。つまり、次の条件がすべて満たされています。タイプ:
- 普通クラス
- 標準レイアウトクラス
- すべての非静的データ メンバーは POD クラス タイプです
通常のクラスは、型がリソースを考慮しないこと、つまり最も基本的な構築メソッドと破棄メソッドを指定し、標準レイアウト クラスは型が各フィールドをどのようにレイアウトするかを指定することが理解できます。標準レイアウトクラスであればCプログラムでも問題なく動作しますが、この型は自明な型ではない可能性があるため、PODは2つの概念に分かれています。
最も理解しておくべきことは、RAII を使用して、複雑な構造とデストラクターを使用してリソースを独自に管理しているということです 。これは通常のクラスではありませんが、標準のレイアウト クラス::std::vector
であるため、メモリ アライメント メソッドに完全に従っており、また、 used 内部値をコピーします。memcpy
// #include <stdint.h>
// #include <stdlib.h>
// #include <string.h>
// #include <iostream>
// #include <vector>
::std::vector<char> v{'a', 'b', 'c'};
uintptr_t *copy = reinterpret_cast<uintptr_t *>(::alloca(sizeof v));
::memcpy(copy, &v, sizeof v);
for (size_t i = 0, e = sizeof(v) / sizeof(uintptr_t); i < e; ++i) {
::std::cout << copy[i] << ::std::endl;
}
// maybe output:
// 94066226852544
// 94066226852547
// 94066226852547
標準レイアウトクラスのメモリアラインメント
メモリの調整には従うべきルールがいくつかあります。
- オブジェクトの開始アドレスは、そのアライメント サイズで割り切れます。
- 開始アドレスに対するメンバーのオフセットは、そのメンバー自身のアライメント サイズで割り切れます。それ以外の場合は、前のメンバーの後のバイトを埋めます。
- クラスのサイズはそのアライメント サイズで割り切れます。それ以外の場合は最後にバイトが埋め込まれます。
- 空のクラスの場合、このクラスのオブジェクトは標準に従って 1 バイトを占有する必要があり (空の 基本クラスが最適化されていない限り)、C の空のクラスのサイズは 0 バイトです。
- デフォルトでは、型のアライメント サイズは、そのすべてのフィールドの最大アライメント サイズと同じです。
通常の標準レイアウトクラス
標準レイアウト クラスでは、上記のルールを使用して型のサイズを簡単に決定できます。
struct S {}; // sizeof = 1, alignof = 1
struct T : public S { char x; }; // sizeof = 1, alignof = 1
struct U {
int x; // offsetof = 0
char y; // offsetof = 4
char z; // offsetof = 5
}; // sizeof = 8, alignof = 4
struct V {
int a; // offsetof = 0
T b; // offsetof = 4
U c; // offsetof = 8
double d; // offsetof = 16
}; // sizeof = 24, alignof = 8
struct W {
int val; // offset = 0
W *left; // offset = 8
W *right; // offset = 16
}; // sizeof = 24, alignof = 8
最後に配列について説明しますが、配列とは、この位置にこの型の配列の長さと変数を導入したようなものです。
struct S { int x[4]; }; // sizeof = 16, alignof = 4
struct T {
int a; // offsetof = 0
char b[9]; // offsetof = 4
short c[2]; // offsetof = 14
double *d; // offsetof = 24
}; // sizeof = 32, alignof = 8
struct U {
char x; // offsetof = 0
char y[1]; // offsetof = 1
short z; // offsetof = 2
}; // sizeof = 4, alignof = 2
これで終わりだと思いますか?もちろんそうではありません。C 言語には非常に興味深い使用法があります。それは、 C99 で登場した柔軟な配列宣言です。最後のフィールドを長さ 0 の配列として定義します。この時点で、配列の基礎となるデータ型は型の位置合わせサイズに影響しますが、型全体のサイズには影響しません。もちろん、C++ 標準はサポートされておらず、拡張は完全にコンパイラに依存します。
struct S {
int i; // offset = 0
double d[]; // offset = 8
}; // sizeof = 8, alignof = 8
struct T {
int i; // offset = 0
char c[0]; // offset = 4
}; // sizeof = 4, alignof = 4
柔軟な配列メンバーは初期化できないため、柔軟な配列メンバーを持つクラスでは動的割り当てを使用する必要があります。実際、コンパイラは配列の長さを決定できないため、たとえ指定された追加スペースが基礎となる型データを格納するのに十分でない場合でも、アクセスの正確性はプログラマによって保証され、アクセス オーバーフローの範囲は UB になります。 。
S s1; // sizeof(s1) = 8, length(d) = 1, accessing d is a UB
// S s2 = {1, {3.14}}; // error: initialization of flexible array member is not allowed
S* s3 = reinterpret_cast<S*>(alloca(sizeof(S))); // equivalent to s1
// s4: sizeof(*s4) = 8, length(d) = 6
S *s4 = reinterpret_cast<S *>(alloca(sizeof(S) + 6 * sizeof(S::d[0])));
// s5: sizeof(*s5) = 8, length(d) = 1, accessing d[1] is a UB
S *s5 = reinterpret_cast<S *>(alloca(sizeof(S) + 10));
*s4 = *s5; // copy size = sizeof(S)
ビットフィールドを備えた標準レイアウト クラス
ビット フィールドを持つ標準レイアウト クラスの場合も非常に単純です。ビット フィールドは基になるデータ全体に格納されません。つまり、残りのビットが十分でない場合、次のビット フィールドのフィールドは次の基になるデータに格納されます。データ。名前のないビット フィールド フィールドは、プレースホルダーの役割を果たすことができます。さらに、ビット フィールドが宣言された後、実際にはクラスに基礎となるデータが埋め込まれ、クラスのサイズと配置は基礎となるデータの影響を受けます。
struct S {
// offsetof = 0
unsigned char b1 : 3, : 2;
// offsetof = 1
unsigned char b2 : 6, b3 : 2;
}; // sizeof = 2, alignof = 1
ビットフィールド フィールドのサイズは 0 として指定できます。これは、次のビットフィールドが次の基になるデータで宣言されることを意味します。ただし、実際の長さ 0 のビット フィールド フィールドは、クラスの基礎となるデータを導入しません。
struct S { int : 0; }; // sizeof = 1, alignof = 1
struct T {
uint64_t : 0;
uint32_t x; // offsetof = 0
}; // sizeof = 4, alignof = 4
struct U {
// offsetof = 0
unsigned char b1 : 3, : 0;
// offsetof = 1
unsigned char b2 : 2;
}; // sizeof = 2, alignof = 1
配置サイズを手動で指定する標準レイアウト クラス
この章の冒頭の 5 つのルールに戻りますが、実際、これは位置合わせを手動で指定する場合にも当てはまります。
#pragma pack(N)
フィールドの指定と gnu::packed
配置は、各フィールドが連続して配置されるパッケージ化された方法で行われるため、フィールド間に新たなメモリホールが発生することがなく、メモリの無駄な浪費を軽減できます。
struct [[gnu::packed]] S {
uint8_t x; // offsetof = 0
uint16_t y; // offsetof = 1
}; // sizeof = 3, alignof = 1
struct [[gnu::packed]] T {
uint16_t x : 4;
uint8_t y; // offsetof = 1
}; // sizeof = 2, alignof = 1
struct [[gnu::packed]] alignas(4) U {
uint8_t x; // offsetof = 0
uint16_t y; // offsetof = 1
}; // sizeof = 4, alignof = 4
struct [[gnu::packed]] alignas(4) V {
uint16_t x : 4;
uint8_t y; // offsetof = 1
}; // sizeof = 4, alignof = 4
alignas
しかし、今日はC++11 で導入された宣言子に焦点を当てます 。実際、構造を整列させる方法を指定するだけでなく、オブジェクトを整列させる方法も指定できます。指定されたアライメント サイズは、2 の正の整数乗である必要があります。指定されたアライメントがデフォルトのアライメントよりも弱い場合、コンパイラはそれを無視するか、エラーを報告することがあります。
最も単純なことは、指定された構造体の宣言から始めることです。
struct alignas(4) S {}; // sizeof = 4, alignof = 4
struct SS {
S s; // offsetof = 0
S *t; // offsetof = 8
}; // sizeof = 16, alignof = 8
struct alignas(SS) T {
S s; // offsetof = 0
char t; // offsetof = 4
short u; // offsetof = 6
short v; // offsetof = 8
}; // sizeof = 16, alignof = 8
struct alignas(1) U : public S {}; // error or ignore
// struct alignas(5) V : public S {}; // error
struct alignas(4) W : public S {};
alignas
の適用は主に、より良いパフォーマンスを得るために、または SIMD 命令と一致させるために行われます。
非標準レイアウト クラスのメモリ アライメント
アクセス制限による非標準レイアウトのクラスについては、標準レイアウトに従って配置されているとは想定できず、その動作はコンパイラに依存します。C++11 標準では、同じアクセス権を持つ変数のみが宣言順に配置されることが保証されていますが、異なるアクセス権を持つ変数の順序は保証されません。
struct S {
public: int s;
int t;
private: int u;
public: int v;
};
つまり、上記の例では、保証されているだけで &S::s < &S::t < &S::v
、保証されていません &S::s < &S::u
。言い換えれば、記憶の中で s, t, u, v
それが現れるかもしれない順序、そして u, s, t, v
それが現れるかもしれない順序です。
もちろん、アクセシビリティによって生じる順序の問題だけでなく、異なるクラスで宣言されたフィールドによっても順序の問題が発生します。つまり、基本クラスで宣言された変数が派生クラスで宣言された変数より前に配置されなければならないと想定することはできません。
struct S { int s; };
struct T { int t; };
struct U : public S, T { int u; };
とはいえ、上記の例では、保証はありません &U::s < &U::u
。ただし、標準では、派生クラス ポインターが基本クラス ポインターに変換されるときに、基本クラスの単語オブジェクトのオフセットが自動的に計算されることが保証されています。ただし、U のオブジェクトの最初のアドレスが S のワード オブジェクトの最初のアドレスであるという保証はありません。
U *up = reinterpret_cast<U *>(alloca(sizeof(U)));
S *ssp = static_cast<S *>(up); // offset adjustment
T *stp = static_cast<T *>(up); // offset adjustment
S *rsp = reinterpret_cast<S *>(up); // no offset adjustment
T *rtp = reinterpret_cast<T *>(up); // no offset adjustment
最後に、仮想クラスのメモリ アライメントについて話しましょう。これは非常に興味深い質問です。標準では仮想関数の実装方法は指定されていませんが、ほとんどのコンパイラは仮想関数を実装するために仮想テーブルを使用します。つまり、オブジェクトに仮想関数テーブルへのポインタを挿入します。ただし、仮想テーブルは 1 つのオブジェクトにのみ存在し、基本クラスのサブオブジェクトには仮想テーブルが存在しないことに注意してください。
struct S {
bool s; // offsetof = 0
}; // sizeof = 1, alignof = 1
struct T {
virtual ~T() = default;
int t;
};
struct U : public S, T {
virtual ~U() = default;
int u;
};
コンパイラの実装では、最初に仮想基本クラスを配置し、次に非仮想基本クラスを配置する可能性が高いため、クラスのサイズとレイアウトを異なる配置で決定することはできません。
GLSLang のメモリ アラインメント
GLSL 4.60、Vulkan バインディング
GLSLang では、ワードの長さは 4 バイトです。GLSLang のアライメントも C/C++ のアライメントとよく似ているため、標準レイアウト クラスのメモリ アライメントで説明されているアライメントは基本的にここと同じです。さらに、GLSLang の基本型のサイズはワード長の倍数であるため、後続の sizeof
結果の単位はデフォルトでワードになります。
バッファのレイアウト装飾
buffer
読み取りおよび書き込み可能なグローバル オブジェクトとして、手動で指定しない限り、そのレイアウトは実装によって定義されます。uniform
これは特別なグローバル バッファであり、読み取り専用であり、デフォルトの std140 レイアウトであり、変更することはできません。push_constant
レジスタに格納される特別なユニフォームであり、サイズは約 16 ワードです。実装では代わりにユニフォームを使用できます。超過した部分はユニフォームバッファに保存されます。デフォルトのレイアウトは std430 ですが、レイアウトは変更できます。
バッファーでは、デフォルトの行列は列優先行列 ( column_major ) ですが、これはレイアウトで変更できます。
layout(binding = 0, column_major) buffer CMTest {
// matrix stride = 16
mat2x3 cm; // is equalent to 2-elements array of vec3
};
layout(binding = 1, row_major) buffer RMTest {
// matrix stride = 8
mat2x3 rm; // is equalent to 3-elements array of vec2
};
packed
これは CPU のコンセプトと一致しており、フィールドを可能な限りコンパクトに配置し、配置に関係なくメモリを節約します。ただし、SPIRVでは使用packed
およびshared
レイアウトを禁止します。
GLSLang のレイアウトでは、オフセットもアライメント サイズの整数倍になります。std140 レイアウトには次の規則があります。
- 整列されたサイズが自身のサイズと同じであるスカラー型
- サイズ N の基礎となるタイプを持つ 2 値または 4 値ベクトル。ベクトル サイズはアライメント サイズと同じで、アライメント サイズは 2N2N2N または 4N4N4N です。特に、3 値ベクトルのサイズは 3N3N3N ですが、アライメント サイズは 4N4N4N です。
- 配列内の各要素を 4 ワードの倍数に埋めます。
- 構造体変数のアライメント サイズは 4 ワードの倍数に埋められます。
- C 列と R 行を持つ列優先行列は、C 個の R 要素ベクトルを持つ配列と等価です。同様に、N 要素の列優先行列を持つ配列は、N×CN \times CN×C 配列と等価です。 R 要素ベクトルの
- C 列と R 行を持つ行優先行列は、R 個の C 要素ベクトルを持つ配列と等価です。同様に、N 要素の行優先行列を持つ配列は、N×RN \times RN×R 配列と等価です。 C 要素ベクトルの
struct S {
vec2 v;
};
layout(binding = 0, std140) buffer BufferObject {
mat2x3 m; // offsetof = 0
bool b[2]; // offsetof = 8
vec3 v1; // offsetof = 16
uint u; // offsetof = 19
S s; // offsetof = 20
float f2; // offsetof = 24
vec2 v2; // offsetof = 26
dvec3 dv; // offsetof = 32
} bo; // sizeof = 40, alignof = 8
std430 レイアウトの場合、配列要素と構造要素を 4 ワードに位置合わせして埋めるという std140 の要件はなくなりました。つまり、std430 はよりコンパクトで、CPU 内のレイアウトに近くなります。
struct S {
vec2 v;
};
layout(binding = 0, std430) buffer BufferObject {
mat2x3 m; // offsetof = 0
bool b[2]; // offsetof = 8
vec3 v1; // offsetof = 12
uint u; // offsetof = 15
S s; // offsetof = 16
float f2; // offsetof = 18
vec2 v2; // offsetof = 20
dvec3 dv; // offsetof = 24
} bo; // sizeof = 32, alignof = 8
デフォルトのレイアウトはすでに非常に優れていますが、場合によっては、次のフィールドのオフセットを手動で変更することもできます。現時点ではこれを使用する必要があります offset
。ただし、コンパイラは、手動で設定されたオフセットが他のフィールドと重複するかどうかをチェックしません。
layout(binding = 0, std430) buffer BufferObject {
mat2x3 m; // offsetof = 0
bool b[2]; // offsetof = 8
layout(offset = 48) uint u; // offsetof = 12
vec2 v; // offsetof = 14
layout(offset = 0) int i; // offset = 0
} bo;
align
CPU の使用法も、上記の CPU での使用法と同様です。
layout(binding = 0, std430) buffer BufferObject {
vec2 a; // offsetof = 0
layout(align = 16) float b; // offsetof = 4
} bo; // sizeof = 8, alignof = 4
位置
この位置は、各シェーダデータ送信の格納ポイントに相当し、その番号に従って、前のシェーダ in
と次のシェーダとが 一致するout
。シェーダー内で同じ場所を複数回宣言することはできません。in と out は完全に異なる場所です。
layout(location = 0) in vec2 i;
// layout(location = 0) in vec2 i2; // error
layout(location = 0) out vec2 o; // okay
ロケーションのサイズは 4 ワードです。宣言された各変数は 1 つの位置を占有し、変数のサイズが 4 ワードを超える場合は、次の位置を占有します。
layout(location = 0) in dvec4 dv;
// location = 1, occupied by dv
// layout(location = 1) in vec4 v; // error
layout(location = 2) in vec4 v;
配列の各要素は位置を占め、要素が占める位置の値は順番にインクリメントされます。
layout(location = 0) in float a[2];
// location = 1, occupied by a[1]
layout(location = 2) in float f1;
layout(location = 3) in mat2 m[2]; // cxr matrix is equialent to c-elements array of r-vector
// location = 4, occupied by m[0]
// location = 5, occupied by m[1]
// location = 6, occupied by m[1]
layout(location = 7) in float f2;
いちいち位置を指定するのは面倒なので、 block
最初の変数の初期位置値を指定して、他の変数の位置値が自動的に増加するようにすることができます。
layout(location = 3) in block {
float a[2]; // location = 3
mat2 m; // location = 5
vec2 v; // location = 7
layout(location = 0) mat2 m2; // location = 0
bool b; // location = 2
// vec3 v3; // error
layout(location = 8) vec3 v3; // location = 8
};
struct を使用して場所をインクリメントすることもできますが、異なる点は、struct で場所を指定できないことです。
layout(locaton = 3) in struct {
vec3 a; // location = 3
mat2 b; // location = 4, 5
// layout(location = 6) vec2 c; // error
};
ロケーションのサイズは 4 ワードであると前に述べましたが、ロケーションが変数の格納にその一部のみを使用する場合、明らかに非効率です。component
ロケーション内の変数のオフセットを指定できます。ただし、コンポーネントのオフセット後の残りの部分は変数を格納できなければならないことに注意してください。
layout(location = 0, component = 0) in float x; // l = 0, c = 0
layout(location = 0, component = 1) in float y; // l = 0, c = 1
layout(location = 0, component = 2) in float z; // l = 0, c = 2
layout(location = 1) in vec2 a; // l = 1, c = 0
// layout(location = 1, component = 2) in dvec3 b; // error
layout(location = 2, component = 0) in float b; // l = 2, c = 0
layout(location = 2, component = 1) in vec3 c; // l = 2, c = 1
配列のコンポーネントが指定されている場合、配列の各要素は引き続き各位置を増分的に占有しますが、各位置の開始位置は指定されたコンポーネントになります。
layout(location = 0, component = 2) in float f[6]; // every element c = 2
// layout(location = 2, component = 0) in vec4 v; // error
layout(location = 1, component = 0) in vec2 v; // l = 1, c = 0
// f[1] at location 1, component 2
GLM および GLSLang を使用したデータの受け渡し
この記事を書いた理由は完全に、ホストとデバイス間でデータを転送するときにアライメント関連のバグが発生したためです。
struct PCO {
uint32_t time; // offsetof = 0
::glm::vec2 extent; // offsetof = 4
}; // sizeof = 12, alignof = 4
layout(push_constant) uniform PCO {
int time; // offsetof = 0
vec2 extent; // offsetof = 2
}; // sizeof = 4, alignof = 2
コードに問題がないことを何度も確認した後、 time
フィールドと extent
フィールドを交換してみると、プログラムは正常に実行できます。明らかに、ホストの位置合わせがデバイスの位置合わせと一致していません。SPIRV を使用して packed
メモリ サイズを圧縮することはできないため、調整は手動でのみ行うことができます。
以前の研究を通じて、この問題を解決するためのより洗練された方法がいくつかあります。
- ビットフィールドを使用してホールを生成し、構造を glsl のレイアウトと強制的に一致させます。
- 指定されたフィールドは、glsl のアライメント サイズと一致しています。
struct PCO {
uint32_t time; // offsetof = 0
uint32_t : 1, : 0;
::glm::vec2 extent; // offsetof = 8
}; // sizeof = 16, alignof = 4
struct PCO {
uint32_t time; // offsetof = 0
alignas(8)::glm::vec2 extent; // offsetof = 8
}; // sizeof = 16, alignof = 8