最初にきっかけとなったポストを示す。"why the _hacker version is faster?" ということなので、ポスト初見の場合、一度考えてもいいかもしれない。
Some JIT magic for you - both methods do the same work, why the _hacker version is faster? 🙃 pic.twitter.com/i7YdmKWJVy
— Egor Bogatov (@EgorBo) 2023年12月26日
ちなみに、ポストのベンチマークは .NET 9 で実行されているとのこと。そして、.NET 8 はそのままでは同様の最適化を受けられない。.NET 8 環境下で試したい場合、コンパイラにもう少しわかりやすく長さを示す必要がある。
public class CopyToHackBenchmark { private const int constant = 10; private readonly int[] srcArray = new int[constant]; private readonly int[] dstArray = new int[constant]; public int[] CopyToSimpleWay() { srcArray.AsSpan().CopyTo(dstArray); return dstArray; } public int[] CopyToImperfectlyHacked() { // .NET 8 では冒頭ポストのように単に Span にするだけではダメ var src = srcArray.AsSpan(); var dst = dstArray.AsSpan(); if (src.Length == constant) src.CopyTo(dst); else src.CopyTo(dst); return dstArray; } public int[] CopyToHacked() { // このようにすると同様の最適化を受けられる var src = srcArray.AsSpan(0, constant); var dst = dstArray.AsSpan(0, constant); if (src.Length == constant) src.CopyTo(dst); else src.CopyTo(dst); return dstArray; } }
試せば分かるが、確かに速くなる。
さて、"why" の答えについて。ポストのリプライで正解を告げられているのは次のポスト。
Arraycopy for "known" length is unrolled as a short sequence of individual load-store pairs?
— Aleksey Shipilëv (@shipilev) 2023年12月26日
(コンパイラが?)既知の長さについてのコピーは、一連の簡潔なロード/ストアにアンローリングされるから、というもの。アセンブリのレイヤーのお話。
正解を告げたリプライの画像を見ると、vmovdqu
命令の連続になっている。
さて、仕組みが分かったところで条件を変えて試してみた。今回は LINQPad の出力で確認した。
- .NET8, LINQPad 8, x64
#LINQPad optimize+ using System; using System.Runtime.CompilerServices; public class CopyToHackBenchmark { // 自分の環境で vmovdqu 命令の連続が消える境界 // - const を外すとダメ // - object[]: ダメ(長さ問わず) // - int[]: 32まで // - double[]: 16まで // - byte[]: 128まで private const int constant = 128; private byte[] srcArray = default!; private byte[] dstArray = default!; public void Setup() { srcArray = new byte[constant]; dstArray = new byte[constant]; } //[MethodImpl(MethodImplOptions.NoInlining)] // ダメ private static void CopyToHacked<T>(ReadOnlySpan<T> src, Span<T> dst, int size) { if (src.Length == size) src.CopyTo(dst); else src.CopyTo(dst); } public byte[] CopyToSimpleWay() { srcArray.AsSpan().CopyTo(dstArray); return dstArray; } public byte[] CopyToHacked() { var src = srcArray.AsSpan(0, constant); var dst = dstArray.AsSpan(0, constant); CopyToHacked(src, dst, constant); // 少なくともこの場合はインライン化必須 return dstArray; } }
アセンブラに明るくないので、出力を見つつ手探り。試した限りでは以下だった。
object[]
(参照型の配列)はダメ- 長さを示すのに変数はダメ(
const
(またはリテラル)の必要) - 128バイトまで(例えば
int[]
なら長さ32まで) - メソッドの呼び出しを隔てるとダメ(例えば、上記のような場合はインライン化される必要)
仕組みを確認し条件を変えて試してみたが、正直なところ知識を噛み砕けてはいない。常用できる類ではないと言い訳してとりあえずそのまま引き出しの中へ。
書いてあったら最適化であると頭には浮かぶと思うので、その点は前進。
ちなみに、Dynamic PGOで賢く同様の最適化がかかるようPRを出しているよう。
.NET 9 では気にしなくても自動的にこの恩恵を受けれているかもしれない。