2016-08-15 44 views
5

配列を別の小さな配列の値に基づいて更新するコードがあります。 c5バイト構造体へのアクセスは8バイトよりもはるかに遅い

for (var i = 0; i < result.Length; i++) 
    { 
    var c = cards[i]; 
    result[i] -= one[c.C0] + one[c.C1]; 
    } 

はデッキからカードを表すバイトのペアである構造体です。

private void TestCards2(int testRepetitions, float[] result, float[] one, Cards[] cards) 
{ 
    for (var r = 0; r < testRepetitions; r++) 
    for (var i = 0; i < result.Length; i++) 
    { 
     var c = cards[i]; 
     result[i] -= one[c.C0] + one[c.C1]; 
    } 
} 

設定testRepetitions = 25万、とのアレイを使用して: oneは、(デッキから52枚のカードの各々に対するエントリを持つ)52のアレイサイズ

私はこのコードをプロファイルするベンチマークを書いてあります256要素(result.Length = 256)、私のマシンでは約8.5秒で実行されます。私は5枚のカード(5バイト)を保持するために、その構造体を変更すると、同じベンチマークは今〜13Sを取る

struct Cards 
{ 
    public byte C0; 
    public byte C1; 

    public Cards(byte c0, byte c1) 
    { 
    C0 = c0; 
    C1 = c1; 
    } 
} 

:ここ

Cards構造体です。 なぜそれが起こりますか?計算は同じで、残っている3枚のカードは使用されておらず、すべての配列はL1キャッシュに収まるのに十分小さい。

さらに見知らぬ人は、カードを8バイト保持するように変更すると、ベンチマークが10秒かかります。

マイセットアップ:ここで何が起こっているか

Test With 2 Cards. Time = 8582 ms 
Test With 5 Cards. Time = 12910 ms 
Test With 8 Cards. Time = 10180 ms 

VS 2015 Update 3. 
.NET 4.6.2 
Release Build x64 
CPU: Haswell i7-5820K CPU @ 3.30GHz 

ここで私が得た正確なタイミングがありますか?

ベンチマークコード:

