2016-09-24 20 views
34

C++ 17 đã thêm std::hardware_destructive_interference_size and std::hardware_constructive_interference_size. Đầu tiên, tôi nghĩ rằng đó chỉ là một cách di động để có được kích thước của một dòng bộ nhớ cache L1 nhưng đó là một sự đơn giản hóa.Tìm hiểu std :: hardware_destructive_interference_size và std :: hardware_constructive_interference_size

Câu hỏi:

  • Làm thế nào là những hằng số liên quan đến kích thước L1 dòng bộ nhớ cache?
  • Có ví dụ điển hình nào thể hiện trường hợp sử dụng của họ không?
  • Cả hai đều được xác định static constexpr. Đó không phải là một vấn đề nếu bạn xây dựng một nhị phân và thực hiện nó trên các máy khác với kích thước dòng bộ nhớ cache khác nhau? Làm thế nào nó có thể bảo vệ chống lại việc chia sẻ sai trong kịch bản đó khi bạn không chắc chắn về máy mà mã của bạn sẽ chạy?

Trả lời

25

Mục đích của các hằng số này thực sự là nhận được kích thước đường bộ nhớ cache. Nơi tốt nhất để đọc về cơ sở lý luận cho họ là trong đề xuất riêng của mình:

http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0154r1.html

tôi sẽ trích dẫn một đoạn trong hợp lý ở đây để dễ-of-đọc:

[. ..] độ chi tiết của bộ nhớ không can thiệp (đến thứ tự đầu tiên) [thường] được gọi là kích thước dòng bộ nhớ cache .

Sử dụng bộ nhớ cache -line kích thước rơi vào hai loại lớn:

  • Tránh giao thoa triệt tiêu (giả chia sẻ) giữa các đối tượng với các mẫu truy cập thời gian chạy tạm rời nhau từ chủ đề khác nhau.
  • Tăng cường nhiễu xây dựng (chia sẻ thực) giữa các đối tượng có mẫu truy cập thời gian chạy cục bộ theo thời gian.

Vấn đề quan trọng nhất với số lượng thực hiện hữu ích này là tính khả thi của các phương pháp được sử dụng trong thực tiễn hiện tại để xác định giá trị của nó, mặc dù phổ biến và phổ biến như một nhóm. [...]

Chúng tôi mong muốn đóng góp một phát minh khiêm tốn cho nguyên nhân này, trừu tượng về số lượng này có thể được thận trọng được xác định cho các mục đích do triển khai:

  • kích thước giao thoa triệt tiêu: một số đó là thích hợp như là một bù đắp giữa hai đối tượng có khả năng tránh chia sẻ sai do các kiểu truy cập thời gian chạy khác nhau từ các luồng khác nhau.
  • Kích thước can thiệp xây dựng: một số thích hợp làm giới hạn kích thước dấu chân bộ nhớ kết hợp của hai đối tượng và căn chỉnh để có khả năng quảng bá chia sẻ thực sự giữa chúng.

Trong cả hai trường hợp, các giá trị này được cung cấp trên cơ sở chất lượng thực hiện, hoàn toàn là gợi ý có khả năng cải thiện hiệu suất. Đây là những giá trị di động lý tưởng để sử dụng với từ khóa alignas(), hiện có gần như không có sử dụng di động được hỗ trợ tiêu chuẩn.


"Làm thế nào là những hằng số liên quan đến kích thước L1 dòng bộ nhớ cache?"

Về lý thuyết, khá trực tiếp.

Giả sử trình biên dịch biết chính xác kiến ​​trúc mà bạn sẽ chạy trên đó - sau đó chúng gần như chắc chắn sẽ cung cấp cho bạn kích thước dòng bộ nhớ cache L1 chính xác. (Như đã lưu ý sau, đây là một giả định lớn.)

Đối với những gì đáng giá, tôi hầu như luôn mong đợi các giá trị này giống nhau. Tôi tin rằng lý do duy nhất họ được tuyên bố riêng là để hoàn thành. (Điều đó nói rằng, có lẽ một trình biên dịch muốn ước tính L2 cache size-line thay vì L1 cache size-line cho giao thoa tăng cường; Tôi không biết nếu điều này sẽ thực sự có ích, mặc dù.)


" Có một ví dụ điển hình cho thấy các trường hợp sử dụng của họ không? "

Ở cuối câu trả lời này tôi đã đính kèm chương trình điểm chuẩn dài thể hiện chia sẻ sai và chia sẻ thực.

