[英]Optimizing C code without parallel programming
我寫的C代碼是
for(i=1;i<10000;i++)
x[i]=array1[h][x[i]^x[i-1]]
和
for(i=9999;i>0;i--)
x[i]=x[i-1]^array2[h][x[i]]
筆記:
1- array1和array2包含字節值
2-第二個循環執行與第一個循環相反的功能
3- h是字節值,在loop1和loop2中相同
我的問題是
第二個循環比第一個循環快,我理解這一點,因為在第一個循環中,x中的每個值都取決於前一個字節IE的新值。 要計算x2,您必須計算x1,而在第二個循環中,每個字節取決於已存在的前一個字節的舊值IE。 要計算x9999,您需要使用x9998的舊值而不是新值,因此無需等待x9999的計算,這在C代碼中的完成方式以及所謂的是並行編程,這意味着C語言對某些循環進行了並行編程。在沒有用戶控制和編寫這種並行的情況下不是連續的
問題是:為什么2.循環比1.循環快?
非常感謝
我是C代碼的初學者
抱歉,這個問題太簡單了
您的第一個循環取決於先前迭代的結果。 簡而言之,這意味着處理器要等到i=1
才能開始考慮i=2
,因為x[2]
取決於x[1]
。 但是,第二個循環不依賴於先前迭代的結果。
通過添加-O3
標志(這是一個大寫的“ o”,而不是零)來啟用編譯器優化可能會加快兩個循環的速度並使它們接近相同的速度。 有一些“手動”優化,例如循環矢量化或使用仍然可以實現的更廣泛的數據類型,但是請首先嘗試-O3
標志。 如果您不知道如何執行,請查看IDE的幫助文件中的“編譯器標志”。
就是說,看起來您正在實施某種加密。 實際上,此代碼看起來像是RC4等密碼的精簡版本。 如果您正在這樣做,那么我會為您提供一些警告:
1)如果您要為生產代碼編寫加密,而這取決於您的安全性,則建議您使用知名且經過測試的庫中的內容,而不是編寫自己的庫,這樣會更快,更安全。
2)如果您要為生產代碼編寫自己的加密算法(而不只是“為了好玩”),請不要這樣做。 安全的算法比任何人都可以設計的東西要多,您無法通過滾動自己獲得任何東西。
3)如果您正在編寫或實現有趣的算法,那就太好了! 完成一些現實世界的實現后,您可能會發現一些好主意。
大多數現代處理器只能根據源數據的就緒性來破壞指令的順序,並亂序執行它們。 想想一個池,您將第一個〜50個迭代倒入一個穩定狀態(可能比它們執行的速度快)-假設您有多個ALU,可以開始並行執行多少個? 在某些情況下,您甚至可以並行化所有代碼,使您受執行資源數量(可能很高)的束縛。 編輯:重要的是要注意,這在復雜的控制流程中會變得更加困難(例如,如果您的循環中有一堆if條件,尤其是如果它們取決於數據),因為您需要預測它們並刷新較新的指令錯了
一個好的編譯器還可以在循環展開和向量化的基礎上添加,這進一步增強了這種並行性,並可以從CPU獲得執行BW。
Dan對依賴完全正確(盡管這不是簡單的“管道”)。 在第一個循環中,每次迭代的x [i-1]將被識別為與前一個迭代的x [i]混疊(通過CPU別名檢測),從而使其成為先寫后讀的方案並強制執行等待並轉發結果(跨越多個迭代,這形成了一長串的依賴關系-雖然您可以看到迭代N,但是直到完成N-1(等待N-2)之后,您才能執行它上..)。 順便說一句,如果復雜到轉發的情況(例如緩存行拆分或頁面拆分訪問),這可能會變得更糟。
第二個循環也使用其他單元格中的值,但是有一個重要的區別-程序順序首先讀取x [i-1]的值(用於計算x [i]),然后才寫入x [i-1] 。 這將寫入后讀取方式更改為讀取后寫入方式,這要簡單得多,因為沿着管道進行的加載比存儲要早得多。 現在,允許處理器預先讀取所有值(將它們保留在內部寄存器中的某個位置),並並行運行計算。 由於沒有人依賴它們,因此可以隨意緩沖和完成寫入操作。
編輯:在某些情況下,另一個需要考慮的問題是內存訪問模式,但是在這種情況下,它看起來像是數組x(跨度為1的跨步)上的簡單流模式,無論是正方向還是負方向,但都可以輕松識別並且預取器應該開始觸發,因此這些訪問中的大多數都應該訪問緩存。 另一方面,array1 / 2訪問很復雜,因為它們是由加載結果決定的-這也會使程序停頓一些,但是在兩種情況下都是一樣的。
for(i=1;i<10000;i++)
x[i]=array1[h][x[i]^x[i-1]]
for循環的每次迭代都需要從array1獲取一個值。 每當訪問值時,都會讀取該值附近的數據(通常是緩存行大小)並將其存儲在緩存中。 L1和L2緩存的緩存行大小不同,我認為它們分別為64字節和128字節。 下次訪問相同的數據或上一個值附近的數據時,很有可能發生高速緩存命中,從而將操作速度提高了一個數量級。
現在,在上面的for循環中,x [i] ^ x [i-1]可以求值數組索引,其值不位於連續迭代的高速緩存行的大小之內。 讓我們以L1緩存為例。 對於for循環的第一次迭代,將訪問值array [h] [x [i] ^ x [i-1]],該值位於主存儲器中。 圍繞此字節值的64個字節的數據被引入並存儲在L1高速緩存中的高速緩存行中。 對於下一次迭代,x [i] ^ x [i-1]可能導致一個索引,其值存儲在不在第一次迭代中帶來的64字節附近的位置。 因此,再次訪問高速緩存未命中和主存儲器。 在執行for循環期間,這可能會發生很多次,從而導致性能下降。
嘗試查看x [i] ^ x [i-1]對每次迭代求和的結果。 如果它們相差很大,則緩慢的部分原因是上述原因。
下面的鏈接很好地解釋了這個概念。
在這兩種情況下,您都應該說unsigned char * aa = &array1[h];
(或第二個循環為array2[h]
)。 希望編譯器在確保可以的情況下提升索引操作沒有任何意義。
這兩個循環在做不同的事情:
循環1在索引到aa
之前執行x[i] ^ x[i-1]
,而循環2在之前將x[i]
索引aa
,然后在之后執行^ x[i-1]
。
無論如何,我將對x[i]
和x[i-1]
使用指針,並且我將展開循環,因此循環1看起來像這樣:
unsigned char * aa = &array1[h];
unsigned char * px = &x[1];
unsigned char * px1 = &x[0];
for (i = 1; i < 10; i++){
*px = aa[ *px ^ *px1 ]; px++; px1++;
}
for ( ; i < 10000; i += 10 ){
*px = aa[ *px ^ *px1 ]; px++; px1++;
*px = aa[ *px ^ *px1 ]; px++; px1++;
*px = aa[ *px ^ *px1 ]; px++; px1++;
*px = aa[ *px ^ *px1 ]; px++; px1++;
*px = aa[ *px ^ *px1 ]; px++; px1++;
*px = aa[ *px ^ *px1 ]; px++; px1++;
*px = aa[ *px ^ *px1 ]; px++; px1++;
*px = aa[ *px ^ *px1 ]; px++; px1++;
*px = aa[ *px ^ *px1 ]; px++; px1++;
*px = aa[ *px ^ *px1 ]; px++; px1++;
}
一種替代方法是使用單個p
指針,並使用硬偏移,如下所示:
unsigned char * aa = &array1[h];
unsigned char * px = &x[0];
for (i = 1; i < 10; i++){
px[1] = aa[ px[1] ^ px[0] ]; px++;
}
for ( ; i < 10000; i += 10, px += 10 ){
px[ 1] = aa[ px[ 1] ^ px[0] ];
px[ 2] = aa[ px[ 2] ^ px[1] ];
px[ 3] = aa[ px[ 3] ^ px[2] ];
px[ 4] = aa[ px[ 4] ^ px[3] ];
px[ 5] = aa[ px[ 5] ^ px[4] ];
px[ 6] = aa[ px[ 6] ^ px[5] ];
px[ 7] = aa[ px[ 7] ^ px[6] ];
px[ 8] = aa[ px[ 8] ^ px[7] ];
px[ 9] = aa[ px[ 9] ^ px[8] ];
px[10] = aa[ px[10] ^ px[9] ];
}
我不確定哪個會更快。
再次,有些人會說編譯器的優化器會為您完成此操作,但是對其進行幫助沒有害處。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.