class TestAdjustment 
    { 
    public void Test() 
    { 
     using (Process p = Process.GetCurrentProcess()) 
     p.PriorityClass = ProcessPriorityClass.High; 

     var size = 256; 

     float[] one = ArrayUtils.CreateRandomFloatArray(size:52); 
     int[] card0 = ArrayUtils.RandomIntArray(size, minValue:0, maxValueInclusive:51); 
     int[] card1 = ArrayUtils.RandomIntArray(size, minValue: 0, maxValueInclusive: 51); 

     Cards[] cards = CreateCardsArray(card0, card1); 
     Cards5[] cards5 = CreateCards5Array(card0, card1); 
     Cards8[] cards8 = CreateCards8Array(card0, card1); 

     float[] result = ArrayUtils.CreateRandomFloatArray(size); 
     float[] resultClone = result.ToArray(); 


     var testRepetitions = 25*1000*1000; 

     var sw = Stopwatch.StartNew(); 


     TestCards2(testRepetitions, result, one, cards); 
     WriteLine($"Test With 2 Cards. Time = {sw.ElapsedMilliseconds} ms"); 
     result = resultClone.ToArray(); //restore original array from the clone, so that next method works on the same data 
     sw.Restart(); 


     TestCards5(testRepetitions, result, one, cards5); 
     WriteLine($"Test With 5 Cards. Time = {sw.ElapsedMilliseconds} ms"); 
     result = resultClone.ToArray(); 
     sw.Restart(); 


     TestCards8(testRepetitions, result, one, cards8); 
     WriteLine($"Test With 8 Cards. Time = {sw.ElapsedMilliseconds} ms"); 


    } 



    private void TestCards2(int testRepetitions, float[] result, float[] one, Cards[] cards) 
    { 
     for (var r = 0; r < testRepetitions; r++) 
     for (var i = 0; i < result.Length; i++) 
     { 
      var c = cards[i]; 
      result[i] -= one[c.C0] + one[c.C1]; 
     } 
    } 

    private void TestCards5(int testRepetitions, float[] result, float[] one, Cards5[] cards) 
    { 
     for (var r = 0; r < testRepetitions; r++) 
     for (var i = 0; i < result.Length; i++) 
     { 
      var c = cards[i]; 
      result[i] -= one[c.C0] + one[c.C1]; 
     } 
    } 


    private void TestCards8(int testRepetitions, float[] result, float[] one, Cards8[] cards) 
    { 
     for (var r = 0; r < testRepetitions; r++) 
     for (var i = 0; i < result.Length; i++) 
     { 
      var c = cards[i]; 
      result[i] -= one[c.C0] + one[c.C1]; 
     } 
    } 


    private Cards[] CreateCardsArray(int[] c0, int[] c1) 
    { 
     var result = new Cards[c0.Length]; 
     for (var i = 0; i < result.Length; i++) 
     result[i] = new Cards((byte)c0[i], (byte)c1[i]); 

     return result; 
    } 

    private Cards5[] CreateCards5Array(int[] c0, int[] c1) 
    { 
     var result = new Cards5[c0.Length]; 
     for (var i = 0; i < result.Length; i++) 
     result[i] = new Cards5((byte)c0[i], (byte)c1[i]); 

     return result; 
    } 

    private Cards8[] CreateCards8Array(int[] c0, int[] c1) 
    { 
     var result = new Cards8[c0.Length]; 
     for (var i = 0; i < result.Length; i++) 
     result[i] = new Cards8((byte)c0[i], (byte)c1[i]); 

     return result; 
    } 
    } 


    struct Cards 
    { 
    public byte C0; 
    public byte C1; 

    public Cards(byte c0, byte c1) 
    { 
     C0 = c0; 
     C1 = c1; 
    } 
    } 

    struct Cards5 
    { 
    public byte C0, C1, C2, C3, C4; 

    public Cards5(byte c0, byte c1) 
    { 
     C0 = c0; 
     C1 = c1; 
     C2 = C3 = C4 = 0; 
    } 
    } 

    struct Cards8 
    { 
    public byte C0, C1, C2, C3, C4, C5, C6, C7; 


    public Cards8(byte c0, byte c1) 
    { 
     C0 = c0; 
     C1 = c1; 
     C2 = C3 = C4 = C5 = C6 = C7 = 0; 
    } 
    } 

編集 私は億回の繰り返しで、もう一度ベンチマークを再実行してきました。

Test With 5 Cards. Time = 52245 ms 
Test With 8 Cards. Time = 40531 ms 

と逆の順序で:ここでの結果はある

Test With 8 Cards. Time = 41041 ms 
Test With 5 Cards. Time = 52034 ms 

は4 Proの表面上でそれを実行する(Skylakeマイクロアーキテクチャi7-6650Uターボはブーストへ〜3.4GHz以上):

Test With 8 Cards. Time = 47913 ms 
Test With 5 Cards. Time = 55182 ms 

違いはありますし、順序にも依存しません。

インテルVTuneを使用してプロファイリングを実行し、「5カード」バージョンの場合は0.3、「8カード」の場合は0.27と表示されます。

Edit2初期ランダム配列を作成するためのArrayUtilsクラスが追加されました。

public static class ArrayUtils 
    { 
    static Random rand = new Random(137); 

    public static float[] CreateRandomFloatArray(int size) 
    { 
     var result = new float[size]; 
     for (int i = 0; i < size; i++) 
     result[i] = (float) rand.NextDouble(); 

     return result; 
    } 

    public static int[] RandomIntArray(int size, int minValue, int maxValueInclusive) 
    { 
     var result = new int[size]; 
     for (int i = 0; i < size; i++) 
     result[i] = rand.Next(minValue, maxValueInclusive + 1); 

     return result; 

    } 
    } 
+1

