2009-01-02 82 views
3

基本上你有這樣做的方法有兩種:.NET矩形陣列:如何在循環中訪問?

for (int x = 0; x < UPPER_X; x++) 
    for (int y = 0; y < UPPER_Y; y++) 
    {   
     arr1[x, y] = get_value(); 
     arr2[y, x] = get_value(); 
    } 

唯一的區別是什麼變量在內部循環改變:第一或第二。我聽說結果因語言而異。

.NET的正確順序是什麼?

回答

3

其中一個比另一個更快的原因與處理器的緩存以及數據在內存中的佈局方式有關。

存儲二維數據的方法有兩種,一維地址空間,可以存儲第一行,第二行等所有數據(又名row major order),或者你可以通過欄目(又名column major order)來完成。以下是這兩個選項對於3x3陣列的內存位置。

行數:

1 2 3 
4 5 6 
7 8 9 

色譜柱:

1 4 7 
2 5 8 
3 6 9 

當訪問一個存儲器位置,整個高速緩存行(其可以是8個512字節之間,根據維基百科)被加載到緩存。所以如果你訪問下一個內存位置,它將已經在緩存中。因此,按順序訪問內存比在地址空間中跳轉要快得多。因此,對於大型二維數組,選擇行或列作爲內部循環變量之間可能存在顯着的速度差異。

4

你應該基準你的特定情況,以確保。

你會認爲會有矩形陣列(即連續分配的內存)無差異,但根據本MSDN article是有區別的:

通過

你可以得到更好的結果轉換多維將數組 整合到一維數組中。如果 你不介意的語法,這可能是 瑣碎;只需使用一個索引作爲 偏移量。例如,下面的 聲明瞭一個一維數組來 被用作一個二維數組:

double[] myArray = new double[ROW_DIM * COLUMN_DIM]; 

用於索引該 數組的元素,使用下面的偏移量:

myArray[row * COLUMN_DIM + column]; 

這無疑比 的速度相等價格鋸齒形或矩形 陣列。

+0

我想這應該是中編譯器的優化的範圍很好,但我不記得做在C同樣的事情在昏暗的和遙遠的過去。 – 2009-01-02 02:11:08

+0

這隻有在您比列更快地改變列時纔有效。如果反過來這樣做,則會失去參考位置並可能遭受緩存錯過處罰。 – tvanfosson 2009-01-02 04:18:39

+0

感謝您的MSDN鏈接。 – 2009-01-02 04:58:50

1

所以我做了基準測試,結果是訪問arr1的速度提高了三倍。

+0

你可以編輯你的答案或問題的基準細節?我必須看到一些非常非常有說服力的證據才能讓這個表現再次受到重視。你有與此有關的可衡量的性能問題嗎? – 2009-01-02 02:20:08

1

很有意思,我從未停下來考慮能夠簡單地通過訪問數組索引「非順序」是如此巨大的差異。我給它一個嘗試自己,也發現了下面的代碼有第二環取介於2 - 更長的時間,3:

// Hmmm, how to insert blank lines in the code formatter??? 
static void Main(string[] args) 
{ 
    Stopwatch timer = new Stopwatch(); 
    int arraySize = 10000; 

    // First array, access X by Y 
    int[,] xbyy = new int[arraySize, arraySize]; 


    timer.Start(); 
    for (int x = 0; x < arraySize; x++) 
     for (int y = 0; y < arraySize; y++) 
     { 
      xbyy[x, y] = 15; 
     } 
    timer.Stop(); 
    TimeSpan duration = timer.Elapsed; 
    string realTimeFormat = string.Format("{0:00} minutes {1:00} seconds {2:000} milliseconds", 
        duration.Minutes, duration.Seconds, duration.Milliseconds); 
    Console.WriteLine("X by Y took " + realTimeFormat); 



    // Seecond array, access Y by X 
    int[,] ybyx = new int[arraySize, arraySize]; 

    timer.Start(); 
    for (int x = 0; x < arraySize; x++) 
     for (int y = 0; y < arraySize; y++) 
     { 
      ybyx[y, x] = 15; 
     } 
    timer.Stop(); 
    duration = timer.Elapsed; 
    realTimeFormat = string.Format("{0:00} minutes {1:00} seconds {2:000} milliseconds", 
       duration.Minutes, duration.Seconds, duration.Milliseconds); 
    Console.WriteLine("Y by X took " + realTimeFormat); 


    Console.ReadLine(); 
} 

爲了保持總之,這裏是所發出的IL代碼片段被Y環X和Y by X循環。

初始代碼循環之前,