Nó thể hiện chia sẻ sai bằng cách phân bổ một mảng trình bao bọc int: trong một trường hợp nhiều phần tử phù hợp trong dòng bộ nhớ cache L1 và phần tử đơn lẻ khác chiếm dòng bộ đệm L1. Trong một vòng lặp chặt chẽ, một phần tử cố định được chọn từ mảng và được cập nhật liên tục.

Nó thể hiện chia sẻ thực sự bằng cách phân bổ một cặp int trong một trình bao bọc: trong một trường hợp, hai int trong cặp không khớp với kích thước dòng bộ nhớ cache L1 với nhau và ngược lại. Trong một vòng lặp chặt chẽ, mỗi phần tử của cặp được cập nhật nhiều lần.

Lưu ý rằng mã để truy cập đối tượng đang thử nghiệm không không thay đổi; sự khác biệt duy nhất là cách bố trí và căn chỉnh của các đối tượng.

Tôi không có trình biên dịch C++ 17 (và giả sử hầu hết mọi người hiện không có), vì vậy tôi đã thay thế các hằng số được đề cập với chính mình. Bạn cần cập nhật các giá trị này chính xác trên máy của mình. Điều đó nói rằng, 64 byte có lẽ là giá trị chính xác trên phần cứng máy tính để bàn hiện đại điển hình (tại thời điểm viết bài).

Cảnh báo: thử nghiệm sẽ sử dụng tất cả các lõi trên máy của bạn và phân bổ ~ 256MB bộ nhớ. Đừng quên để biên dịch với tối ưu hóa!

Trên máy tính của tôi, đầu ra là:

 
Hardware concurrency: 16 
sizeof(naive_int): 4 
alignof(naive_int): 4 
sizeof(cache_int): 64 
alignof(cache_int): 64 
sizeof(bad_pair): 72 
alignof(bad_pair): 4 
sizeof(good_pair): 8 
alignof(good_pair): 4 
Running naive_int test. 
Average time: 0.0873625 seconds, useless result: 3291773 
Running cache_int test. 
Average time: 0.024724 seconds, useless result: 3286020 
Running bad_pair test. 
Average time: 0.308667 seconds, useless result: 6396272 
Running good_pair test. 
Average time: 0.174936 seconds, useless result: 6668457 

tôi nhận được ~ 3.5x tăng tốc bằng cách tránh giả chia sẻ, và ~ 1.7 lần tăng tốc bằng cách đảm bảo đúng chia sẻ.


"Cả hai đều được định nghĩa constexpr tĩnh. Đó là không phải là một vấn đề nếu bạn xây dựng một nhị phân và thực hiện nó trên các máy khác với dòng bộ nhớ cache kích thước khác nhau? Làm thế nào nó có thể bảo vệ chống lại việc chia sẻ sai trong kịch bản đó khi bạn đang không chắc chắn mã máy của bạn sẽ chạy ở đâu? "

Điều này thực sự sẽ là một vấn đề.Các hằng số này không được đảm bảo ánh xạ tới bất kỳ kích thước đường bộ nhớ cache nào trên máy đích cụ thể, nhưng được dự định là xấp xỉ tốt nhất mà trình biên dịch có thể tập hợp lại.

Điều này được ghi chú trong đề xuất và trong phụ lục, chúng đưa ra ví dụ về cách một số thư viện cố gắng phát hiện kích thước dòng bộ nhớ cache tại thời gian biên dịch dựa trên các gợi ý và macro môi trường khác nhau. Bạn được đảm bảo rằng giá trị này ít nhất là alignof(max_align_t), mức ràng buộc thấp hơn rõ ràng.

Nói cách khác, giá trị này nên được sử dụng làm trường hợp dự phòng của bạn; bạn được tự do để xác định một giá trị chính xác nếu bạn biết điều đó, ví dụ .:

constexpr std::size_t cache_line_size() { 
#ifdef KNOWN_L1_CACHE_LINE_SIZE 
    return KNOWN_L1_CACHE_LINE_SIZE; 
#else 
    return std::hardware_destructive_interference_size; 
#endif 
} 

Trong biên soạn, nếu bạn muốn đảm nhận một kích thước bộ nhớ cache-line chỉ xác định KNOWN_L1_CACHE_LINE_SIZE.

Hy vọng điều này sẽ hữu ích! chương trình

Benchmark:

#include <chrono> 
#include <condition_variable> 
#include <cstddef> 
#include <functional> 
#include <future> 
#include <iostream> 
#include <random> 
#include <thread> 
#include <vector> 

// !!! YOU MUST UPDATE THIS TO BE ACCURATE !!! 
constexpr std::size_t hardware_destructive_interference_size = 64; 

// !!! YOU MUST UPDATE THIS TO BE ACCURATE !!! 
constexpr std::size_t hardware_constructive_interference_size = 64; 

constexpr unsigned kTimingTrialsToComputeAverage = 100; 
constexpr unsigned kInnerLoopTrials = 1000000; 

typedef unsigned useless_result_t; 
typedef double elapsed_secs_t; 

//////// CODE TO BE SAMPLED: 

// wraps an int, default alignment allows false-sharing 
struct naive_int { 
    int value; 
}; 
static_assert(alignof(naive_int) < hardware_destructive_interference_size, ""); 

// wraps an int, cache alignment prevents false-sharing 
struct cache_int { 
    alignas(hardware_destructive_interference_size) int value; 
}; 
static_assert(alignof(cache_int) == hardware_destructive_interference_size, ""); 

// wraps a pair of int, purposefully pushes them too far apart for true-sharing 
struct bad_pair { 
    int first; 
    char padding[hardware_constructive_interference_size]; 
    int second; 
}; 
static_assert(sizeof(bad_pair) > hardware_constructive_interference_size, ""); 

// wraps a pair of int, ensures they fit nicely together for true-sharing 
struct good_pair { 
    int first; 
    int second; 
}; 
static_assert(sizeof(good_pair) <= hardware_constructive_interference_size, ""); 

// accesses a specific array element many times 
template <typename T, typename Latch> 
useless_result_t sample_array_threadfunc(
    Latch& latch, 
    unsigned thread_index, 
    T& vec) { 
    // prepare for computation 
    std::random_device rd; 
    std::mt19937 mt{ rd() }; 
    std::uniform_int_distribution<int> dist{ 0, 4096 }; 

    auto& element = vec[vec.size()/2 + thread_index]; 

    latch.count_down_and_wait(); 

    // compute 
    for (unsigned trial = 0; trial != kInnerLoopTrials; ++trial) { 
     element.value = dist(mt); 
    } 

    return static_cast<useless_result_t>(element.value); 
} 

// accesses a pair's elements many times 
template <typename T, typename Latch> 
useless_result_t sample_pair_threadfunc(
    Latch& latch, 
    unsigned thread_index, 
    T& pair) { 
    // prepare for computation 
    std::random_device rd; 
    std::mt19937 mt{ rd() }; 
    std::uniform_int_distribution<int> dist{ 0, 4096 }; 

    latch.count_down_and_wait(); 

    // compute 
    for (unsigned trial = 0; trial != kInnerLoopTrials; ++trial) { 
     pair.first = dist(mt); 
     pair.second = dist(mt); 
    } 

    return static_cast<useless_result_t>(pair.first) + 
     static_cast<useless_result_t>(pair.second); 
} 

//////// UTILITIES: 

// utility: allow threads to wait until everyone is ready 
class threadlatch { 
public: 
    explicit threadlatch(const std::size_t count) : 
     count_{ count } 
    {} 

    void count_down_and_wait() { 
     std::unique_lock<std::mutex> lock{ mutex_ }; 
     if (--count_ == 0) { 
      cv_.notify_all(); 
     } 
     else { 
      cv_.wait(lock, [&] { return count_ == 0; }); 
     } 
    } 

private: 
    std::mutex mutex_; 
    std::condition_variable cv_; 
    std::size_t count_; 
}; 

// utility: runs a given function in N threads 
std::tuple<useless_result_t, elapsed_secs_t> run_threads(
    const std::function<useless_result_t(threadlatch&, unsigned)>& func, 
    const unsigned num_threads) { 
    threadlatch latch{ num_threads + 1 }; 

    std::vector<std::future<useless_result_t>> futures; 
    std::vector<std::thread> threads; 
    for (unsigned thread_index = 0; thread_index != num_threads; ++thread_index) { 
     std::packaged_task<useless_result_t()> task{ 
      std::bind(func, std::ref(latch), thread_index) 
     }; 

     futures.push_back(task.get_future()); 
     threads.push_back(std::thread(std::move(task))); 
    } 

    const auto starttime = std::chrono::high_resolution_clock::now(); 

    latch.count_down_and_wait(); 
    for (auto& thread : threads) { 
     thread.join(); 
    } 

    const auto endtime = std::chrono::high_resolution_clock::now(); 
    const auto elapsed = std::chrono::duration_cast< 
     std::chrono::duration<double>>(
      endtime - starttime 
      ).count(); 

    useless_result_t result = 0; 
    for (auto& future : futures) { 
     result += future.get(); 
    } 

    return std::make_tuple(result, elapsed); 
} 

