Я искал самый быстрый способ для больших массивов данных popcount
. Я столкнулся с очень странным эффектом: изменение переменной цикла от unsigned
до uint64_t
привело к снижению производительности на 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
мегабайт, где x
считывается из командной строки. Затем мы перебираем буфер и используем развернутую версию x86 popcount
, чтобы выполнить popcount. Чтобы получить более точный результат, мы делаем 10000 раз. Мы измеряем время для popcount. В верхнем регистре внутренняя переменная цикла unsigned
, в нижнем регистре внутренняя переменная цикла 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.50 GHz, запуск test 1
(так что 1 случайные данные MB):
- unsigned 41959360000 0.401554 sec 26.113 GB/s
- uint64_t 41959360000 0,759822 сек 13.8003 GB/s
Как вы видите, пропускная способность версии uint64_t
только наполовину - одна из версий unsigned
! Проблема заключается в том, что генерируется другая сборка, но почему? Во-первых, я подумал о ошибке компилятора, поэтому я попробовал clang++
(Ubuntu Clang версия 3.4-1ubuntu3):
clang++ -O3 -march=native -std=c++11 teest.cpp -o test
Результат: test 1
- unsigned 41959360000 0.398293 sec 26.3267 Гб/с
- uint64_t 41959360000 0.680954 sec 15.3986 Гб/с
Итак, это почти тот же результат и по-прежнему странный. Но теперь это становится супер странным. Я заменяю размер буфера, который был прочитан с ввода с константой 1
, поэтому я меняю:
uint64_t size = atol(argv[1]) << 20;
к
uint64_t size = 1 << 20;
Таким образом, компилятор теперь знает размер буфера во время компиляции. Возможно, он может добавить некоторые оптимизации! Вот цифры для g++
:
- unsigned 41959360000 0.509156 sec 20.5944 GB/s
- uint64_t 41959360000 0.508673 sec 20.6139 GB/s
Теперь обе версии одинаково быстры. Однако unsigned
получил еще медленнее! Он упал с 26
до 20 GB/s
, таким образом, заменив непостоянное на постоянное значение, приведет к деоптимизации. Серьезно, я понятия не имею, что здесь происходит! Но теперь до clang++
с новой версией:
- unsigned 41959360000 0.677009 sec 15.4884 GB/s
- uint64_t 41959360000 0,676909 сек 15.4906 GB/s
Подождите, что? Теперь обе версии упали до медленного числа в 15 бит/с. Таким образом, замена непостоянного на постоянное значение даже приводит к медленному коду в случаях для Clang!