C11 Nguyên tử mua / phát hành và x86_64 thiếu tải / lưu trữ kết hợp?


10

Tôi đang vật lộn với Mục 5.1.2.4 của Tiêu chuẩn C11, đặc biệt là ngữ nghĩa của Phát hành / Mua lại. Tôi lưu ý rằng https://preshing.com/20120913/acquire-and-release-semantics/ (trong số những người khác) nói rằng:

... Ngữ nghĩa phát hành ngăn chặn sắp xếp lại bộ nhớ của bản phát hành ghi với bất kỳ thao tác đọc hoặc ghi nào trước nó theo thứ tự chương trình.

Vì vậy, cho những điều sau đây:

typedef struct test_struct
{
  _Atomic(bool) ready ;
  int  v1 ;
  int  v2 ;
} test_struct_t ;

extern void
test_init(test_struct_t* ts, int v1, int v2)
{
  ts->v1 = v1 ;
  ts->v2 = v2 ;
  atomic_store_explicit(&ts->ready, false, memory_order_release) ;
}

extern int
test_thread_1(test_struct_t* ts, int v2)
{
  int v1 ;
  while (atomic_load_explicit(&ts->ready, memory_order_acquire)) ;
  ts->v2 = v2 ;       // expect read to happen before store/release 
  v1     = ts->v1 ;   // expect write to happen before store/release 
  atomic_store_explicit(&ts->ready, true, memory_order_release) ;
  return v1 ;
}

extern int
test_thread_2(test_struct_t* ts, int v1)
{
  int v2 ;
  while (!atomic_load_explicit(&ts->ready, memory_order_acquire)) ;
  ts->v1 = v1 ;
  v2     = ts->v2 ;   // expect write to happen after store/release in thread "1"
  atomic_store_explicit(&ts->ready, false, memory_order_release) ;
  return v2 ;
}

nơi những cái đó được thực thi:

>   in the "main" thread:  test_struct_t ts ;
>                          test_init(&ts, 1, 2) ;
>                          start thread "2" which does: r2 = test_thread_2(&ts, 3) ;
>                          start thread "1" which does: r1 = test_thread_1(&ts, 4) ;

Do đó, tôi mong muốn chủ đề "1" có r1 == 1 và chủ đề "2" có r2 = 4.

Tôi mong đợi điều đó bởi vì (theo ký sinh 16 và 18 của giáo phái 5.1.2.4):

  • tất cả các lần đọc và ghi (không phải nguyên tử) là "được giải trình tự trước" và do đó "xảy ra trước" việc ghi / giải phóng nguyên tử trong luồng "1",
  • trong đó "inter-thread-xảy ra-trước" nguyên tử đọc / thu nhận trong luồng "2" (khi nó đọc 'true'),
  • lần lượt là "tuần tự trước" và do đó "xảy ra trước" (không phải nguyên tử) đọc và ghi (trong luồng "2").

Tuy nhiên, hoàn toàn có thể là tôi đã không hiểu được tiêu chuẩn.

Tôi quan sát rằng mã được tạo cho x86_64 bao gồm:

test_thread_1:
  movzbl (%rdi),%eax      -- atomic_load_explicit(&ts->ready, memory_order_acquire)
  test   $0x1,%al
  jne    <test_thread_1>  -- while is true
  mov    %esi,0x8(%rdi)   -- (W1) ts->v2 = v2
  mov    0x4(%rdi),%eax   -- (R1) v1     = ts->v1
  movb   $0x1,(%rdi)      -- (X1) atomic_store_explicit(&ts->ready, true, memory_order_release)
  retq   

test_thread_2:
  movzbl (%rdi),%eax      -- atomic_load_explicit(&ts->ready, memory_order_acquire)
  test   $0x1,%al
  je     <test_thread_2>  -- while is false
  mov    %esi,0x4(%rdi)   -- (W2) ts->v1 = v1
  mov    0x8(%rdi),%eax   -- (R2) v2     = ts->v2   
  movb   $0x0,(%rdi)      -- (X2) atomic_store_explicit(&ts->ready, false, memory_order_release)
  retq   

với điều kiện là R1 và X1 xảy ra theo thứ tự đó, điều này mang lại kết quả mà tôi mong đợi.

Nhưng sự hiểu biết của tôi về x86_64 là việc đọc xảy ra theo thứ tự với các lần đọc và ghi khác xảy ra theo thứ tự với các lần ghi khác, nhưng đọc và viết có thể không xảy ra theo thứ tự với nhau. Điều đó ngụ ý rằng X1 có thể xảy ra trước R1 và ngay cả đối với X1, X2, W2, R1 cũng xảy ra theo thứ tự đó - tôi tin. [Điều này dường như rất khó xảy ra, nhưng nếu R1 bị xử lý bởi một số vấn đề về bộ đệm?]

Xin vui lòng: những gì tôi không hiểu?

Tôi lưu ý rằng nếu tôi thay đổi tải / cửa hàng ts->readythành memory_order_seq_cst, mã được tạo cho các cửa hàng là:

  xchg   %cl,(%rdi)

phù hợp với hiểu biết của tôi về x86_64 và sẽ cho kết quả mà tôi mong đợi.


