2013-05-23 43 views
12

Tôi đã cảm thấy rằng tải bộ nhớ không thể được nâng lên trên một tải thu được trong mô hình bộ nhớ C++ 11. Tuy nhiên, nhìn vào mã mà gcc 4.8 tạo ra chỉ có vẻ đúng với các tải nguyên tử khác, không phải tất cả bộ nhớ. Nếu đó là sự thật và mua tải không đồng bộ hóa tất cả bộ nhớ (chỉ std::atomics) sau đó tôi không chắc chắn làm thế nào nó sẽ có thể thực hiện mutexes mục đích chung về std :: nguyên tử.Nâng tải trọng không nguyên tử lên thông qua việc mua các tải nguyên tử

Các mã sau đây:

extern std::atomic<unsigned> seq; 
extern std::atomic<int> data; 

int reader() { 
    int data_copy; 
    unsigned seq0; 
    unsigned seq1; 
    do { 
     seq0 = seq.load(std::memory_order_acquire); 
     data_copy = data.load(std::memory_order_relaxed); 
     std::atomic_thread_fence(std::memory_order_acquire); 
     seq1 = seq.load(std::memory_order_relaxed); 
    } while (seq0 != seq1); 
    return data_copy; 
} 

Tạo:

_Z6readerv: 
.L3: 
    mov ecx, DWORD PTR seq[rip] 
    mov eax, DWORD PTR data[rip] 
    mov edx, DWORD PTR seq[rip] 
    cmp ecx, edx 
    jne .L3 
    rep ret 

nào có vẻ đúng với tôi.

Tuy nhiên thay đổi dữ liệu là một int hơn std::atomic:

extern std::atomic<unsigned> seq; 
extern int data; 

int reader() { 
    int data_copy; 
    unsigned seq0; 
    unsigned seq1; 
    do { 
     seq0 = seq.load(std::memory_order_acquire); 
     data_copy = data; 
     std::atomic_thread_fence(std::memory_order_acquire); 
     seq1 = seq.load(std::memory_order_relaxed); 
    } while (seq0 != seq1); 
    return data_copy; 
} 

Tạo này:

_Z6readerv: 
    mov eax, DWORD PTR data[rip] 
.L3: 
    mov ecx, DWORD PTR seq[rip] 
    mov edx, DWORD PTR seq[rip] 
    cmp ecx, edx 
    jne .L3 
    rep ret 

Vì vậy, những gì đang xảy ra?

+0

Nếu bạn viết lại các op nguyên tử để 'tải (rel); hàng rào (acq); 'trong phiên bản thứ hai, hiện đầu ra asm của nó? – yohjp

+0

@yoyjp Bạn đang đề cập đến việc tải 'seq0'? Nếu vậy thì không, nó không ảnh hưởng đến mã được tạo ra ở tất cả. – jleahy

+0

Không, tôi đã đề cập 'seq1'. Một "thu thập hàng rào" mà đã có được ngữ nghĩa là bao gồm 'seq1.load (thư giãn) -> hàng rào (có được)' lệnh ops, không phải 'hàng rào (có được) -> seq1.load (thư giãn)' trong bộ nhớ C++ 11 mô hình. "Hàng rào" của C++ ** chỉ ảnh hưởng _happens-trước mối quan hệ_ giữa các hoạt động nguyên tử hoặc/và hàng rào, nó có ** không ** tác động trực tiếp lên các vars phi nguyên tử. Trong thời điểm này, "hàng rào" của C++ khá khác với hướng dẫn rào cản bộ nhớ của trình biên dịch/bộ biên dịch (như mfence của x86). – yohjp

Trả lời

4

Tại sao một tải được kéo lên trên một Acquire

tôi đã đăng này trên gcc bugzilla và họ đã xác nhận nó như là một lỗi.

bí danh thiết lập là -1 (ALIAS_SET_MEMORY_BARRIER) MEM có nghĩa vụ phải ngăn chặn điều này, nhưng PRE không biết về bất động sản đặc biệt này (nó sẽ "giết" tất cả các refs qua nó).

Dường như gcc wiki có một trang đẹp về việc này.

Nói chung, phát hành là một rào cản đối với chìm mã, có được là một rào cản đối với cẩu mã.

Tại sao mã này vẫn còn bị hỏng

Theo this paper mã của tôi là vẫn không chính xác, bởi vì nó giới thiệu một cuộc chạy đua dữ liệu. Mặc dù gcc được vá tạo mã đúng nhưng vẫn không thích hợp để truy cập data mà không cần gói nó trong std::atomic.Lý do là các cuộc đua dữ liệu là hành vi không xác định, ngay cả khi tính toán kết quả từ chúng được loại bỏ.

Một ví dụ biếu không của AdamH.Peterson:

int foo(unsigned x) { 
    if (x < 10) { 
     /* some calculations that spill all the 
      registers so x has to be reloaded below */ 
     switch (x) { 
     case 0: 
      return 5; 
     case 1: 
      return 10; 
     // ... 
     case 9: 
      return 43; 
     } 
    } 
    return 0; 
} 

