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 } (...)