.method private hidebysig static void Main(string[] args) cil managed 
{ 
    .entrypoint 
    // Code size  290 (0x122) 
    .maxstack 4 
    .locals init ([0] class [System]System.Diagnostics.Stopwatch timer, 
      [1] int32 arraySize, 
      [2] int32[0...,0...] xbyy, 
      [3] int32 x, 
      [4] int32 y, 
      [5] valuetype [mscorlib]System.TimeSpan duration, 
      [6] string realTimeFormat, 
      [7] int32[0...,0...] ybyx, 
      [8] int32 V_8, 
      [9] int32 V_9) 
    IL_0000: newobj  instance void [System]System.Diagnostics.Stopwatch::.ctor() 
    IL_0005: stloc.0 
    IL_0006: ldc.i4  0x2710 
    IL_000b: stloc.1 

由X

IL_0090: ldloc.1 
    IL_0091: ldloc.1 
    IL_0092: newobj  instance void int32[0...,0...]::.ctor(int32, 
                  int32) 
    IL_0097: stloc.s ybyx 
    IL_0099: ldloc.0 
    IL_009a: callvirt instance void [System]System.Diagnostics.Stopwatch::Start() 
    IL_009f: ldc.i4.0 
    IL_00a0: stloc.s V_8 
    IL_00a2: br.s  IL_00c7 
    IL_00a4: ldc.i4.0 
    IL_00a5: stloc.s V_9 
    IL_00a7: br.s  IL_00bc 
    IL_00a9: ldloc.s ybyx 
    IL_00ab: ldloc.s V_9 
    IL_00ad: ldloc.s V_8 
    IL_00af: ldc.i4.s 15 
    IL_00b1: call  instance void int32[0...,0...]::Set(int32, 
                  int32, 
                  int32) 
    IL_00b6: ldloc.s V_9 
    IL_00b8: ldc.i4.1 
    IL_00b9: add 
    IL_00ba: stloc.s V_9 
    IL_00bc: ldloc.s V_9 
    IL_00be: ldloc.1 
    IL_00bf: blt.s  IL_00a9 
    IL_00c1: ldloc.s V_8 
    IL_00c3: ldc.i4.1 
    IL_00c4: add 
    IL_00c5: stloc.s V_8 
    IL_00c7: ldloc.s V_8 
    IL_00c9: ldloc.1 
    IL_00ca: blt.s  IL_00a4 
    IL_00cc: ldloc.0 

的IL邏輯流程由Y

IL_000c: ldloc.1 
    IL_000d: ldloc.1 
    IL_000e: newobj  instance void int32[0...,0...]::.ctor(int32, 
                  int32) 
    IL_0013: stloc.2 
    IL_0014: ldloc.0 
    IL_0015: callvirt instance void [System]System.Diagnostics.Stopwatch::Start() 
    IL_001a: ldc.i4.0 
    IL_001b: stloc.3 
    IL_001c: br.s  IL_003d 
    IL_001e: ldc.i4.0 
    IL_001f: stloc.s y 
    IL_0021: br.s  IL_0034 
    IL_0023: ldloc.2 
    IL_0024: ldloc.3 
    IL_0025: ldloc.s y 
    IL_0027: ldc.i4.s 15 
    IL_0029: call  instance void int32[0...,0...]::Set(int32, 
                  int32, 
                  int32) 
    IL_002e: ldloc.s y 
    IL_0030: ldc.i4.1 
    IL_0031: add 
    IL_0032: stloc.s y 
    IL_0034: ldloc.s y 
    IL_0036: ldloc.1 
    IL_0037: blt.s  IL_0023 
    IL_0039: ldloc.3 
    IL_003a: ldc.i4.1 
    IL_003b: add 
    IL_003c: stloc.3 
    IL_003d: ldloc.3 
    IL_003e: ldloc.1 
    IL_003f: blt.s  IL_001e 
    IL_0041: ldloc.0 

循環ý循環X是有些相似。我可以觀察到的主要區別是第一個循環設法使用位置2和3的stloc和ldloc作爲第一個數組和X索引變量。到了第二個循環時,它已經花費了maxstack,因此使用了stloc.s和ldloc.s指令。我相信這是堆棧中引用變量與堆中引用變量之間的差異,並導致性能下降。現在

,如果換成其中環路測試,以便在Y的X迴路獲取運行第一訪問堆棧引用的順序,你會看到持續時間得到逆轉的時機。


更新:我錯誤的引用堆棧或堆地址。這似乎只是的方法中的第一四個變量是更有效的與專用stloc.0,1,3,4和ldloc.0,1,3,4點的操作碼來引用。

http://weblogs.asp.net/mnolton/archive/2004/01/09/48992.aspx