Dưới đây là một trình biên dịch có thể tối ưu hóa việc chuyển đổi vào một bảng nhảy, và nhờ vào câu lệnh if ở trên sẽ có thể tránh một tấm séc phạm vi. Tuy nhiên, nếu cuộc đua dữ liệu không phải là hành vi không xác định thì cần phải kiểm tra phạm vi thứ hai.

+4

2 không tương thích. Mã của bạn có một cuộc đua dữ liệu và tiêu chuẩn C++ nói rõ ràng (1.10 21) rằng mã của bạn dựa vào hành vi không xác định. Mã IS không chính xác (hoặc ít nhất là thiếu đồng bộ hóa đúng để chứng minh quan điểm của bạn). Một lần nữa, các giấy hp làm cho điều này rõ ràng là tốt (tác giả là một trong những kiến ​​trúc sư của mô hình bộ nhớ C++ 11) 1,10 13 nói rằng gcc không được phép làm mã nâng. Nếu điều này xảy ra trên mã C++ hợp lệ, đó là lỗi. Toàn bộ vấn đề là * nếu * không có cuộc đua dữ liệu, mã được tạo sẽ chính xác (ít nhất tôi không hiểu tại sao không) – Guillaume

+4

@GuillaumeMorin: Tôi bị cám dỗ đồng ý. Chuỗi phát hành được hình thành bởi cửa hàng và tải chỉ là một thành phần trong chuỗi xảy ra trước đó. Mã sẽ đúng với chuỗi thứ hai được đăng nếu chuỗi đầu tiên đã nói điều gì đó như 'if (seq_copy == 2) {data_copy = data; } '. Trong trường hợp đó 'dữ liệu = 2' * xảy ra trước * cửa hàng nguyên tử, mà * đồng bộ hóa với * tải nguyên tử, mà * xảy ra trước * dữ liệu' data_copy ='. Với mã như được đăng, truy cập vào 'dữ liệu' gây ra một cuộc đua. (Và mã đã sửa cũng tạo ra đầu ra chính xác cho tôi). –

+0

@GuillaumeMorin Tôi nhận ra bạn đã đúng, tôi đã sửa đổi câu trả lời của mình để giúp bất kỳ ai khác nhìn thấy điều này. Đó là một sự xấu hổ các vùng nước bị lầy lội bởi thực tế là cũng có một lỗi trong gcc. – jleahy

0

Tôi vẫn còn mới về lý do về các hoạt động và các rào cản bộ nhớ không tuần tự nhất quán, nhưng có thể là việc tạo mã này là chính xác (hoặc không được phép). Trên khuôn mặt của nó, nó chắc chắn trông cá, nhưng tôi sẽ không ngạc nhiên nếu không có cách nào cho một chương trình phù hợp tiêu chuẩn để nói rằng tải từ dữ liệu đã được treo (có nghĩa là mã này là chính xác theo "như thể " qui định).

Chương trình đang đọc hai giá trị tiếp theo từ một nguyên tử, một trước khi tải và một sau khi tải và phát hành lại tải bất cứ khi nào chúng không khớp. Về nguyên tắc, không có lý do nào mà hai lần đọc nguyên tử là phải thấy các giá trị khác nhau từ nhau. Ngay cả khi một ghi nguyên tử vừa mới xảy ra, không có cách nào cho chuỗi này có thể phát hiện ra rằng nó không đọc lại giá trị cũ. Sau đó, chuỗi sẽ quay trở lại vòng lặp và cuối cùng đọc hai giá trị nhất quán từ nguyên tử, sau đó quay lại, nhưng sau đó, seq0seq1 sẽ bị hủy, chương trình không thể cho biết giá trị trong seq0 không tương ứng với giá trị đã đọc từ data. Về nguyên tắc, điều này cũng gợi ý với tôi rằng toàn bộ vòng lặp có thể đã được elided và chỉ tải từ data thực sự là cần thiết cho tính chính xác, nhưng không bỏ qua vòng lặp không nhất thiết phải là vấn đề chính xác.

Nếu reader() là để trả về một pair<int,unsigned> bao gồm seq0 (hoặc seq1) và vòng lặp kéo lên cùng được tạo ra, tôi nghĩ rằng nó có thể là mã không chính xác (nhưng một lần nữa tôi mới để không tuần tự-phù này lập luận hoạt động).

+0

Tôi không chắc chắn bạn đang ở đây. Nếu những gì bạn đang nói là sự thật thì sự kết hợp các rào cản này là không đủ để thực hiện một khóa seq, trái với những gì HP đã viết trong bài báo này: http://www.hpl.hp.com/techreports/ 2012/HPL-2012-68.pdf. Hơn nữa, bạn vẫn mong đợi trình biên dịch tạo ra cùng một mã cho cả hai đầu vào (và có lẽ chỉ cần bỏ qua toàn bộ vòng lặp, vì nó không có bất kỳ rào cản nào). – jleahy

+0

