Używanie instrukcji SIMD w językach C/C++ w Linux

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
  • 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 typu uchar.
  • 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ł.

TUX - maskotka systemu Linux

About the author

Autor "BIELI" to zapalony entuzjasta otwartego oprogramowania, który dzieli się swoją pasją na blogu poznajlinuxa.pl. Jego wpisy są skarbnicą wiedzy na temat Linuxa, programowania oraz najnowszych trendów w świecie technologii. Autor "BIELI" wierzy w siłę społeczności Open Source i zawsze stara się inspirować swoich czytelników do eksplorowania i eksperymentowania z kodem.