Często użytkownicy Linuxa zadają sobie pytanie, jak użyć instrukcji SIMD w językach C/C++ w praktyce programistycznej.
Przykład kodu SIMD w C/C++
1. Dodawanie dwóch wektorów za pomocą SSE
#include <iostream>
#include <xmmintrin.h> // SSE
int main() {
float a[4] = {1.0, 2.0, 3.0, 4.0};
float b[4] = {5.0, 6.0, 7.0, 8.0};
float result[4];
__m128 va = _mm_loadu_ps(a); // Załaduj dane do rejestru SIMD
__m128 vb = _mm_loadu_ps(b);
__m128 vr = _mm_add_ps(va, vb); // Dodaj wektory
_mm_storeu_ps(result, vr); // Zapisz wynik
for (int i = 0; i < 4; ++i)
std::cout << result[i] << " ";
return 0;
}
2. Dodawanie za pomocą AVX (dla 8 elementów float)
#include <iostream>
#include <immintrin.h> // AVX
int main() {
float a[8] = {1,2,3,4,5,6,7,8};
float b[8] = {8,7,6,5,4,3,2,1};
float result[8];
__m256 va = _mm256_loadu_ps(a);
__m256 vb = _mm256_loadu_ps(b);
__m256 vr = _mm256_add_ps(va, vb);
_mm256_storeu_ps(result, vr);
for (int i = 0; i < 8; ++i)
std::cout << result[i] << " ";
return 0;
}
Kompilacja z odpowiednimi flagami
Aby skompilować kod z użyciem SIMD, użyj:
$ g++ -O3 -march=native -o simd_example simd_example.cpp
# przykład działania pierwszego programu
$ ./simd_example_sse
6 8 10 12
# przykład działania drugiego programu
$ ./simd_example_avx
9 9 9 9 9 9 9 9
Flaga kompilatora -march=native
pozwala kompilatorowi wygenerować instrukcje SIMD zgodne z Twoim CPU.
W celu obsługi SIMD w C konieczne jest załączanie odpowiednich plików nagłówkowych (wersja generyczna dla architektu x86, x86_64 oraz ARM):
#ifdef _MSC_VER
#include <intrin.h>
#else
#if defined __x86_64__ || defined __i386__
#include <x86intrin.h>
#elif defined __ARM_NEON__
#include <arm_neon.h>
#endif
#endif
Biblioteki wspierające SIMD dla C/C++
Pojawia się od razu pytanie: czy są jakieś biblioteki standardowe w C i w C++ robią to co w powyższym przykładzie transparentnie pod spodem, abym mógł korzystać z wysokopoziomowego API?
Biblioteki w C/C++ z transparentnym wsparciem SIMD
1. Eigen (C++)
- Opis: Biblioteka do obliczeń macierzowych i liniowych.
- SIMD: Automatycznie wykorzystuje SSE/AVX/NEON, jeśli są dostępne.
- Zalety: Bardzo czytelne API, przypomina kod matematyczny.
- Przykład CPP:
#include <Eigen/Dense>
Eigen::Vector4f a(1, 2, 3, 4);
Eigen::Vector4f b(5, 6, 7, 8);
Eigen::Vector4f c = a + b; // SIMD pod spodem
2. xtensor (C++)
- Opis: Biblioteka do obliczeń numerycznych, podobna do NumPy.
- SIMD: Wspiera SIMD przez
xsimd
, który automatycznie wybiera najlepsze instrukcje. - Zalety: Wysokopoziomowe operacje na tablicach, kompatybilna z Pythonem.
- Przykład CPP:
#include <xtensor/xarray.hpp>
xt::xarray<float> a = {1, 2, 3, 4};
xt::xarray<float> b = {5, 6, 7, 8};
auto c = a + b; // SIMD przez xsimd
3. xsimd (C++)
- Opis: Niskopoziomowa biblioteka SIMD, ale z wysokopoziomowym API.
- SIMD: Obsługuje SSE, AVX, NEON, AVX512.
- Zalety: Można pisać kod wektorowy bez ręcznego używania rejestrów.
- Przykład CPP:
#include <xsimd/xsimd.hpp>
xsimd::batch<float, 4> a = {1, 2, 3, 4};
xsimd::batch<float, 4> b = {5, 6, 7, 8};
auto c = a + b; // SIMD automatycznie
4. Intel IPP / MKL (C)
- Opis: Biblioteki od Intela do obliczeń sygnałowych i matematycznych.
- SIMD: Silnie zoptymalizowane pod AVX/AVX2/AVX512.
- Zalety: Maksymalna wydajność na procesorach Intela.
- Uwaga: Wymaga licencji (częściowo darmowa).
5. OpenCV (C++)
- Opis: Biblioteka do przetwarzania obrazów i widzenia komputerowego.
- SIMD: Wiele funkcji (np.
cv::filter2D
,cv::resize
) korzysta z SIMD automatycznie. - Zalety: Nie trzeba pisać kodu SIMD – wystarczy używać API.
- SIMD jest aktywowany, jeśli:
- Kompilujesz OpenCV z flagami:
WITH_OPENCL=ON
- lub
ENABLE_SSE=ON
,ENABLE_AVX=ON
- Kompilujesz OpenCV z flagami:
- SIMD jest aktywowany, jeśli:
- Przykład dodawania dwóch obrazków w CPP:
#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
// Załaduj dwa obrazy w skali szarości
cv::Mat img1 = cv::imread("image1.jpg", cv::IMREAD_GRAYSCALE);
cv::Mat img2 = cv::imread("image2.jpg", cv::IMREAD_GRAYSCALE);
if (img1.empty() || img2.empty()) {
std::cerr << "Nie można załadować obrazów!" << std::endl;
return -1;
}
// Upewnij się, że obrazy mają ten sam rozmiar
cv::resize(img2, img2, img1.size());
// Dodaj obrazy – OpenCV używa SIMD automatycznie
cv::Mat result;
cv::add(img1, img2, result);
// Pokaż wynik
cv::imshow("Wynik dodawania", result);
cv::waitKey(0);
return 0;
}
#Kompilacja:
# $ g++ -O3 -march=native -o simd_opencv simd_opencv.cpp `pkg-config --cflags --libs opencv4`
Przykład niskopoziomowego API OpenCV do SIMD
Poniżej znajdziesz przykład użycia niskopoziomowego API OpenCV do SIMD, czyli modułu cv::hal
, który pozwala pisać własne funkcje z wykorzystaniem SIMD w sposób przenośny i zoptymalizowany.
#include <opencv2/core.hpp>
#include <opencv2/imgcodecs.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/core/hal/intrin.hpp>
#include <iostream>
void simd_add(const cv::Mat& a, const cv::Mat& b, cv::Mat& out) {
CV_Assert(a.size() == b.size() && a.type() == b.type());
out.create(a.size(), a.type());
const int total = a.rows * a.cols;
const uchar* pa = a.ptr();
const uchar* pb = b.ptr();
uchar* pout = out.ptr();
int i = 0;
// SIMD przetwarzanie 16 bajtów naraz (dla uchar)
for (; i <= total - 16; i += 16) {
cv::v_uint8x16 va = cv::v_load(pa + i);
cv::v_uint8x16 vb = cv::v_load(pb + i);
cv::v_uint8x16 vr = cv::v_add_wrap(va, vb); // dodanie z zawijaniem
cv::v_store(pout + i, vr);
}
// Pozostałe bajty (jeśli niepodzielne przez 16)
for (; i < total; ++i) {
pout[i] = pa[i] + pb[i];
}
}
int main() {
cv::Mat img1 = cv::imread("image1.jpg", cv::IMREAD_GRAYSCALE);
cv::Mat img2 = cv::imread("image2.jpg", cv::IMREAD_GRAYSCALE);
if (img1.empty() || img2.empty()) {
std::cerr << "Nie można załadować obrazów!" << std::endl;
return -1;
}
cv::resize(img2, img2, img1.size());
cv::Mat result;
simd_add(img1, img2, result);
cv::imshow("Wynik SIMD", result);
cv::waitKey(0);
return 0;
}
Kompilacja:
$ g++ -O3 -march=native -o simd_hal_example simd_hal_example.cpp `pkg-config --cflags --libs opencv4`
Co robi ten kod z niskopoziomowego API do OpenCV pod SIMD?
- Używa
cv::v_uint8x16
– wektora 16 bajtów typuuchar
. - Funkcja
cv::v_load
ładuje dane do rejestru SIMD. cv::v_add_wrap
dodaje dwa wektory z zawijaniem (czyli bez przepełnienia).cv::v_store
zapisuje wynik do pamięci.
Dzięki temu kod działa szybciej niż klasyczna pętla, a OpenCV automatycznie wybiera najlepsze instrukcje SIMD dla Twojej architektury (SSE, AVX, NEON itd.).
Podsumowanie
Kompilatory takie jak GCC i Clang potrafią automatycznie generować kod SIMD z prostych pętli, jeśli użyjesz flagi -O3 -march=native
.
Możesz też używać OpenMP z #pragma omp simd
do wymuszenia wektoryzacji.
Jeśli chcecie zgłębić temat SIMD i C++ polecam ten materiał.