この問題は再現できません。 Test with 8 Cardsは最も速いですが、2枚のカードでテストするのが最も時間がかかります。私はこのことを説明する方法も知らないかもしれません:)おそらく、あなたのケースはこの行の浅いコピーに関係しています: 'var c = cards [i];'。 5または2バイトのプロパティを持つより8つのプロパティを持つ構造体をシャローコピーするには、より多くの時間がかかります。 –

+0

@ Yeldar私のベンチマークでは、5バイトの構造体は8バイトより遅く、2バイトは最速です。 – Michal

+1

ベンチマークなど*非常に*高速なコードはあまりにも難しいです。 2回と8回のテストの違いは、1回の割り当てにつきわずか0.25ナノ秒であり、クロックスピードの数倍ですらありません。テストを並べ替えるだけで、任意の結果が得られます。あなたが実際にテストしているのは、あなたのマシンが十分にプロセッサを冷却する能力です。ファンをオンにするのが少し遅いようですが、これは珍しいことではありません。より一貫性のある結果を望むなら、熱をあまり上げないでください。2500万がそれをより良くしません。そしてケースを開き、ダストバニーを吸う。 –

答えて

19

これは、アラインされていないメモリアクセスに関するものです。アライメントされていないメモリ準備完了待ち時間は、整列されたメモリ読出待ち時間よりも大きい。実験を完了するには、構造体Cards3Cards4などを追加してみましょう。対応する配列がどのようにメモリ内に表現されているかを見てみましょう。

enter image description here

次は、あなたのベンチマークを改善しましょう。

  1. 我々は(このツールは、そうでウォームアップ、反復量の自動選択、統計計算、などベンチマークルーチンの多くを行います)BenchmarkDotNetを使用します。
  2. すべてのCards2 .. Cards8アレイについてベンチマークを行います。そのうち3つだけでなく、
  3. また、Full .NET Framework(LegacyJIT-x86、LegacyJIT-x64、RyuJIT-x64)およびMono用のすべてのJITコンパイラを確認します。ここで

私の環境です:

Host Process Environment Information: 
BenchmarkDotNet.Core=v0.9.9.0 
OS=Microsoft Windows NT 6.2.9200.0 
Processor=Intel(R) Core(TM) i7-4810MQ CPU 2.80GHz, ProcessorCount=8 
Frequency=2728068 ticks, Resolution=366.5598 ns, Timer=TSC 
CLR1=MS.NET 4.0.30319.42000, Arch=64-bit RELEASE [RyuJIT] 
CLR2=Mono JIT compiler version 4.4.0, Arch=32-bit 
GC=Concurrent Workstation 
JitModules=clrjit-v4.6.1080.0 

そして私の結果:

Method | Platform |  Jit | Toolchain | Runtime | Median | StdDev | 
------- |--------- |---------- |---------- |-------- |---------- |---------- | 
    C2 |  Host |  Host |  Mono | Mono | 3.9230 ns | 0.0532 ns | 
    C3 |  Host |  Host |  Mono | Mono | 4.8223 ns | 0.0920 ns | 
    C4 |  Host |  Host |  Mono | Mono | 5.9149 ns | 0.1207 ns | 
    C5 |  Host |  Host |  Mono | Mono | 6.3981 ns | 0.0913 ns | 
    C6 |  Host |  Host |  Mono | Mono | 7.1179 ns | 0.1222 ns | 
    C7 |  Host |  Host |  Mono | Mono | 7.6318 ns | 0.1269 ns | 
    C8 |  Host |  Host |  Mono | Mono | 8.4650 ns | 0.1497 ns | 
    C2 |  X64 | LegacyJit |  Host | Host | 2.3515 ns | 0.0150 ns | 
    C3 |  X64 | LegacyJit |  Host | Host | 4.2553 ns | 0.0700 ns | 
    C4 |  X64 | LegacyJit |  Host | Host | 1.4366 ns | 0.0385 ns | 
    C5 |  X64 | LegacyJit |  Host | Host | 2.3688 ns | 0.0359 ns | 
    C6 |  X64 | LegacyJit |  Host | Host | 2.3684 ns | 0.0404 ns | 
    C7 |  X64 | LegacyJit |  Host | Host | 3.0404 ns | 0.0664 ns | 
    C8 |  X64 | LegacyJit |  Host | Host | 1.4510 ns | 0.0333 ns | 
    C2 |  X64 | RyuJit |  Host | Host | 1.9281 ns | 0.0306 ns | 
    C3 |  X64 | RyuJit |  Host | Host | 2.1183 ns | 0.0348 ns | 
    C4 |  X64 | RyuJit |  Host | Host | 1.9395 ns | 0.0397 ns | 
    C5 |  X64 | RyuJit |  Host | Host | 2.7706 ns | 0.0387 ns | 
    C6 |  X64 | RyuJit |  Host | Host | 2.6471 ns | 0.0513 ns | 
    C7 |  X64 | RyuJit |  Host | Host | 2.9743 ns | 0.0541 ns | 
    C8 |  X64 | RyuJit |  Host | Host | 2.6280 ns | 0.1526 ns | 
    C2 |  X86 | LegacyJit |  Host | Host | 3.0854 ns | 0.2172 ns | 
    C3 |  X86 | LegacyJit |  Host | Host | 3.1627 ns | 0.1126 ns | 
    C4 |  X86 | LegacyJit |  Host | Host | 3.0577 ns | 0.0929 ns | 
    C5 |  X86 | LegacyJit |  Host | Host | 5.0957 ns | 0.1601 ns | 
    C6 |  X86 | LegacyJit |  Host | Host | 6.1723 ns | 0.1177 ns | 
    C7 |  X86 | LegacyJit |  Host | Host | 7.1155 ns | 0.0803 ns | 
    C8 |  X86 | LegacyJit |  Host | Host | 3.7703 ns | 0.1276 ns | 

enter image description here

完全なソースコード:

using System; 
using System.Linq; 
using BenchmarkDotNet.Attributes; 
using BenchmarkDotNet.Attributes.Exporters; 
using BenchmarkDotNet.Attributes.Jobs; 
using BenchmarkDotNet.Running; 

[LegacyJitX86Job, LegacyJitX64Job, RyuJitX64Job, MonoJob] 
[RPlotExporter] 
public class CardBenchmarks 
{ 
    public const int Size = 256; 

    private float[] result, one; 
    private Cards2[] cards2; 
    private Cards3[] cards3; 
    private Cards4[] cards4; 
    private Cards5[] cards5; 
    private Cards6[] cards6; 
    private Cards7[] cards7; 
    private Cards8[] cards8; 

    [Setup] 
    public void Setup() 
    { 
    result = ArrayUtils.CreateRandomFloatArray(Size); 
    one = ArrayUtils.CreateRandomFloatArray(size: 52); 
    var c0 = ArrayUtils.RandomByteArray(Size, minValue: 0, maxValueInclusive: 51); 
    var c1 = ArrayUtils.RandomByteArray(Size, minValue: 0, maxValueInclusive: 51); 

    cards2 = CardUtls.Create2(c0, c1); 
    cards3 = CardUtls.Create3(c0, c1); 
    cards4 = CardUtls.Create4(c0, c1); 
    cards5 = CardUtls.Create5(c0, c1); 
    cards6 = CardUtls.Create6(c0, c1); 
    cards7 = CardUtls.Create7(c0, c1); 
    cards8 = CardUtls.Create8(c0, c1); 
    } 

    [Benchmark(OperationsPerInvoke = Size)] 
    public void C2() 
    { 
    for (var i = 0; i < result.Length; i++) 
    { 
     var c = cards2[i]; 
     result[i] -= one[c.C0] + one[c.C1]; 
    } 
    } 

