docs.unity3d.com
    目次を表示する/隠す

    NoAlias 属性

    [NoAlias] 属性を使用することで、ポインターおよび構造体のエイリアシングに関する追加情報を Burst に伝えることができます。

    ほとんどのユースケースでは、[NoAlias] 属性を使用する必要はありません。[NativeContainer] 属性の構造体や、ジョブ構造体のフィールドでは、この属性を使用する必要はありません。これは、Burst コンパイラーによってエイリアスがないという情報が推論されるためです。

    [NoAlias] 属性が公開されている理由は、Burst ではエイリアシングを推論できない複雑なデータ構造体を構築できるようにするためです。別のポインターのエイリアスになる可能性のあるポインターに [NoAlias] 属性を使用すると、未定義の動作が発生し、バグの追跡が困難になる可能性があります。

    この属性の使い方は以下のとおりです。

    • 関数のパラメーターで、そのパラメーターが関数の他のパラメーターのエイリアスにならないことを示す。
    • 構造体のフィールドで、そのフィールドが構造体の他のフィールドのエイリアスにならないことを示す。
    • 構造体で、構造体のアドレスがその構造体自体の中に出現しないことを示す。
    • 関数の戻り値で、返されるポインターが同じ関数から返される他のポインターのエイリアスにならないことを示す。

    NoAlias 関数パラメーター

    以下は、エイリアシングの例です。

    int Foo(ref int a, ref int b)
    {
        b = 13;
        a = 42;
        return b;
    }
    

    この場合、Burst によって以下のようなアセンブリが生成されます。

    mov     dword ptr [rdx], 13
    mov     dword ptr [rcx], 42
    mov     eax, dword ptr [rdx]
    ret
    

    これは、Burst が以下の処理を実行することを意味します。

    • 13 を b に格納する。
    • 42 を a に格納する。
    • b の値を再ロードして返す。

    Burst が b を再ロードする必要があるのは、a と b が同じメモリに確保されているかどうかを認識できないためです。

    以下のように [NoAlias] 属性をコードに追加して変更します。

    int Foo([NoAlias] ref int a, ref int b)
    {
        b = 13;
        a = 42;
        return b;
    }
    

    この場合、Burst によって以下のようなアセンブリが生成されます。

    mov     dword ptr [rdx], 13
    mov     dword ptr [rcx], 42
    mov     eax, 13
    ret
    

    今度は、b のロードが、リターンレジスタに定数 13 を移動する処理で置き換えられています。

    NoAlias 構造体フィールド

    以下の例は前の例と同じものですが、適用対象が構造体になっています。

    struct Bar
    {
        public NativeArray<int> a;
        public NativeArray<float> b;
    }
    
    int Foo(ref Bar bar)
    {
        bar.b[0] = 42.0f;
        bar.a[0] = 13;
        return (int)bar.b[0];
    }
    

    この場合、Burst によって以下のようなアセンブリが生成されます。

    mov     rax, qword ptr [rcx + 16]
    mov     dword ptr [rax], 1109917696
    mov     rcx, qword ptr [rcx]
    mov     dword ptr [rcx], 13
    cvttss2si       eax, dword ptr [rax]
    ret
    

    この場合、Burst は以下を実行します。

    • b のデータのアドレスを rax にロードする。
    • 42 をこれにを格納する (1109917696 は 0x42280000 なので 42.0f となる)。
    • a のデータのアドレスを rcx にロードする。
    • 13 をこれに格納する。
    • b のデータを再ロードして、返す準備として整数に変換する。

    2 つの NativeArrays が同じメモリに確保されないことがわかっている場合は、コードを以下のように変更できます。

    struct Bar
    {
        [NoAlias]
        public NativeArray<int> a;
    
        [NoAlias]
        public NativeArray<float> b;
    }
    
    int Foo(ref Bar bar)
    {
        bar.b[0] = 42.0f;
        bar.a[0] = 13;
        return (int)bar.b[0];
    }
    

    a と b の両方に [NoAlias] 属性を設定すると、Burst はこの 2 つが構造体内で互いのエイリアスにならないものと認識し、以下のようなアセンブリを生成します。

    mov     rax, qword ptr [rcx + 16]
    mov     dword ptr [rax], 1109917696
    mov     rax, qword ptr [rcx]
    mov     dword ptr [rax], 13
    mov     eax, 42
    ret
    

    つまり、Burst からは整数の定数 42 が返されます。

    NoAlias 構造体

    Burst では、構造体へのポインターが構造体自体の中に出現しないことを前提にしています。ただし、以下のようにこの前提が成立しない場合もあります。

    unsafe struct CircularList
    {
        public CircularList* next;
    
        public CircularList()
        {
            // "空の" リストはそれ自体を指します。
            next = this;
        }
    }
    

    リストは、通常ではこの構造体へのポインターに対して構造体自体の内部のどこからでもアクセス可能である、数少ない構造体の 1 つです。

    以下に、構造体に [NoAlias] を設定すると役立つ場合の例を示します。

    unsafe struct Bar
    {
        public int i;
        public void* p;
    }
    
    float Foo(ref Bar bar)
    {
        *(int*)bar.p = 42;
        return ((float*)bar.p)[bar.i];
    }
    

    これにより、以下のアセンブリが生成されます。

    mov     rax, qword ptr [rcx + 8]
    mov     dword ptr [rax], 42
    mov     rax, qword ptr [rcx + 8]
    mov     ecx, dword ptr [rcx]
    movss   xmm0, dword ptr [rax + 4*rcx]
    ret
    

    この場合、Burst は以下を実行します。

    • p を rax にロードする。
    • 42 を p に格納する。
    • p を rax に再ロードする。
    • i を ecx にロードする。
    • p にあるインデックスの i 番目の要素を返す。

    この場合、Burst は p を 2 回ロードします。これは、p が構造体 bar のアドレスを指しているかどうかが Burst にはわからないためです。一度 p に 42 を格納してから、bar から p のアドレスを再ロードする必要があるため、処理には大きなコストがかかります。

    これを回避するために [NoAlias] を追加します。

    [NoAlias]
    unsafe struct Bar
    {
        public int i;
        public void* p;
    }
    
    float Foo(ref Bar bar)
    {
        *(int*)bar.p = 42;
        return ((float*)bar.p)[bar.i];
    }
    

    これにより、以下のアセンブリが生成されます。

    mov     rax, qword ptr [rcx + 8]
    mov     dword ptr [rax], 42
    mov     ecx, dword ptr [rcx]
    movss   xmm0, dword ptr [rax + 4*rcx]
    ret
    

    こうすると、p が bar へのポインターになりえないことが [NoAlias] により明示されるので、Burst が p のアドレスをロードする回数は 1 回だけになります。

    NoAlias 関数戻り値

    関数によっては、一意のポインターしか返せないものがあります。例えば、malloc は一意のポインターしか返しません。この場合、[return:NoAlias] を使うと Burst に有益な情報を提供できます。

    Important

    [return: NoAlias] は、一意のポインターを生成することが保証されている関数でのみ使用してください。例えば、バンプアロケーションや、malloc などの関数です。Burst はパフォーマンスを考慮し、積極的に関数をインライン化します。そのため、小さな関数は、属性なしでも結果が変わらないようにその親にインライン化されます。

    以下の例では、スタックアロケーションでメモリを確保したバンプアロケーターを使用しています。

    // スタック領域が割り当てられたメモリに一意のアドレスを返すだけです。
    // Burst はこのような小さな関数を常にインライン化しようとするため、
    // この例の目的を達することができないので、
    // この関数は非インラインにしています
    [MethodImpl(MethodImplOptions.NoInlining)]
    unsafe int* BumpAlloc(int* alloca)
    {
        int location = alloca[0]++;
        return alloca + location;
    }
    
    unsafe int Func()
    {
        int* alloca = stackalloc int[128];
    
        // alloca の開始時にサイズを格納します。
        alloca[0] = 1;
    
        int* ptr1 = BumpAlloc(alloca);
        int* ptr2 = BumpAlloc(alloca);
    
        *ptr1 = 42;
        *ptr2 = 13;
    
        return *ptr1;
    }
    

    これにより、以下のアセンブリが生成されます。

    push    rsi
    push    rdi
    push    rbx
    sub     rsp, 544
    lea     rcx, [rsp + 36]
    movabs  rax, offset memset
    mov     r8d, 508
    xor     edx, edx
    call    rax
    mov     dword ptr [rsp + 32], 1
    movabs  rbx, offset "BumpAlloc(int* alloca)"
    lea     rsi, [rsp + 32]
    mov     rcx, rsi
    call    rbx
    mov     rdi, rax
    mov     rcx, rsi
    call    rbx
    mov     dword ptr [rdi], 42
    mov     dword ptr [rax], 13
    mov     eax, dword ptr [rdi]
    add     rsp, 544
    pop     rbx
    pop     rdi
    pop     rsi
    ret
    

    Burst が実行する主な処理は以下のとおりです。

    • ptr1 を rdi に保持する。
    • ptr2 を rax に保持する。
    • 42 を ptr1 に格納する。
    • 13 を ptr2 に格納する。
    • ptr1 を再ロードして返す。

    [return: NoAlias] 属性を追加すると、以下のようになります。

    [MethodImpl(MethodImplOptions.NoInlining)]
    [return: NoAlias]
    unsafe int* BumpAlloc(int* alloca)
    {
        int location = alloca[0]++;
        return alloca + location;
    }
    
    unsafe int Func()
    {
        int* alloca = stackalloc int[128];
    
        // alloca の開始時にサイズを格納します。
        alloca[0] = 1;
    
        int* ptr1 = BumpAlloc(alloca);
        int* ptr2 = BumpAlloc(alloca);
    
        *ptr1 = 42;
        *ptr2 = 13;
    
        return *ptr1;
    }
    

    これにより、以下のアセンブリが生成されます。

    push    rsi
    push    rdi
    push    rbx
    sub     rsp, 544
    lea     rcx, [rsp + 36]
    movabs  rax, offset memset
    mov     r8d, 508
    xor     edx, edx
    call    rax
    mov     dword ptr [rsp + 32], 1
    movabs  rbx, offset "BumpAlloc(int* alloca)"
    lea     rsi, [rsp + 32]
    mov     rcx, rsi
    call    rbx
    mov     rdi, rax
    mov     rcx, rsi
    call    rbx
    mov     dword ptr [rdi], 42
    mov     dword ptr [rax], 13
    mov     eax, 42
    add     rsp, 544
    pop     rbx
    pop     rdi
    pop     rsi
    ret
    

    この場合、Burst は ptr2 を再ロードせず、42 をリターンレジスタに移動します。

    トップに戻る
    Copyright © 2023 Unity Technologies — 商標と利用規約
    • 法律関連
    • プライバシーポリシー
    • クッキー
    • 私の個人情報を販売または共有しない
    • Your Privacy Choices (Cookie Settings)