thread_hardware_destructive_interference_size

thread_hardware_destructive_interference_size (하드웨어 간섭 크기)

이 페이지는 C++17에서 도입된 std::hardware_destructive_interference_sizestd::hardware_constructive_interference_size 상수에 대해 설명해요. 이 상수들은 L1 데이터 캐시 라인 크기에 대한 이식 가능한 접근을 제공하며, 스레드 간 캐시 간섭을 피하거나 활용하는 데 사용돼요. 이 값들은 구현에 따라 정의되며, 대상 하드웨어의 캐시 특성에 맞춰 데이터 배치를 최적화할 수 있게 해줘요.

출처: cppreference

본문

<new> 헤더에 정의된 두 상수는 다음과 같아요.

Defined in header
inline constexpr std :: size_t hardware_destructive_interference_size = /implementation-defined/ ; (1) (since C++17)
inline constexpr std :: size_t hardware_constructive_interference_size = /implementation-defined/ ; (2) (since C++17)
  • hardware_destructive_interference_size는 서로 다른 스레드가 동시에 접근할 때 캐시 간섭을 피하기 위해 데이터를 분리하는 데 권장되는 최소 크기예요.
  • hardware_constructive_interference_size는 함께 접근되는 데이터를 한 캐시 라인에 모아두는 데 권장되는 최대 크기예요.

다음 예제는 두 상수를 사용해 데이터를 분리하거나 결합하는 방법을 보여줘요.

struct keep_apart
{
    alignas(std::hardware_destructive_interference_size) std::atomic<int> cat;
    alignas(std::hardware_destructive_interference_size) std::atomic<int> dog;
};
struct together
{
    std::atomic<int> dog;
    int puppy;
};

struct kennel
{
    // Other data members...

    alignas(sizeof(together)) together pack;

    // Other data members...
};

static_assert(sizeof(together) <= std::hardware_constructive_interference_size);

참고 사항

이 상수들은 L1 데이터 캐시 라인 크기에 대한 이식 가능한 접근 방법을 제공해요.

Feature-test macro Value Std Feature
__cpp_lib_hardware_interference_size 201703L (C++17) constexpr std :: hardware_constructive_interference_size and constexpr std :: hardware_destructive_interference_size

예제

이 프로그램은 두 개의 스레드를 사용해 주어진 전역 객체의 데이터 멤버에 원자적으로 쓰기를 수행해요. 첫 번째 객체는 하나의 캐시 라인에 들어가므로 "하드웨어 간섭"이 발생해요. 두 번째 객체는 데이터 멤버를 서로 다른 캐시 라인에 분리하므로, 스레드 쓰기 후 발생할 수 있는 "캐시 동기화"를 피할 수 있어요.

#include <atomic>
#include <chrono>
#include <cstddef>
#include <iomanip>
#include <iostream>
#include <mutex>
#include <new>
#include <thread>

#ifdef __cpp_lib_hardware_interference_size
    using std::hardware_constructive_interference_size;
    using std::hardware_destructive_interference_size;
#else
    // 64 bytes on x86-64 │ L1_CACHE_BYTES │ L1_CACHE_SHIFT │ __cacheline_aligned │ ...
    constexpr std::size_t hardware_constructive_interference_size = 64;
    constexpr std::size_t hardware_destructive_interference_size = 64;
#endif

std::mutex cout_mutex;

constexpr int max_write_iterations{10'000'000}; // the benchmark time tuning

struct alignas(hardware_constructive_interference_size)
OneCacheLiner // occupies one cache line
{
    std::atomic_uint64_t x{};
    std::atomic_uint64_t y{};
}
oneCacheLiner;

struct TwoCacheLiner // occupies two cache lines
{
    alignas(hardware_destructive_interference_size) std::atomic_uint64_t x{};
    alignas(hardware_destructive_interference_size) std::atomic_uint64_t y{};
}
twoCacheLiner;

inline auto now() noexcept { return std::chrono::high_resolution_clock::now(); }

template<bool xy>
void oneCacheLinerThread()
{
    const auto start{now()};

    for (uint64_t count{}; count != max_write_iterations; ++count)
        if constexpr (xy)
            oneCacheLiner.x.fetch_add(1, std::memory_order_relaxed);
        else
            oneCacheLiner.y.fetch_add(1, std::memory_order_relaxed);

    const std::chrono::duration<double, std::milli> elapsed{now() - start};
    std::lock_guard lk{cout_mutex};
    std::cout << "oneCacheLinerThread() spent " << elapsed.count() << " ms\n";
    if constexpr (xy)
        oneCacheLiner.x = elapsed.count();
    else
        oneCacheLiner.y = elapsed.count();
}