    [Benchmark(OperationsPerInvoke = Size)] 
    public void C3() 
    { 
    for (var i = 0; i < result.Length; i++) 
    { 
     var c = cards3[i]; 
     result[i] -= one[c.C0] + one[c.C1]; 
    } 
    } 

    [Benchmark(OperationsPerInvoke = Size)] 
    public void C4() 
    { 
    for (var i = 0; i < result.Length; i++) 
    { 
     var c = cards4[i]; 
     result[i] -= one[c.C0] + one[c.C1]; 
    } 
    } 

    [Benchmark(OperationsPerInvoke = Size)] 
    public void C5() 
    { 
    for (var i = 0; i < result.Length; i++) 
    { 
     var c = cards5[i]; 
     result[i] -= one[c.C0] + one[c.C1]; 
    } 
    } 

    [Benchmark(OperationsPerInvoke = Size)] 
    public void C6() 
    { 
    for (var i = 0; i < result.Length; i++) 
    { 
     var c = cards6[i]; 
     result[i] -= one[c.C0] + one[c.C1]; 
    } 
    } 

    [Benchmark(OperationsPerInvoke = Size)] 
    public void C7() 
    { 
    for (var i = 0; i < result.Length; i++) 
    { 
     var c = cards7[i]; 
     result[i] -= one[c.C0] + one[c.C1]; 
    } 
    } 

    [Benchmark(OperationsPerInvoke = Size)] 
    public void C8() 
    { 
    for (var i = 0; i < result.Length; i++) 
    { 
     var c = cards8[i]; 
     result[i] -= one[c.C0] + one[c.C1]; 
    } 
    } 
} 

public static class ArrayUtils 
{ 
    private static readonly Random Rand = new Random(137); 

    public static float[] CreateRandomFloatArray(int size) 
    { 
    var result = new float[size]; 
    for (int i = 0; i < size; i++) 
     result[i] = (float) Rand.NextDouble(); 
    return result; 
    } 

    public static byte[] RandomByteArray(int size, int minValue, int maxValueInclusive) 
    { 
    var result = new byte[size]; 
    for (int i = 0; i < size; i++) 
     result[i] = (byte) Rand.Next(minValue, maxValueInclusive + 1); 
    return result; 
    } 
} 

public static class CardUtls 
{ 
    private static T[] Create<T>(int length, Func<int, T> create) => Enumerable.Range(0, length).Select(create).ToArray(); 

    public static Cards2[] Create2(byte[] c0, byte[] c1) => Create(c0.Length, i => new Cards2 {C0 = c0[i], C1 = c1[i]}); 
    public static Cards3[] Create3(byte[] c0, byte[] c1) => Create(c0.Length, i => new Cards3 {C0 = c0[i], C1 = c1[i]}); 
    public static Cards4[] Create4(byte[] c0, byte[] c1) => Create(c0.Length, i => new Cards4 {C0 = c0[i], C1 = c1[i]}); 
    public static Cards5[] Create5(byte[] c0, byte[] c1) => Create(c0.Length, i => new Cards5 {C0 = c0[i], C1 = c1[i]}); 
    public static Cards6[] Create6(byte[] c0, byte[] c1) => Create(c0.Length, i => new Cards6 {C0 = c0[i], C1 = c1[i]}); 
    public static Cards7[] Create7(byte[] c0, byte[] c1) => Create(c0.Length, i => new Cards7 {C0 = c0[i], C1 = c1[i]}); 
    public static Cards8[] Create8(byte[] c0, byte[] c1) => Create(c0.Length, i => new Cards8 {C0 = c0[i], C1 = c1[i]}); 
} 

public struct Cards2 
{ 
    public byte C0, C1; 
} 

public struct Cards3 
{ 
    public byte C0, C1, C2; 
} 

public struct Cards4 
{ 
    public byte C0, C1, C2, C3; 
} 

public struct Cards5 
{ 
    public byte C0, C1, C2, C3, C4; 
} 

public struct Cards6 
{ 
    public byte C0, C1, C2, C3, C4, C5; 
} 

