В этом примере clang векторизует индексирование, но (по ошибке) не может векторизовать итерации.
Чтобы суммировать результаты, нет никакой разницы между использованием необработанного цикла и использованием std::transform
или std::for_each
. Однако есть различие между использованием индексации и использованием итерации, и для В целях этой конкретной проблемы , clang лучше оптимизирует индексирование, чем оптимизирует итерацию, потому что индексирование становится векторизованным. std::transform
и std::for_each
используют итерации, поэтому они становятся медленнее (при компиляции под Clang).
В чем разница между индексированием и повторением?
Индексирование - это когда вы используете целое число для индексации в массиве.
- Итерация - это когда вы увеличиваете указатель с begin()
до end()
.
Давайте напишем цикл с использованием индексации и с помощью итерации, и сравним производительность итерации (с помощью необработанного цикла) с индексацией.
// Indexing
for(int i = 0; i < a.size(); i++) {
a[i] = !a[i];
}
// Iterating, used by std::for_each and std::transform
bool* begin = a.data();
bool* end = begin + a.size();
for(; begin != end; ++begin) {
*begin = !*begin;
}
Пример использования индексации лучше оптимизирован и работает в 4-5 раз быстрее при компиляции с помощью clang.
Чтобы продемонстрировать это, давайте добавим два дополнительных теста, оба с использованием необработанного цикла. Один будет использовать итератор, а другой будет использовать необработанные указатели.
static void RawLoopIt(benchmark::State& state) {
std::array<bool, 16> a;
std::fill(a.begin(), a.end(), true);
for(auto _ : state) {
auto scan = a.begin();
auto end = a.end();
for (; scan != end; ++scan) {
*scan = !*scan;
}
benchmark::DoNotOptimize(a);
}
}
BENCHMARK(RawLoopIt);
static void RawLoopPtr(benchmark::State& state) {
std::array<bool, 16> a;
std::fill(a.begin(), a.end(), true);
for(auto _ : state) {
bool* scan = a.data();
bool* end = scan + a.size();
for (; scan != end; ++scan) {
*scan = !*scan;
}
benchmark::DoNotOptimize(a);
}
}
BENCHMARK(RawLoopPtr);
При использовании указателя или итератора от begin
до end
эти функции по производительности идентичны использованию std::for_each
или std::transform
.
Результаты Clang Quick-bench:
Это подтверждается выполнением локального теста clang:
me@K2SO:~/projects/scratch$ clang++ -O3 bench.cpp -lbenchmark -pthread -o clang-bench
me@K2SO:~/projects/scratch$ ./clang-bench
2019-07-05 16:13:27
Running ./clang-bench
Run on (8 X 4000 MHz CPU s)
CPU Caches:
L1 Data 32K (x4)
L1 Instruction 32K (x4)
L2 Unified 256K (x4)
L3 Unified 8192K (x1)
Load Average: 0.44, 0.55, 0.59
-----------------------------------------------------
Benchmark Time CPU Iterations
-----------------------------------------------------
ForEach 8.32 ns 8.32 ns 83327615
Transform 8.29 ns 8.28 ns 82536410
RawLoop 1.92 ns 1.92 ns 361745495
RawLoopIt 8.31 ns 8.31 ns 81848945
RawLoopPtr 8.28 ns 8.28 ns 82504276
GCC не имеет этой проблемы.
Для целей этого примера нет принципиальной разницы между индексированием или итерацией. Оба они применяют идентичное преобразование к массиву, и компилятор должен быть в состоянии скомпилировать их одинаково.
Действительно, GCC способен сделать это, причем все методы работают на быстрее , чем соответствующая версия, скомпилированная в clang.
GCC Результаты быстрого стенда:
GCC Местные результаты:
2019-07-05 16:13:35
Running ./gcc-bench
Run on (8 X 4000 MHz CPU s)
CPU Caches:
L1 Data 32K (x4)
L1 Instruction 32K (x4)
L2 Unified 256K (x4)
L3 Unified 8192K (x1)
Load Average: 0.52, 0.57, 0.60
-----------------------------------------------------
Benchmark Time CPU Iterations
-----------------------------------------------------
ForEach 1.43 ns 1.43 ns 484760981
Transform 1.44 ns 1.44 ns 485788409
RawLoop 1.43 ns 1.43 ns 484973417
RawLoopIt 1.44 ns 1.44 ns 482685685
RawLoopPtr 1.44 ns 1.44 ns 483736235
Индексирование на самом деле быстрее, чем итерация? Нет. Индексирование выполняется быстрее, потому что clang векторизует его.
Под капотом не выполняется ни итерация , ни индексация. Вместо этого gcc и clang векторизуют операцию, обрабатывая массив как два 64-битных целых числа и используя для них битовую-xor. Это можно увидеть в сборке, используемой для переворачивания битов:
movabs $0x101010101010101,%rax
nopw %cs:0x0(%rax,%rax,1)
xor %rax,(%rsp)
xor %rax,0x8(%rsp)
sub $0x1,%rbx
Итерация медленнее при компиляции clang , потому что по какой-то причине clang не может векторизовать операцию при использовании итерации. Это дефект в clang, и тот, который применяется специально к этой проблеме. По мере того как лязг улучшается, это несоответствие должно исчезнуть, и сейчас я не о чем беспокоюсь.
Не оптимизировать микро. Позвольте компилятору справиться с этим и, если необходимо, проверьте, генерирует ли gcc или clang более быстрый код для вашего конкретного варианта использования . Ни один не лучше для всех случаев. Например, clang лучше справляется с векторизацией некоторых математических операций.