[英]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
電話( 2
對NumRows + 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)可以更輕松地直接訪問值,但是... 我認為第二種方法總是更好的。 根據應用程序的不同,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.