Примеры предварительной выборки?
Может ли кто-нибудь привести пример или ссылку на пример, который использует __builtin_prefetch
в GCC (или только инструкцию prefetcht0 asm в целом), чтобы получить существенное преимущество в производительности? В частности, я хотел бы, чтобы этот пример отвечал следующим критериям:
- Это простой, маленький, самодостаточный пример.
- Удаление команды
__builtin_prefetch
приводит к ухудшению производительности. - Замена команды
__builtin_prefetch
с соответствующим доступом к памяти приводит к ухудшению производительности.
То есть, мне нужен самый короткий пример, показывающий, что __builtin_prefetch
выполняет оптимизацию, без которой невозможно управлять.
- Как вы прокручиваете загруженные в настоящее время сборки?
- Почему медленная инструкция цикла? Не удалось ли Intel эффективно внедрить его?
- Размер сборки .NET влияет на производительность?
- Visual Studio 2010 не создает перед запуском при изменении кода
- Как загрузить сборку .NET для операций отражения и впоследствии выгрузить ее?
- Как получить вывод ассемблера из источника C / C ++ в gcc?
- Определите, были ли сборки .NET построены из одного источника
- Как извлечь сборку из GAC?
- Ошибка в построении gradleа после обновления Android Studio с log4j
- Дополнительная информация о макете памяти исполняемой программы (процесса)
- .Net: Запуск кода при загрузке сборки
- В чем разница между MOV и LEA?
- Как загрузить сборку во время выполнения перед событием AssemblyResolve?
Вот фактический fragment кода, который я вытащил из более крупного проекта. (Извините, это самый короткий, который я могу найти, который имел заметное ускорение от предварительной выборки.) Этот код выполняет очень большую передачу данных.
В этом примере используются инструкции предварительной выборки SSE, которые могут быть такими же, как и GCC.
Для запуска этого примера вам нужно будет скомпилировать это для x64 и иметь более 4 ГБ памяти. Вы можете запустить его с меньшим размером данных, но со временем это будет слишком быстро.
#include using std::cout; using std::endl; #include #include #include #include #define ENABLE_PREFETCH #define f_vector __m128d #define i_ptr size_t inline void swap_block(f_vector *A,f_vector *B,i_ptr L){ // To be super-optimized later. f_vector *stop = A + L; do{ f_vector tmpA = *A; f_vector tmpB = *B; *A++ = tmpB; *B++ = tmpA; }while (A < stop); } void transpose_even(f_vector *T,i_ptr block,i_ptr x){ // Transposes T. // T contains x columns and x rows. // Each unit is of size (block * sizeof(f_vector)) bytes. //Conditions: // - 0 < block // - 1 < x i_ptr row_size = block * x; i_ptr iter_size = row_size + block; // End of entire matrix. f_vector *stop_T = T + row_size * x; f_vector *end = stop_T - row_size; // Iterate each row. f_vector *y_iter = T; do{ // Iterate each column. f_vector *ptr_x = y_iter + block; f_vector *ptr_y = y_iter + row_size; do{ #ifdef ENABLE_PREFETCH _mm_prefetch((char*)(ptr_y + row_size),_MM_HINT_T0); #endif swap_block(ptr_x,ptr_y,block); ptr_x += block; ptr_y += row_size; }while (ptr_y < stop_T); y_iter += iter_size; }while (y_iter < end); } int main(){ i_ptr dimension = 4096; i_ptr block = 16; i_ptr words = block * dimension * dimension; i_ptr bytes = words * sizeof(f_vector); cout << "bytes = " << bytes << endl; // system("pause"); f_vector *T = (f_vector*)_mm_malloc(bytes,16); if (T == NULL){ cout << "Memory Allocation Failure" << endl; system("pause"); exit(1); } memset(T,0,bytes); // Perform in-place data transpose cout << "Starting Data Transpose... "; clock_t start = clock(); transpose_even(T,block,dimension); clock_t end = clock(); cout << "Done" << endl; cout << "Time: " << (double)(end - start) / CLOCKS_PER_SEC << " seconds" << endl; _mm_free(T); system("pause"); }
Когда я запускаю его с включенным ENABLE_PREFETCH, это вывод:
bytes = 4294967296 Starting Data Transpose... Done Time: 0.725 seconds Press any key to continue . . .
Когда я запускаю его с отключенным ENABLE_PREFETCH, это вывод:
bytes = 4294967296 Starting Data Transpose... Done Time: 0.822 seconds Press any key to continue . . .
Таким образом, 13% ускоряется от предварительной выборки.
РЕДАКТИРОВАТЬ:
Вот еще несколько результатов:
Operating System: Windows 7 Professional/Ultimate Compiler: Visual Studio 2010 SP1 Compile Mode: x64 Release Intel Core i7 860 @ 2.8 GHz, 8 GB DDR3 @ 1333 MHz Prefetch : 0.868 No Prefetch: 0.960 Intel Core i7 920 @ 3.5 GHz, 12 GB DDR3 @ 1333 MHz Prefetch : 0.725 No Prefetch: 0.822 Intel Core i7 2600K @ 4.6 GHz, 16 GB DDR3 @ 1333 MHz Prefetch : 0.718 No Prefetch: 0.796 2 x Intel Xeon X5482 @ 3.2 GHz, 64 GB DDR2 @ 800 MHz Prefetch : 2.273 No Prefetch: 2.666
Двоичный поиск – простой пример, который может извлечь выгоду из явной предварительной выборки. Шаблон доступа в двоичном поиске выглядит довольно случайным для аппаратного префишера, поэтому мало шансов, что он точно предсказать, что выбрать.
В этом примере я предварительно выбираю два возможных «средних» местоположения следующей итерации цикла в текущей итерации. Один из предварительных примеров, вероятно, никогда не будет использоваться, но другой будет (если это не будет окончательная итерация).
#include #include #include int binarySearch(int *array, int number_of_elements, int key) { int low = 0, high = number_of_elements-1, mid; while(low <= high) { mid = (low + high)/2; #ifdef DO_PREFETCH // low path __builtin_prefetch (&array[(mid + 1 + high)/2], 0, 1); // high path __builtin_prefetch (&array[(low + mid - 1)/2], 0, 1); #endif if(array[mid] < key) low = mid + 1; else if(array[mid] == key) return mid; else if(array[mid] > key) high = mid-1; } return -1; } int main() { int SIZE = 1024*1024*512; int *array = malloc(SIZE*sizeof(int)); for (int i=0;i
Когда я компилирую и запускаю этот пример с включенным DO_PREFETCH, я вижу сокращение времени выполнения на 20%:
$ gcc c-binarysearch.c -DDO_PREFETCH -o with-prefetch -std=c11 -O3 $ gcc c-binarysearch.c -o no-prefetch -std=c11 -O3 $ perf stat -e L1-dcache-load-misses,L1-dcache-loads ./with-prefetch Performance counter stats for './with-prefetch': 356,675,702 L1-dcache-load-misses # 41.39% of all L1-dcache hits 861,807,382 L1-dcache-loads 8.787467487 seconds time elapsed $ perf stat -e L1-dcache-load-misses,L1-dcache-loads ./no-prefetch Performance counter stats for './no-prefetch': 382,423,177 L1-dcache-load-misses # 97.36% of all L1-dcache hits 392,799,791 L1-dcache-loads 11.376439030 seconds time elapsed
Обратите внимание, что мы делаем в два раза больше загрузок кеша L1 в версии предварительной выборки. На самом деле мы делаем гораздо больше работы, но шаблон доступа к памяти более дружелюбен к конвейеру. Это также показывает компромисс. Хотя этот блок кода работает быстрее изолированно, мы загрузили много мусора в кеш, и это может оказать большее давление на другие части приложения.
Из документации :
for (i = 0; i < n; i++) { a[i] = a[i] + b[i]; __builtin_prefetch (&a[i+j], 1, 1); __builtin_prefetch (&b[i+j], 0, 1); /* ... */ }
Я многому научился из превосходных ответов, предоставленных @JamesScriven и @Mystical. Тем не менее, их примеры дают лишь скромный импульс – цель этого ответа – представить пример (я должен признаться, несколько искусственный), когда предварительная выборка оказывает большее влияние (около фактора 4 на моей машине).
Для современных архитектур существует три возможных флажка: скорость процессора, ширина полосы памяти и латентность памяти. Предварительная выборка предназначена для уменьшения латентности доступа к памяти.
В идеальном сценарии, где латентность соответствует шагам вычисления X, у нас будет oracle, который скажет нам, какую память мы будем получать в шагах вычисления X, будет запущена предварительная выборка этих данных, время X вычислений – шаги позже.
Для многих алгоритмов мы (почти) в этом совершенном мире. Для простого для цикла легко предсказать, какие данные понадобятся X шагов позже. Внештатное исполнение и другие аппаратные трюки здесь работают очень хорошо, скрывая латентность почти полностью.
В этом причина, почему существует такое скромное улучшение для примера @ Mystical: Prefetcher уже очень хорош – просто нет места для улучшения. Задача также связана с памятью, поэтому, вероятно, не так много ширины полосы – это может стать ограничивающим фактором. Я мог видеть в лучшем случае улучшение на моей машине на 8%.
Критическое понимание из примера @JamesScriven: ни мы, ни CPU не знаем следующий адрес доступа до того, как текущие данные извлекаются из памяти – эта зависимость очень важна, в противном случае выполнение вне очереди приведет к перемотке вперед и аппаратное обеспечение будет иметь возможность предварительной выборки данных. Однако, поскольку мы можем размышлять только об одном шаге, этого не так много. Я не смог получить более 40% на моей машине.
Итак, давайте подберем конкуренцию и подготовьте данные таким образом, чтобы мы знали, к какому адресу обращаются по шагам X, но не позволяют аппаратным средствам найти его из-за зависимостей от еще не полученных данных (см. Всю программу в конце ответа):
//making random accesses to memory: unsigned int next(unsigned int current){ return (current*10001+328)%SIZE; } //the actual work is happening here void operator()(){ //set up the oracle - let see it in the future oracle_offset steps unsigned int prefetch_index=0; for(int i=0;i
Некоторые замечания:
- данные подготовлены таким образом, что oracle всегда прав.
- возможно, удивительно, чем меньше задача, связанная с процессором, тем больше ускорение: мы можем скрыть задержку почти полностью, таким образом, ускорение - это время
CPU-time+original-latency-time/CPU-time
.
Компиляция и выполнение:
>>> g++ -std=c++11 prefetch_demo.cpp -O3 -o prefetch_demo >>> ./prefetch_demo #preloops time no prefetch time prefetch factor ... 7 1.0711102260000001 0.230566831 4.6455521002498408 8 1.0511602149999999 0.22651144600000001 4.6406494398521474 9 1.049024333 0.22841439299999999 4.5926367389641687 ....
к ускорению между 4 и 5.
Список prefetch_demp.cpp
:
//prefetch_demo.cpp #include #include #include #include const int SIZE=1024*1024*1; const int STEP_CNT=1024*1024*10; unsigned int next(unsigned int current){ return (current*10001+328)%SIZE; } template struct Worker{ std::vector mem; double result; int oracle_offset; void operator()(){ unsigned int prefetch_index=0; for(int i=0;i &mem_): mem(mem_), result(0.0), oracle_offset(0) {} }; template double timeit(Worker &worker){ auto begin = std::chrono::high_resolution_clock::now(); worker(); auto end = std::chrono::high_resolution_clock::now(); return std::chrono::duration_cast(end-begin).count()/1e9; } int main() { //set up the data in special way! std::vector keys(SIZE); for (int i=0;i without_prefetch(keys); Worker with_prefetch(keys); std::cout<<"#preloops\ttime no prefetch\ttime prefetch\tfactor\n"; std::cout<