Please consider the following minimal example minimal.cpp
(https://godbolt.org/z/x7dYes91M).
#include <immintrin.h>
#include <algorithm>
#include <ctime>
#include <iostream>
#include <numeric>
#include <vector>
#define NUMBER_OF_TUPLES 134'217'728UL
void transform(std::vector<int64_t>* input, std::vector<double>* output, size_t batch_size) {
for (size_t startOfBatch = 0; startOfBatch < NUMBER_OF_TUPLES; startOfBatch += batch_size) {
size_t endOfBatch = std::min(startOfBatch + batch_size, NUMBER_OF_TUPLES);
for (size_t idx = startOfBatch; idx < endOfBatch;) {
if (endOfBatch - idx >= 8) {
auto _loaded = _mm512_loadu_epi64(&(*input)[idx]);
auto _converted = _mm512_cvtepu64_pd(_loaded);
_mm512_storeu_epi64(&(*output)[idx], _converted);
idx += 8;
} else {
(*output)[idx] = static_cast<double>((*input)[idx]);
idx++;
}
}
asm volatile("" : : "r,m"(output->data()) : "memory");
}
}
void do_benchmark(size_t batch_size) {
std::vector<int64_t> input(NUMBER_OF_TUPLES);
std::vector<double> output(NUMBER_OF_TUPLES);
std::iota(input.begin(), input.end(), 0);
auto t = std::clock();
transform(&input, &output, batch_size);
auto elapsed = std::clock() - t;
std::cout << "Elapsed time for a batch size of " << batch_size << ": " << elapsed << std::endl;
}
int main() {
do_benchmark(7UL);
do_benchmark(8UL);
do_benchmark(9UL);
}
It transforms the input
array of int64_t
to the output array of double
in batches of a given batch_size
.
We have inserted the following AVX-512 intrinsics in case there are still more or equal than 8 tuples in the input, to process them all at once and therefore increase the performance
auto _loaded = _mm512_loadu_epi64(&(*input)[idx]);
auto _converted = _mm512_cvtepu64_pd(_loaded);
_mm512_storeu_epi64(&(*output)[idx], _converted);
Otherwise, we fall back to the scalar implementation.
To make sure that the compiler doesn't collapse the two loops, we use the asm volatile("" : : "r,m"(output->data()) : "memory")
call, to make sure that the output data is flushed after each batch.
It is compiled and executed on an Intel(R) Xeon(R) Gold 5220R CPU
using
clang++ -Wall -Wextra -march=cascadelake -mavx512f -mavx512cd -mavx512vl -mavx512dq -mavx512bw -mavx512vnni -O3 minimal.cpp -o minimal
Executing the code, however, results in the following surprising output
Elapsed time for a batch size of 7: 204007
Elapsed time for a batch size of 8: 237600
Elapsed time for a batch size of 9: 209838
It shows, that for some reason, using a batch_size
of 8, the code is significantly slower.
However, both, using a batch_size
of 7 or 9, is significantly faster.
This is surprising to me, since a batch size of 8 should be the perfect configuration, since it only has to use the AVX-512 instructions and can always perfectly process 64 Byte at a time. Why is this case so significantly slower, though?
Edit:
Added perf
results for cache misses
Batch Size 7
Performance counter stats for process id '653468':
6,894,467,363 L1-dcache-loads (44.43%)
1,647,244,371 L1-dcache-load-misses # 23.89% of all L1-dcache accesses (44.43%)
7,548,224,648 L1-dcache-stores (44.43%)
6,726,036 L2-loads (44.43%)
3,766,847 L2-loads-misses # 56.61% of all LL-cache accesses (44.46%)
6,171,407 L2-loads-stores (44.45%)
6,764,242 LLC-loads (44.46%)
4,548,106 LLC-loads-misses # 68.35% of all LL-cache accesses (44.46%)
6,954,088 LLC-loads-stores (44.45%)
Batch Size 8
Performance counter stats for process id '654880':
1,009,889,247 L1-dcache-loads (44.41%)
1,413,152,123 L1-dcache-load-misses # 139.93% of all L1-dcache accesses (44.45%)
1,528,453,525 L1-dcache-stores (44.48%)
158,053,929 L2-loads (44.51%)
155,407,942 L2-loads-misses # 98.18% of all LL-cache accesses (44.50%)
158,335,431 L2-loads-stores (44.46%)
158,349,901 LLC-loads (44.42%)
155,902,630 LLC-loads-misses # 98.49% of all LL-cache accesses (44.39%)
158,447,095 LLC-loads-stores (44.39%)
11.011153400 seconds time elapsed
Batch Size 9
Performance counter stats for process id '656032':
1,766,679,021 L1-dcache-loads (44.38%)
1,600,639,108 L1-dcache-load-misses # 90.60% of all L1-dcache accesses (44.42%)
2,233,035,727 L1-dcache-stores (44.46%)
138,071,488 L2-loads (44.49%)
136,132,162 L2-loads-misses # 98.51% of all LL-cache accesses (44.52%)
138,020,805 L2-loads-stores (44.49%)
138,522,404 LLC-loads (44.45%)
135,902,197 LLC-loads-misses # 98.35% of all LL-cache accesses (44.42%)
138,122,462 LLC-loads-stores (44.38%)