// utility: sample the time it takes to run func on N threads 
void run_tests(
    const std::function<useless_result_t(threadlatch&, unsigned)>& func, 
    const unsigned num_threads) { 
    useless_result_t final_result = 0; 
    double avgtime = 0.0; 
    for (unsigned trial = 0; trial != kTimingTrialsToComputeAverage; ++trial) { 
     const auto result_and_elapsed = run_threads(func, num_threads); 
     const auto result = std::get<useless_result_t>(result_and_elapsed); 
     const auto elapsed = std::get<elapsed_secs_t>(result_and_elapsed); 

     final_result += result; 
     avgtime = (avgtime * trial + elapsed)/(trial + 1); 
    } 

    std::cout 
     << "Average time: " << avgtime 
     << " seconds, useless result: " << final_result 
     << std::endl; 
} 

int main() { 
    const auto cores = std::thread::hardware_concurrency(); 
    std::cout << "Hardware concurrency: " << cores << std::endl; 

    std::cout << "sizeof(naive_int): " << sizeof(naive_int) << std::endl; 
    std::cout << "alignof(naive_int): " << alignof(naive_int) << std::endl; 
    std::cout << "sizeof(cache_int): " << sizeof(cache_int) << std::endl; 
    std::cout << "alignof(cache_int): " << alignof(cache_int) << std::endl; 
    std::cout << "sizeof(bad_pair): " << sizeof(bad_pair) << std::endl; 
    std::cout << "alignof(bad_pair): " << alignof(bad_pair) << std::endl; 
    std::cout << "sizeof(good_pair): " << sizeof(good_pair) << std::endl; 
    std::cout << "alignof(good_pair): " << alignof(good_pair) << std::endl; 

    { 
     std::cout << "Running naive_int test." << std::endl; 

     std::vector<naive_int> vec; 
     vec.resize((1u << 28)/sizeof(naive_int)); // allocate 256 mibibytes 

     run_tests([&](threadlatch& latch, unsigned thread_index) { 
      return sample_array_threadfunc(latch, thread_index, vec); 
     }, cores); 
    } 
    { 
     std::cout << "Running cache_int test." << std::endl; 

     std::vector<cache_int> vec; 
     vec.resize((1u << 28)/sizeof(cache_int)); // allocate 256 mibibytes 

     run_tests([&](threadlatch& latch, unsigned thread_index) { 
      return sample_array_threadfunc(latch, thread_index, vec); 
     }, cores); 
    } 
    { 
     std::cout << "Running bad_pair test." << std::endl; 

     bad_pair p; 

     run_tests([&](threadlatch& latch, unsigned thread_index) { 
      return sample_pair_threadfunc(latch, thread_index, p); 
     }, cores); 
    } 
    { 
     std::cout << "Running good_pair test." << std::endl; 

     good_pair p; 

     run_tests([&](threadlatch& latch, unsigned thread_index) { 
      return sample_pair_threadfunc(latch, thread_index, p); 
     }, cores); 
    } 
} 
+16

Tôi là tác giả đề nghị, câu trả lời tuyệt vời! Để làm rõ một điểm bạn đã thực hiện: "Tôi hầu như luôn luôn mong đợi các giá trị này giống nhau. Tôi tin rằng lý do duy nhất chúng được khai báo riêng biệt là để hoàn thành.". Có, chúng phải luôn luôn giống nhau trừ khi: 1) ISA đã được vận chuyển với các kích thước bộ nhớ đệm khác nhau và không có vòm đích nào được cho; 2) bạn đang nhắm mục tiêu một ISA ảo như WebAssembly mà ISA thực tế không xác định (sau đó bạn sẽ có được nỗ lực tốt nhất). Ngày constexpr: nó được yêu cầu để constexpr cho giá trị có thể sử dụng trong việc xác định bố trí struct. Giá trị thời gian chạy rất hữu ích trong các trường hợp khác. –

Các vấn đề liên quan