5
Trên x86, tất cả các cửa hàng thông thường (không phải là tạm thời) đều có ngữ nghĩa phát hành. Hướng dẫn dành cho nhà phát triển phần mềm Intel® 64 và IA-32 Architectures Tập 3 (3A, 3B, 3C & 3D): Hướng dẫn lập trình hệ thống , 8.2.3.3 Stores Are Not Reordered With Earlier Loads. Vì vậy, trình biên dịch của bạn đang dịch chính xác mã của bạn (thật đáng ngạc nhiên), sao cho mã của bạn hoàn toàn tuần tự và không có gì thú vị xảy ra đồng thời.
EOF

Cảm ơn bạn ! (Tôi đang lặng lẽ đi bonkers.) FWIW Tôi khuyên bạn nên liên kết - đặc biệt là phần 3, "Mô hình lập trình viên". Nhưng để tránh sai lầm mà tôi mắc phải, hãy lưu ý rằng trong "3.1 Máy trừu tượng" có "luồng phần cứng", mỗi luồng là "một thứ tự duy nhất luồng luồng thực hiện " (tôi nhấn mạnh thêm). Bây giờ tôi có thể quay lại để cố gắng hiểu Tiêu chuẩn C11 ... với sự bất đồng về nhận thức ít hơn :-)
Chris Hall

Câu trả lời:


1

Mô hình bộ nhớ của x86 về cơ bản là tính nhất quán tuần tự cộng với bộ đệm lưu trữ (với chuyển tiếp cửa hàng). Vì vậy, mỗi cửa hàng là một cửa hàng phát hành 1 . Đây là lý do tại sao chỉ các cửa hàng seq-cst cần bất kỳ hướng dẫn đặc biệt. ( C / C ++ 11 ánh xạ nguyên tử tới asm ). Ngoài ra, https://stackoverflow.com/tags/x86/info có một số liên kết đến tài liệu x86, bao gồm mô tả chính thức về mô hình bộ nhớ x86-TSO (về cơ bản không thể đọc được đối với hầu hết mọi người; yêu cầu lội qua rất nhiều định nghĩa).

Vì bạn đã đọc loạt bài viết xuất sắc của Jeff Preshing, tôi sẽ chỉ cho bạn một bài viết khác chi tiết hơn: https://preshing.com/20120930/weak-vs-strong-memory-models/

Sắp xếp lại thứ tự duy nhất được phép trên x86 là StoreLoad, không phải LoadStore , nếu chúng ta đang nói về các điều khoản đó. (Chuyển tiếp cửa hàng có thể thực hiện thêm các công cụ thú vị nếu tải chỉ chồng lên một phần cửa hàng; Hướng dẫn tải vô hình toàn cầu , mặc dù bạn sẽ không bao giờ nhận được mã đó trong mã do trình biên dịch tạo stdatomic.)

@EOF nhận xét với trích dẫn đúng từ hướng dẫn của Intel:

Hướng dẫn sử dụng dành cho nhà phát triển phần mềm Intel® 64 và IA-32 Architectures Tập 3 (3A, 3B, 3C & 3D): Hướng dẫn lập trình hệ thống, 8.2.3.3 Cửa hàng không được sắp xếp lại với tải trước đó.


Chú thích 1: bỏ qua các cửa hàng NT được đặt hàng yếu; đây là lý do tại sao bạn bình thường sfencesau khi làm cửa hàng NT. Việc triển khai C11 / C ++ 11 cho rằng bạn không sử dụng các cửa hàng NT. Nếu là bạn, hãy sử dụng _mm_sfencetrước một hoạt động phát hành để đảm bảo nó tôn trọng các cửa hàng NT của bạn. (Nói chung không sử dụng _mm_mfence/ _mm_sfencetrong các trường hợp khác ; thông thường bạn chỉ cần chặn sắp xếp lại thời gian biên dịch. Hoặc tất nhiên chỉ sử dụng stdatomic.)


Tôi tìm thấy x86-TSO: Mô hình lập trình viên nghiêm ngặt và có thể sử dụng được cho Bộ đa xử lý x86 dễ đọc hơn Mô tả chính thức (có liên quan) mà bạn đã tham chiếu. Nhưng tham vọng thực sự của tôi là hiểu đầy đủ các phần 5.1.2.4 và 7.17.3 của Tiêu chuẩn C11 / C18. Cụ thể, tôi nghĩ rằng tôi nhận được Bản phát hành / Mua lại / Mua lại + Phát hành, nhưng memory_order_seq_cst được định nghĩa riêng và tôi đang đấu tranh để xem tất cả chúng khớp với nhau như thế nào :-(
Chris Hall

@ChrisHall: Tôi thấy nó giúp nhận ra chính xác mức độ yếu của acq / rel và bạn cần xem xét các máy như POWER có thể sắp xếp lại IRIW. (điều mà seq-cst cấm nhưng acq / rel không). Hai nguyên tử sẽ ghi vào các vị trí khác nhau trong các luồng khác nhau sẽ luôn được nhìn thấy theo cùng một thứ tự bởi các luồng khác? . Ngoài ra Làm thế nào để đạt được rào cản StoreLoad trong C ++ 11? có một số cuộc thảo luận về việc ít tiêu chuẩn chính thức đảm bảo về việc đặt hàng bên ngoài các trường hợp đồng bộ hóa - với hoặc mọi thứ - seq-cst.
Peter Cordes

@ChrisHall: Điều chính seq-cst làm là chặn sắp xếp lại StoreLoad. (Trên x86, đó là điều duy nhất nó vượt ra ngoài acq / rel). preshing.com/20120515/memory-reordering-caught-in-the-act sử dụng asm, nhưng nó tương đương với seq-cst so với acq / rel
Peter Cordes
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.