簡體   English   中英

在c / c ++中實現2d數組的數據局部性

[英]data locality for implementing 2d array in c/c++

很久以前,受“ C中的數字配方”的啟發,我開始使用以下結構來存儲矩陣(2D數組)。

double **allocate_matrix(int NumRows, int NumCol)
{
  double **x;
  int i;

  x = (double **)malloc(NumRows * sizeof(double *));
  for (i = 0; i < NumRows; ++i) x[i] = (double *)calloc(NumCol, sizeof(double));
  return x;
}

double **x = allocate_matrix(1000,2000);
x[m][n] = ...;

但是最近發現許多人實現矩陣如下

double *x = (double *)malloc(NumRows * NumCols * sizeof(double));
x[NumCol * m + n] = ...;

從局部角度來看,第二種方法似乎是完美的,但可讀性很差……所以我開始懷疑,存儲輔助數組或**double指針的第一種方法是否真的不好,否則編譯器最終會對其進行優化,以至於在性能上會與第二種方法差不多嗎? 我很懷疑,因為我認為在第一種方法中,訪問值x[m]然后x[m][n]時會發生兩次跳轉,並且每次CPU都將首先加載x數組和然后是x[m]數組。

ps不必擔心用於存儲**double額外內存,對於大型矩陣,這只是一個很小的百分比。

PPS,因為很多人不理解我的問題非常好,我會嘗試重新塑造它:做我的理解正確的,第一種方法是一種局部性地獄,當每一次x[m][n]被訪問的第一x數組將被加載到CPU緩存中,然后x[m]數組將被加載,從而使每次訪問都以與RAM對話的速度進行。 還是我錯了,從數據局部性的角度來看,第一種方法也可以嗎?

對於C風格的分配,您實際上可以兼得兩者:

double **allocate_matrix(int NumRows, int NumCol)
{
  double **x;
  int i;

  x = (double **)malloc(NumRows * sizeof(double *));
  x[0] = (double *)calloc(NumRows * NumCol, sizeof(double)); // <<< single contiguous memory allocation for entire array
  for (i = 1; i < NumRows; ++i) x[i] = x[i - 1] + NumCols;
  return x;
}

這樣,您可以獲得數據局部性及其相關的緩存/內存訪問優勢,並且可以將數組視為double **或扁平2D數組( array[i * NumCols + j] )互換使用。 你也有少calloc / free電話( 2NumRows + 1 )。

無需猜測編譯器是否會優化第一種方法。 只需使用您知道很快的第二種方法,並使用實現以下方法的包裝器類即可:

double& operator(int x, int y);
double const& operator(int x, int y) const;

...並像這樣訪問您的對象:

arr(2, 3) = 5;

另外,如果您可以在包裝類中承擔更多的代碼復雜性,則可以實現一個類,該類可以用更傳統的arr[2][3] = 5; 句法。 這在Boost.MultiArray庫中以與維度無關的方式實現,但是您也可以使用代理類來執行自己的簡單實現。

注意:考慮到C風格的使用(硬編碼的非通用“ double”類型,普通指針,函數開頭的變量聲明和malloc ),在實現任何一個選項之前,您可能需要更多地使用C ++構造。我提到。

兩種方法完全不同。

  • 盡管第一種方法通過添加另一個間接尋址( double**數組,因此您需要1 + N個malloc)可以更輕松地直接訪問值,但是...
  • 第二種方法保證ALL值是連續存儲的,只需要一個malloc。

我認為第二種方法總是更好的。 根據應用程序的不同,Malloc是一項昂貴的操作,而連續內存是一項巨大的優勢。

在C ++中,您可以像這樣實現它:

std::vector<double> matrix(NumRows * NumCols);
matrix[y * numCols + x] = value;  // Access

並且如果您擔心必須自己計算索引的不便之處,請向其添加一個實現operator(int x, int y)的包裝器。

您也很正確,第一種方法在訪問值時更昂貴。 因為您需要按照x[m]x[m][n]順序進行兩次內存查找。 編譯器無法“優化”。 根據其大小,將對第一個陣列進行緩存,並且性能影響可能不會那么糟。 在第二種情況下,您需要額外的乘法才能直接訪問。

在您使用的第一種方法中,主數組中的double*指向邏輯列(大小為NumCol數組)。

因此,如果您編寫類似下面的內容,則可以從某種意義上(偽代碼)獲得數據局部性的好處:

foreach(row in rows):
    foreach(elem in row):
        //Do something

如果您使用第二種方法嘗試了相同的操作,並且以指定的方式(即x[NumCol*m + n] )完成了元素訪問,則您仍會獲得相同的收益。 這是因為您將數組按行優先順序進行處理。 如果您在按列優先順序訪問元素時嘗試了相同的偽代碼,則假定數組大小足夠大,我認為您會遇到緩存未命中的情況。

除此之外,第二種方法還具有另一個令人希望的特性,即它是單個連續的內存塊,即使您遍歷多行,也可以進一步提高性能(與第一種方法不同)。

因此,總而言之,第二種方法在性能方面應該更好。

如果NumCol是編譯時常量,或者您使用的是啟用了語言擴展的GCC,則可以執行以下操作:

double (*x)[NumCol] = (double (*)[NumCol]) malloc(NumRows * sizeof (double[NumCol]));

然后將x用作2D數組,編譯器將為您執行索引運算。 需要注意的是,除非NumCol是編譯時常量,否則ISO C ++不會允許您這樣做,並且如果您使用GCC語言擴展,則無法將代碼移植到另一個編譯器。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM