Skip to content

Commit 1cf9d25

Browse files
committed
Add vectorization comparison benchmark and investigation notes
1 parent d7b066e commit 1cf9d25

3 files changed

Lines changed: 240 additions & 0 deletions

File tree

perf-tests/CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,7 @@ include_directories(../include ../include/FDTD ../include/FDTD_kokkos ../src ../
22

33
add_subdirectory(sample)
44
add_subdirectory(kokkos_sample)
5+
6+
add_executable(vectorization_compare vectorization_compare.cpp)
7+
target_link_libraries(vectorization_compare Kokkos::kokkos)
8+
set_target_properties(vectorization_compare PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/bin)

perf-tests/README_vectorization.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# Vectorization comparison micro-benchmark
2+
3+
Этот файл добавляет простой и воспроизводимый тест, чтобы сравнить:
4+
5+
1. OpenMP (обычный двойной цикл).
6+
2. Kokkos `MDRangePolicy` + `LayoutRight`.
7+
3. Kokkos `MDRangePolicy` + `LayoutLeft` (две стратегии обхода индексов).
8+
4. «Ручную» SIMD-векторизацию через `Kokkos::Experimental::native_simd`.
9+
10+
## Сборка
11+
12+
```bash
13+
mkdir -p build
14+
cd build
15+
cmake ..
16+
cmake --build . --target vectorization_compare -j
17+
```
18+
19+
## Запуск
20+
21+
```bash
22+
./bin/vectorization_compare 2048 80
23+
```
24+
25+
Аргументы:
26+
- `N` — размер двумерной сетки `N x N`.
27+
- `reps` — число повторов ядра.
28+
29+
## Что именно сравнивается
30+
31+
Вычисление для каждой ячейки:
32+
33+
```text
34+
c = c + 0.75 * a + 0.25 * b
35+
```
36+
37+
Это минимальный memory-bound kernel без сложной физики, чтобы фокус был на доступе в память и векторизации.
38+
39+
## Как интерпретировать
40+
41+
- `LayoutRight` обычно лучше, когда внутренний индекс (`i`) идёт по contiguous памяти.
42+
- Для `LayoutLeft` выгоднее делать «левый» порядок итерации policy (`Iterate::Left`), иначе легко получить strided access.
43+
- Вариант с `native_simd` показывает, что происходит при явном использовании SIMD-регистров.
44+
45+
## Как проверить причины слабой автo-векторизации в Kokkos
46+
47+
В этом окружении submodules не подтянулись из-за сетевых ограничений (доступ к GitHub запрещён), поэтому напрямую посмотреть исходники backend OpenMP не удалось.
48+
49+
После инициализации submodules можно проверить следующие места в Kokkos:
50+
51+
```bash
52+
rg -n "pragma omp|omp simd|ivdep|unroll" 3rdparty/kokkos/core/src/OpenMP 3rdparty/kokkos/core/src/impl
53+
rg -n "KOKKOS_ENABLE_PRAGMA" 3rdparty/kokkos
54+
rg -n "struct ParallelFor" 3rdparty/kokkos/core/src/OpenMP
55+
```
56+
57+
Практически полезно дополнительно собирать с отчётом векторизации:
58+
59+
```bash
60+
cmake .. -DCMAKE_CXX_FLAGS="-O3 -fopenmp -march=native -fopt-info-vec-optimized -fopt-info-vec-missed"
61+
cmake --build . --target vectorization_compare -j
62+
```
63+
64+
И сравнить, какие циклы компилятор реально векторизует в:
65+
- OpenMP-версии;
66+
- Kokkos `MDRangePolicy` с разными layout/iterate;
67+
- SIMD-версии (где векторизация явная).
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
#include <Kokkos_Core.hpp>
2+
#include <Kokkos_SIMD.hpp>
3+
4+
#include <algorithm>
5+
#include <chrono>
6+
#include <cmath>
7+
#include <cstdlib>
8+
#include <iomanip>
9+
#include <iostream>
10+
#include <numeric>
11+
#include <string>
12+
#include <vector>
13+
14+
namespace {
15+
using Clock = std::chrono::high_resolution_clock;
16+
17+
template <class Func>
18+
double measure_seconds(Func&& fn) {
19+
const auto start = Clock::now();
20+
fn();
21+
const auto stop = Clock::now();
22+
return std::chrono::duration<double>(stop - start).count();
23+
}
24+
25+
double checksum(const std::vector<double>& v) {
26+
return std::accumulate(v.begin(), v.end(), 0.0);
27+
}
28+
29+
void init_arrays(std::vector<double>& a, std::vector<double>& b, std::vector<double>& c,
30+
int n) {
31+
for (int j = 0; j < n; ++j) {
32+
for (int i = 0; i < n; ++i) {
33+
const int idx = j * n + i;
34+
a[idx] = std::sin(0.001 * static_cast<double>(i + j));
35+
b[idx] = std::cos(0.0015 * static_cast<double>(i + 2 * j));
36+
c[idx] = 0.0;
37+
}
38+
}
39+
}
40+
41+
double run_openmp(int n, int reps) {
42+
std::vector<double> a(n * n), b(n * n), c(n * n);
43+
init_arrays(a, b, c, n);
44+
45+
const double elapsed = measure_seconds([&]() {
46+
for (int rep = 0; rep < reps; ++rep) {
47+
#pragma omp parallel for schedule(static)
48+
for (int j = 0; j < n; ++j) {
49+
for (int i = 0; i < n; ++i) {
50+
const int idx = j * n + i;
51+
c[idx] += 0.75 * a[idx] + 0.25 * b[idx];
52+
}
53+
}
54+
}
55+
});
56+
57+
std::cout << "openmp_checksum=" << std::setprecision(15) << checksum(c) << '\n';
58+
return elapsed;
59+
}
60+
61+
template <class Layout, class IterateOuter, class IterateInner>
62+
double run_kokkos_layout(const std::string& label, int n, int reps) {
63+
using View2D = Kokkos::View<double**, Layout, Kokkos::DefaultExecutionSpace>;
64+
65+
View2D a("a", n, n), b("b", n, n), c("c", n, n);
66+
67+
Kokkos::parallel_for(
68+
"init", Kokkos::MDRangePolicy<Kokkos::Rank<2>>({0, 0}, {n, n}),
69+
KOKKOS_LAMBDA(const int j, const int i) {
70+
a(j, i) = sin(0.001 * static_cast<double>(i + j));
71+
b(j, i) = cos(0.0015 * static_cast<double>(i + 2 * j));
72+
c(j, i) = 0.0;
73+
});
74+
Kokkos::fence();
75+
76+
const double elapsed = measure_seconds([&]() {
77+
for (int rep = 0; rep < reps; ++rep) {
78+
Kokkos::parallel_for(
79+
label,
80+
Kokkos::MDRangePolicy<Kokkos::Rank<2, IterateOuter, IterateInner>>({0, 0},
81+
{n, n}),
82+
KOKKOS_LAMBDA(const int j, const int i) {
83+
c(j, i) += 0.75 * a(j, i) + 0.25 * b(j, i);
84+
});
85+
}
86+
Kokkos::fence();
87+
});
88+
89+
auto c_host = Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace(), c);
90+
double sum = 0.0;
91+
for (int j = 0; j < n; ++j) {
92+
for (int i = 0; i < n; ++i) {
93+
sum += c_host(j, i);
94+
}
95+
}
96+
std::cout << label << "_checksum=" << std::setprecision(15) << sum << '\n';
97+
return elapsed;
98+
}
99+
100+
double run_kokkos_simd(int n, int reps) {
101+
using simd_t = Kokkos::Experimental::native_simd<double>;
102+
constexpr int lanes = simd_t::size();
103+
104+
std::vector<double> a(n * n), b(n * n), c(n * n);
105+
init_arrays(a, b, c, n);
106+
107+
const int total = n * n;
108+
const int simd_end = (total / lanes) * lanes;
109+
110+
const double elapsed = measure_seconds([&]() {
111+
for (int rep = 0; rep < reps; ++rep) {
112+
#pragma omp parallel for schedule(static)
113+
for (int idx = 0; idx < simd_end; idx += lanes) {
114+
simd_t av, bv, cv;
115+
av.copy_from(a.data() + idx, Kokkos::Experimental::simd_flag_default);
116+
bv.copy_from(b.data() + idx, Kokkos::Experimental::simd_flag_default);
117+
cv.copy_from(c.data() + idx, Kokkos::Experimental::simd_flag_default);
118+
cv += 0.75 * av + 0.25 * bv;
119+
cv.copy_to(c.data() + idx, Kokkos::Experimental::simd_flag_default);
120+
}
121+
for (int idx = simd_end; idx < total; ++idx) {
122+
c[idx] += 0.75 * a[idx] + 0.25 * b[idx];
123+
}
124+
}
125+
});
126+
127+
std::cout << "kokkos_simd_checksum=" << std::setprecision(15) << checksum(c) << '\n';
128+
return elapsed;
129+
}
130+
131+
} // namespace
132+
133+
int main(int argc, char* argv[]) {
134+
int n = 2048;
135+
int reps = 80;
136+
if (argc > 1) n = std::atoi(argv[1]);
137+
if (argc > 2) reps = std::atoi(argv[2]);
138+
139+
Kokkos::initialize(argc, argv);
140+
{
141+
std::cout << "N=" << n << ", reps=" << reps << '\n';
142+
143+
const double t_openmp = run_openmp(n, reps);
144+
const double t_right =
145+
run_kokkos_layout<Kokkos::LayoutRight, Kokkos::Iterate::Right, Kokkos::Iterate::Right>(
146+
"kokkos_layout_right_rr", n, reps);
147+
const double t_left_bad =
148+
run_kokkos_layout<Kokkos::LayoutLeft, Kokkos::Iterate::Right, Kokkos::Iterate::Right>(
149+
"kokkos_layout_left_rr", n, reps);
150+
const double t_left_good =
151+
run_kokkos_layout<Kokkos::LayoutLeft, Kokkos::Iterate::Left, Kokkos::Iterate::Left>(
152+
"kokkos_layout_left_ll", n, reps);
153+
const double t_simd = run_kokkos_simd(n, reps);
154+
155+
std::cout << std::fixed << std::setprecision(6);
156+
std::cout << "openmp_time_s=" << t_openmp << '\n';
157+
std::cout << "kokkos_layout_right_rr_time_s=" << t_right
158+
<< " speedup_vs_openmp=" << t_openmp / t_right << '\n';
159+
std::cout << "kokkos_layout_left_rr_time_s=" << t_left_bad
160+
<< " speedup_vs_openmp=" << t_openmp / t_left_bad << '\n';
161+
std::cout << "kokkos_layout_left_ll_time_s=" << t_left_good
162+
<< " speedup_vs_openmp=" << t_openmp / t_left_good << '\n';
163+
std::cout << "kokkos_simd_time_s=" << t_simd << " speedup_vs_openmp=" << t_openmp / t_simd
164+
<< '\n';
165+
}
166+
Kokkos::finalize();
167+
168+
return 0;
169+
}

0 commit comments

Comments
 (0)