@jleahy, tôi nghĩ rằng giấy không hoàn toàn đúng. Đầu tiên, trong bài báo, tất cả các lần đọc và ghi chia sẻ được thực hiện trên 'nguyên tử' thực tế (trừ các ví dụ mà chúng chỉ ra là không chính xác), vì các biến thông thường phụ thuộc vào ràng buộc dữ liệu (không có truy cập xung đột) để tránh hành vi không xác định. Thứ hai, các vòng mà chúng thực hiện để đánh dấu các lần đọc và viết thực sự thực hiện một số phép tính phi lý trên các giá trị chuỗi đọc để xác minh tính nhất quán, không áp dụng trong ví dụ của bạn. IME, một trong những điểm này là đủ để hiển thị mã rõ ràng khác với trình tối ưu hóa. –

1

Tôi không nghĩ rằng atomic_thread_fence của bạn là chính xác. Mô hình bộ nhớ C++ 11 duy nhất hoạt động với mã của bạn sẽ là seq_cst. Nhưng điều này là rất tốn kém (bạn sẽ nhận được một hàng rào bộ nhớ đầy đủ) cho những gì bạn cần.

Mã gốc hoạt động và tôi nghĩ đây là sự cân bằng hiệu suất tốt nhất.

EDIT dựa trên thông tin cập nhật của bạn:

Nếu bạn đang tìm kiếm lý do chính thức tại sao đoạn code với một int thường xuyên không hoạt động theo cách bạn muốn, tôi tin rằng các giấy rất bạn trích dẫn (http://www.hpl.hp.com/techreports/2012/HPL-2012-68.pdf) đưa ra câu trả lời. Nhìn vào cuối phần 2. Mã của bạn có cùng một vấn đề như mã trong Hình 1. Nó có các cuộc đua dữ liệu. Nhiều luồng có thể thực hiện các thao tác trên cùng một bộ nhớ trên int thông thường cùng một lúc. Nó bị cấm bởi mô hình bộ nhớ C++ 11, mã này là chính thức không hợp lệ mã C++.

gcc hy vọng mã không có chủng tộc dữ liệu, tức là mã C++ hợp lệ. Vì không có chủng tộc và mã tải int vô điều kiện, một tải có thể được phát ra bất cứ nơi nào trong cơ thể của hàm. Vì vậy, gcc là thông minh và nó chỉ phát ra nó một lần vì nó không phải là dễ bay hơi. Câu lệnh có điều kiện thường đi đôi với rào cản có được đóng một vai trò quan trọng trong trình biên dịch sẽ làm gì.

Trong tiếng lóng chính thức của tiêu chuẩn, tải nguyên tử và tải trọng int thông thường không được kết nối. Việc giới thiệu ví dụ về một điều kiện sẽ tạo ra một điểm chuỗi và sẽ buộc trình biên dịch đánh giá int thông thường sau điểm chuỗi (http://msdn.microsoft.com/en-us/library/d45c7a5d.aspx). Sau đó, mô hình bộ nhớ C++ sẽ thực hiện phần còn lại (nghĩa là đảm bảo khả năng hiển thị của CPU thực hiện các hướng dẫn)

Vì vậy, các câu lệnh của bạn đều không đúng. Bạn chắc chắn có thể xây dựng một khóa với C++ 11, không chỉ với một cuộc đua dữ liệu :-) Thông thường một khóa sẽ liên quan đến việc chờ đợi trước khi đọc (đó rõ ràng là những gì bạn đang cố gắng tránh ở đây), do đó bạn không có loại các vấn đề.

Lưu ý rằng khung ban đầu của bạn bị lỗi vì bạn không muốn chỉ kiểm tra seq0! = Seq1 (bạn có thể đang ở giữa bản cập nhật). Giấy seqlock có điều kiện chính xác.

+0

Không có câu trả lời nào trong số này giải thích tại sao các tải thư giãn và phi nguyên tử được xử lý khác nhau đối với thứ tự bộ nhớ. – jleahy

+0

Họ không. Những gì bạn nhận được từ std :: atomic là "volatileness" của kiểu cơ bản. Bạn sẽ nhận được cùng một hành vi trong trường hợp thứ 2 của bạn nếu bạn thay đổi 'int data' thành' dữ liệu int dễ bay hơi'. Nhưng đây là một mẹo. Điều gì thực sự trong tinh thần của C++ 11 là sử dụng một int nhưng với mô hình bộ nhớ thích hợp (seq const, rất tốn kém vì vậy tôi không khuyên bạn nên nó) hoặc làm cho dữ liệu một nguyên tử với tải thoải mái. – Guillaume

+0

Tôi khá chắc chắn bạn không cần seq const, số tiền đó đến một mfence trên x86 và mã này hoạt động tốt bằng cách sử dụng asm volatile và không có mfence. (Tôi đã sử dụng hàng tỷ lần). Bài báo tôi đã đề cập trong bình luận khác của tôi (hpl.hp.com/techreports/2012/HPL-2012-68.pdf) cũng nói rằng đây là một hạn chế đầy đủ. – jleahy

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