kubo39's blog

ただの雑記です。

LDC: LLVMとmemcpyとRVOのはなし

Addressing Rust optimization failures in LLVMのはなしで、これがLDCだとどうなるか。

以下のようなコードでどうなるかみてみる。 このコードはRustのcodegen testを移植したもの。

import std.algorithm : sum;

pragma(mangle, "ThreeSlices")
struct ThreeSlices
{
    uint[] a;
    uint[] b;
    uint[] c;
}

pragma(mangle, "sum_slices")
uint sum_slices(immutable ThreeSlices val)
{
    immutable val2 = val;
    return sum(val2);
}

pragma(inline, false)
pragma(mangle, "sum")
uint sum(immutable ref ThreeSlices val)
{
    return val.a.sum + val.b.sum + val.c.sum;
}

コンパイラがちゃんとLLVM17でビルドされてるか確認。

$ ldc2 --version | head -2
LDC - the LLVM D compiler (1.36.0):
  based on DMD v2.106.1 and LLVM 17.0.6
ldc2 -O2 --output-ll app.d

生成されるLLVM IRは以下のようになった。

(...)
; Function Attrs: nofree nosync nounwind memory(read, argmem: readwrite, inaccessiblemem: none) uwtable
define i32 @sum_slices(ptr nocapture readonly byval(%app.ThreeSlices) align 8 %val) local_unnamed_addr #2 {
  %val2 = alloca %app.ThreeSlices, align 8        ; [#uses = 2, size/byte = 48]
  call void @llvm.memcpy.p0.p0.i64(ptr noundef nonnull align 8 dereferenceable(48) %val2, ptr noundef nonnull align 8 dereferenceable(48) %val, i64 48, i1 false)
  %1 = call i32 @sum(ptr nonnull dereferenceable(48) %val2) #8 ; [#uses = 1]
  ret i32 %1
}
(...)

Rustのようにならなかった理由としてはnoalias attributeが付与されていないためである。この最適化が効くためにはnoaliasとnocaptureが付与されてなければならない。

これはimmutableなポインタにnoaliasを付与するPRが入ることで解決する。

実際にそうなるか確認してみよう。 現在リリースされているLDCではldc.attributes.restrictをつけることで同じ挙動にできる。

import std.algorithm : sum;
import ldc.attributes : restrict;

pragma(mangle, "ThreeSlices")
struct ThreeSlices
{
    uint[] a;
    uint[] b;
    uint[] c;
}

pragma(mangle, "sum_slices")
uint sum_slices(@restrict immutable ThreeSlices val)
{
    immutable val2 = val;
    return sum(val2);
}

pragma(inline, false)
pragma(mangle, "sum")
uint sum(immutable ref ThreeSlices val)
{
    return val.a.sum + val.b.sum + val.c.sum;
}

同じコンパイルオプションでビルドするとちゃんとmemcpyが排除されることが確認できる。

(...)
; [#uses = 0]
; Function Attrs: nofree nosync nounwind memory(read, argmem: readwrite, inaccessiblemem: none) uwtable
define i32 @sum_slices(ptr noalias nocapture readonly byval(%app.ThreeSlices) align 8 %val) local_unnamed_addr #2 {
  %1 = call i32 @sum(ptr nonnull dereferenceable(48) %val) #7 ; [#uses = 1]
  ret i32 %1
}
(...)

実はLDCにはこれ以外にもnoaliasが付与される条件がある。 RVOが適用されるとき、ポインタにnoaliasがつく。

そのため、以下のようなコードでもmemcpyを除去できる。 (ただしこんなコードを書いてはいけない)

import std.algorithm : sum;
import ldc.attributes : restrict;

pragma(mangle, "ThreeSlices")
struct ThreeSlices
{
    uint[] a;
    uint[] b;
    uint[] c;
}

uint gSum;

pragma(mangle, "sum_slices")
ThreeSlices sum_slices()
{
    ThreeSlices val = void;
    auto val2 = val;
    gSum = sum(val2);
    return val;
}

pragma(inline, false)
pragma(mangle, "sum")
uint sum(ref ThreeSlices val)
{
    return val.a.sum + val.b.sum + val.c.sum;
}

このコードをコンパイルすると以下のようなLLVM IRになることが確認できる。

; [#uses = 0]
; Function Attrs: nofree nosync nounwind memory(readwrite, inaccessiblemem: none) uwtable
define void @sum_slices(ptr noalias nocapture readonly sret(%app.ThreeSlices) align 8 %.sret_arg) local_unnamed_addr #2 {
  %1 = tail call i32 @sum(ptr nonnull dereferenceable(48) %.sret_arg) #7 ; [#uses = 1]
  store i32 %1, ptr @_D3app4gSumk, align 4
  ret void
}

これはRVOが適用されたときのみ起きるので、明示的に変換後のようなコードを渡すと効果はない。

// sum_slicesをこのように書き換えてもだめ
pragma(mangle, "sum_slices")
void sum_slices(ref ThreeSlices val)
{
    auto val2 = val;
    gSum = sum(val2);
}
(...)

結果を確認するとmemcpyが生成されている。

(...)
; Function Attrs: nofree nosync nounwind memory(readwrite, inaccessiblemem: none) uwtable
define void @sum_slices(ptr nocapture readonly dereferenceable(48) %val) local_unnamed_addr #2 {
  %val2 = alloca %app.ThreeSlices, align 8        ; [#uses = 2, size/byte = 48]
  call void @llvm.memcpy.p0.p0.i64(ptr noundef nonnull align 8 dereferenceable(48) %val2, ptr noundef nonnull align 1 dereferenceable(48) %val, i64 48, i1 false)
  %1 = call i32 @sum(ptr nonnull dereferenceable(48) %val2) #8 ; [#uses = 1]
  store i32 %1, ptr @_D3app4gSumk, align 4
  ret void
}
(...)