public struct Cards7 
{ 
    public byte C0, C1, C2, C3, C4, C5, C6; 
} 

public struct Cards8 
{ 
    public byte C0, C1, C2, C3, C4, C5, C6, C7; 
} 


class Program 
{ 
    static void Main() 
    { 
    BenchmarkRunner.Run<CardBenchmarks>(); 
    } 
} 

Answer

ご覧のとおり、ベンチマークは厳しいものですが、パフォーマンスに影響するさまざまな要因があります。最も重要なことの1つは、ランタイムがデータをレイアウトする方法です。たとえば、MonoとFull Frameworkには異なるレイアウトアルゴリズムがあるため(MonoではMarshal.SizeOf(typeof(Card2)) == 8)、記述されているMonoの動作を監視しません。

Card5は、Card8(最初の画像を参照)とは異なり、Card5が多くのアライメントのずれた読み取りを生成するため、フルフレームワークにはTime(Card5) > Time(Card8)があります。 the commentから

更新

質問:

3バイトは、あなたのRyuJIT64ベンチマークで8バイトよりも良好に動作する理由を任意のアイデア?

のは、ASMコードを見てみましょう:C3場合

; RyuJIT-x64, C3 
       var c = cards3[i]; 
00007FFEDF0CADCE mov   r10,r9 
00007FFEDF0CADD1 mov   r11d,dword ptr [r10+8] 
00007FFEDF0CADD5 cmp   eax,r11d 
00007FFEDF0CADD8 jae   00007FFEDF0CAE44 
00007FFEDF0CADDA movsxd  r11,eax 
00007FFEDF0CADDD imul  r11,r11,3 
00007FFEDF0CADE1 lea   r10,[r10+r11+10h] 
00007FFEDF0CADE6 movzx  r11d,byte ptr [r10]   ; !!! 
00007FFEDF0CADEA movzx  r10d,byte ptr [r10+1]  ; !!! 

; RyuJIT-x64, C8 
       var c = cards8[i]; 
00007FFEDF0CAE8C mov   rdx,qword ptr [rcx+48h] 
00007FFEDF0CAE90 mov   r8d,dword ptr [rdx+8] 
00007FFEDF0CAE94 cmp   eax,r8d 
00007FFEDF0CAE97 jae   00007FFEDF0CAF0C 
00007FFEDF0CAE99 movsxd  r8,eax 
00007FFEDF0CAE9C mov   rdx,qword ptr [rdx+r8*8+10h] ; !!! 
00007FFEDF0CAEA1 mov   qword ptr [rsp+28h],rdx  ; !!! 

、RyuJITはr11dr10dレジスタにターゲットバイトを保持します。 C8の場合、RyuJITはそれらをスタックに保持します(qword ptr [rsp+28h])。説明:現在のRyuJIT(私の場合はv4.6.1080.0)は、4フィールド以下の構造体のスカラー置換を行います(coreclr#6839参照)。従って、C5,C6,C7及びC8のRyuJIT性能は、C2,C3,C4の性能より悪い。注意:この動作は、RyuJITの将来のバージョンで変更される可能性があります。

+0

詳細な回答ありがとうございます。私は8バイトが5バイトに対してどのようにして実行されたかのために、メモリのアラインメントが疑われました。 RyuJIT64ベンチマークで3バイトが8バイトより優れている理由は何ですか? – Michal

+0

@Michal、私の更新を参照してください。 – AndreyAkinshin

+1

@Michal、ここに対応するcoreclrの問題です:https://github.com/dotnet/coreclr/issues/6839 – AndreyAkinshin

1

これはメモリの配置と関係があります。

技術情報:

いくつかのアーキテクチャは、例えば、MIPSアーキテクチャのため、実際にはメモリで一度に1つだけのバイトを変更することはできません。レジスタにデータのワードをロードし、無関係のビットをマスクして計算を実行する必要があります。

実際にこの問題を回避するため、バイトではなく通常のintを使用すると、速度が向上することがあります。

関連する問題