template<bool xy>
void twoCacheLinerThread()
{
    const auto start{now()};

    for (uint64_t count{}; count != max_write_iterations; ++count)
        if constexpr (xy)
            twoCacheLiner.x.fetch_add(1, std::memory_order_relaxed);
        else
            twoCacheLiner.y.fetch_add(1, std::memory_order_relaxed);

    const std::chrono::duration<double, std::milli> elapsed{now() - start};
    std::lock_guard lk{cout_mutex};
    std::cout << "twoCacheLinerThread() spent " << elapsed.count() << " ms\n";
    if constexpr (xy)
        twoCacheLiner.x = elapsed.count();
    else
        twoCacheLiner.y = elapsed.count();
}

int main()
{
    std::cout << "__cpp_lib_hardware_interference_size "
#   ifdef __cpp_lib_hardware_interference_size
        "= " << __cpp_lib_hardware_interference_size << '\n';
#   else
        "is not defined, use " << hardware_destructive_interference_size
                               << " as fallback\n";
#   endif

    std::cout << "hardware_destructive_interference_size == "
              << hardware_destructive_interference_size << '\n'
              << "hardware_constructive_interference_size == "
              << hardware_constructive_interference_size << "\n\n"
              << std::fixed << std::setprecision(2)
              << "sizeof( OneCacheLiner ) == " << sizeof(OneCacheLiner) << '\n'
              << "sizeof( TwoCacheLiner ) == " << sizeof(TwoCacheLiner) << "\n\n";

    constexpr int max_runs{4};

    int oneCacheLiner_average{0};
    for (auto i{0}; i != max_runs; ++i)
    {
        std::thread th1{oneCacheLinerThread<0>};
        std::thread th2{oneCacheLinerThread<1>};
        th1.join();
        th2.join();
        oneCacheLiner_average += oneCacheLiner.x + oneCacheLiner.y;
    }
    std::cout << "Average T1 time: "
              << (oneCacheLiner_average / max_runs / 2) << " ms\n\n";

    int twoCacheLiner_average{0};
    for (auto i{0}; i != max_runs; ++i)
    {
        std::thread th1{twoCacheLinerThread<0>};
        std::thread th2{twoCacheLinerThread<1>};
        th1.join();
        th2.join();
        twoCacheLiner_average += twoCacheLiner.x + twoCacheLiner.y;
    }
    std::cout << "Average T2 time: "
              << (twoCacheLiner_average / max_runs / 2) << " ms\n\n"
              << "Ratio T1/T2:~ "
              << 1.0 * oneCacheLiner_average / twoCacheLiner_average << '\n';
}

가능한 출력은 다음과 같아요.

__cpp_lib_hardware_interference_size = 201703
hardware_destructive_interference_size == 64
hardware_constructive_interference_size == 64

sizeof( OneCacheLiner ) == 64
sizeof( TwoCacheLiner ) == 128

oneCacheLinerThread() spent 517.83 ms
oneCacheLinerThread() spent 533.43 ms
oneCacheLinerThread() spent 527.36 ms
oneCacheLinerThread() spent 555.69 ms
oneCacheLinerThread() spent 574.74 ms
oneCacheLinerThread() spent 591.66 ms
oneCacheLinerThread() spent 555.63 ms
oneCacheLinerThread() spent 555.76 ms
Average T1 time: 550 ms

twoCacheLinerThread() spent 89.79 ms
twoCacheLinerThread() spent 89.94 ms
twoCacheLinerThread() spent 89.46 ms
twoCacheLinerThread() spent 90.28 ms
twoCacheLinerThread() spent 89.73 ms
twoCacheLinerThread() spent 91.11 ms
twoCacheLinerThread() spent 89.17 ms
twoCacheLinerThread() spent 90.09 ms
Average T2 time: 89 ms

Ratio T1/T2:~ 6.16

같이 보기

hardware_concurrency [static] returns the number of concurrent threads supported by the implementation (public static member function of std::thread ) [edit]
hardware_concurrency [static] returns the number of concurrent threads supported by the implementation (public static member function of std::jthread ) [edit]

더 알아보기 (Learn more)

cppreference