探究u64 循環計數器與x86 CPUs 上的_mm_popcnt_u64 不同尋常的效能差異
簡介
我在尋找快速對大型資料數組進行popcount 的方法時,遇到了一個非常奇怪的現象:將循環變數從 unsigned 更改為 uint64_t 使我的 PC 上的效能下降了 50%。基準測試
#include <iostream> #include <chrono> #include <x86intrin.h> int main(int argc, char* argv[]) { using namespace std; if (argc != 2) { cerr << "usage: array_size in MB" << endl; return -1; } uint64_t size = atol(argv[1])<<20; uint64_t* buffer = new uint64_t[size/8]; char* charbuffer = reinterpret_cast<char*>(buffer); for (unsigned i=0; i<size; ++i) charbuffer[i] = rand()%256; uint64_t count,duration; chrono::time_point<chrono::system_clock> startP,endP; { startP = chrono::system_clock::now(); count = 0; for( unsigned k = 0; k < 10000; k++){ // Tight unrolled loop with unsigned for (unsigned i=0; i<size/8; i+=4) { count += _mm_popcnt_u64(buffer[i]); count += _mm_popcnt_u64(buffer[i+1]); count += _mm_popcnt_u64(buffer[i+2]); count += _mm_popcnt_u64(buffer[i+3]); } } endP = chrono::system_clock::now(); duration = chrono::duration_cast<std::chrono::nanoseconds>(endP-startP).count(); cout << "unsigned\t" << count << '\t' << (duration/1.0E9) << " sec \t" << (10000.0*size)/(duration) << " GB/s" << endl; } { startP = chrono::system_clock::now(); count=0; for( unsigned k = 0; k < 10000; k++){ // Tight unrolled loop with uint64_t for (uint64_t i=0;i<size/8;i+=4) { count += _mm_popcnt_u64(buffer[i]); count += _mm_popcnt_u64(buffer[i+1]); count += _mm_popcnt_u64(buffer[i+2]); count += _mm_popcnt_u64(buffer[i+3]); } } endP = chrono::system_clock::now(); duration = chrono::duration_cast<std::chrono::nanoseconds>(endP-startP).count(); cout << "uint64_t\t" << count << '\t' << (duration/1.0E9) << " sec \t" << (10000.0*size)/(duration) << " GB/s" << endl; } free(charbuffer); }如您所見,我們建立了一個大小為 x MB 的隨機資料緩衝區,其中 x 從命令列讀取。然後,我們迭代緩衝區並使用 x86 popcount 內聯函數的一個展開版本執行 popcount。為了獲得更精確的結果,我們執行 10,000 次 popcount。我們測量 popcount 的時間。在第一種情況下,內部循環變數未簽名,在第二種情況下,內部循環變數為 uint64_t。我認為這不應該有任何區別,但事實並非如此。
(絕對瘋狂的)結果
我這樣編譯它(g 版本:Ubuntu 4.8.2-19ubuntu1):g++ -O3 -march=native -std=c++11 test.cpp -o test這是我在我的Haswell Core i7-4770K CPU @ 3.50GHz 上執行測試1(所以 1MB 隨機資料)的結果:
clang++ -O3 -march=native -std=c++11 teest.cpp -o test測試結果1:
uint64_t size = atol(argv[1]) << 20;改為:
uint64_t size = 1 << 20;因此,編譯器現在知道編譯時的緩衝區大小。也許它可以添加一些優化!以下是 g 中的數字:
uint64_t 4195936000863610086361000963610086320100863201008963150096320100863201008632010089631500963201008632010096315032 GB/秒
現在,兩個版本的速度都一樣快。然而,與 unsigned 相比, velocidade 甚至變得更慢了!它從 26 GB/秒下降到 20 GB/秒,因此用常數值取代一個非常規常數導致uint64_t size = atol(argv[1]) << 20;去最佳化
。嚴重的是,我在這裡毫無頭緒!但現在用 clang 和新版本:
uint64_t size = 1 << 20;
改為:
結果:等等,發生了什麼事?現在,兩個版本都下降到了 15GB/s 的 低速度。因此,用一個常數值取代一個非常規常數值甚至導致了 兩 個版本的程式碼速度變慢對於 Clang!
我請一位使用 Ivy Bridge CPU 的同事編譯我的基準測試。他得到了類似的結果,所以這似乎不是 Haswell 獨有。由於有兩個編譯器在此處產生奇怪的結果,因此這似乎也不是編譯器錯誤。由於我們這裡沒有 AMD CPU,只能使用 Intel 來測試。
更多瘋狂,拜託!
使用第一個範例(帶有atol(argv[1]) 的範例),在變數前面放置一個static,即:
#include <iostream> #include <chrono> #include <x86intrin.h> int main(int argc, char* argv[]) { using namespace std; if (argc != 2) { cerr << "usage: array_size in MB" << endl; return -1; } uint64_t size = atol(argv[1])<<20; uint64_t* buffer = new uint64_t[size/8]; char* charbuffer = reinterpret_cast<char*>(buffer); for (unsigned i=0; i<size; ++i) charbuffer[i] = rand()%256; uint64_t count,duration; chrono::time_point<chrono::system_clock> startP,endP; { startP = chrono::system_clock::now(); count = 0; for( unsigned k = 0; k < 10000; k++){ // Tight unrolled loop with unsigned for (unsigned i=0; i<size/8; i+=4) { count += _mm_popcnt_u64(buffer[i]); count += _mm_popcnt_u64(buffer[i+1]); count += _mm_popcnt_u64(buffer[i+2]); count += _mm_popcnt_u64(buffer[i+3]); } } endP = chrono::system_clock::now(); duration = chrono::duration_cast<std::chrono::nanoseconds>(endP-startP).count(); cout << "unsigned\t" << count << '\t' << (duration/1.0E9) << " sec \t" << (10000.0*size)/(duration) << " GB/s" << endl; } { startP = chrono::system_clock::now(); count=0; for( unsigned k = 0; k < 10000; k++){ // Tight unrolled loop with uint64_t for (uint64_t i=0;i<size/8;i+=4) { count += _mm_popcnt_u64(buffer[i]); count += _mm_popcnt_u64(buffer[i+1]); count += _mm_popcnt_u64(buffer[i+2]); count += _mm_popcnt_u64(buffer[i+3]); } } endP = chrono::system_clock::now(); duration = chrono::duration_cast<std::chrono::nanoseconds>(endP-startP).count(); cout << "uint64_t\t" << count << '\t' << (duration/1.0E9) << " sec \t" << (10000.0*size)/(duration) << " GB/s" << endl; } free(charbuffer); }
以下是她在g 中的結果:
耶,還有另一個替代方案!我們仍然擁有 32GB/s 與 u3,但我們設法將 u64 至少從 13GB/s 版本提升到 20GB/s 版本!在我的同事的電腦上,u64 版本甚至比 u32 版本更快,獲得了最好的結果。遺憾的是,這只適用於 g ,clang 似乎不在乎 static。
**我的問題
以上是為什麼將循環計數器從「unsigned」更改為「uint64_t」會顯著影響 x86 CPU 上「_mm_popcnt_u64」的效能,以及編譯器最佳化和變數宣告如何影響的詳細內容。更多資訊請關注PHP中文網其他相關文章!