Mã ví dụ của IBM, các hàm không đăng ký lại không hoạt động trong hệ thống của tôi


11

Tôi đã được học lại về lập trình. Trên trang web này của IBM (thực sự tốt). Tôi đã thành lập một mã, sao chép dưới đây. Đó là mã đầu tiên xuất hiện trên trang web.

Mã cố gắng hiển thị các vấn đề liên quan đến quyền truy cập chung vào biến trong sự phát triển phi tuyến tính của chương trình văn bản (tính không đồng bộ) bằng cách in hai giá trị liên tục thay đổi trong "bối cảnh nguy hiểm".

#include <signal.h>
#include <stdio.h>

struct two_int { int a, b; } data;

void signal_handler(int signum){
   printf ("%d, %d\n", data.a, data.b);
   alarm (1);
}

int main (void){
   static struct two_int zeros = { 0, 0 }, ones = { 1, 1 };

   signal (SIGALRM, signal_handler); 
   data = zeros;
   alarm (1);
   while (1){
       data = zeros;
       data = ones;
   }
}

Các vấn đề xuất hiện khi tôi cố chạy mã (hoặc tốt hơn, không xuất hiện). Tôi đã sử dụng gcc phiên bản 6.3.0 20170516 (Debian 6.3.0-18 + deb9u1) trong cấu hình mặc định. Đầu ra sai lầm không xảy ra. Tần suất nhận các giá trị cặp "sai" là 0!

Rốt cuộc chuyện gì đang xảy ra vậy? Tại sao không có vấn đề trong việc tái sử dụng các biến toàn cục tĩnh?


1
Đảm bảo rằng tất cả tối ưu hóa trình biên dịch bị vô hiệu hóa và thử lại
roaima

Tôi cho rằng ... nhưng tôi sẽ thay đổi tùy chọn nào? Tôi không có ý kiến. :-(
Daniel Bandeira

5
Điều này trông giống như một câu hỏi lập trình (stack stack). Nó dường như không được đặt ở đây. (Xin lỗi, tôi với có ít phụ trang web, nó được cắt như vậy lên Nhưng đó là cách mà nó được..)
ctrl-alt-delor

1
Mã đăng ký lại đơn giản nhất là bất biến.
ctrl-alt-delor

Lúc đầu, tôi nghĩ rằng câu hỏi sẽ liên quan đến môi trường gcc và Linux. Phát triển, ví dụ, lập lịch cho HĐH (thực hiện thêm văn bản chương trình tín hiệu sau khi ngắt trước khi gọi thủ tục xử lý), ví dụ.
Daniel Bandeira

Câu trả lời:


12

Đó không thực sự là quyền lợi ; bạn không chạy một chức năng hai lần trong cùng một luồng (hoặc trong các luồng khác nhau). Bạn có thể có được điều đó thông qua đệ quy hoặc chuyển địa chỉ của hàm hiện tại dưới dạng con trỏ hàm gọi lại arg sang hàm khác. (Và nó sẽ không an toàn vì nó sẽ đồng bộ).

Đây chỉ là cuộc đua dữ liệu vanilla đơn giản UB (Hành vi không xác định) giữa trình xử lý tín hiệu và luồng chính: chỉ sig_atomic_tđược đảm bảo an toàn cho việc này . Các trường hợp khác có thể xảy ra để hoạt động, như trong trường hợp của bạn khi một đối tượng 8 byte có thể được tải hoặc lưu trữ với một lệnh trên x86-64 và trình biên dịch tình cờ chọn asm đó. (Như câu trả lời của @ icarus cho thấy).

Xem lập trình MCU - Tối ưu hóa C ++ O2 trong khi lặp - trình xử lý ngắt trên vi điều khiển lõi đơn về cơ bản giống như trình xử lý tín hiệu trong một chương trình luồng đơn. Trong trường hợp đó, kết quả của UB là một tải đã được kéo ra khỏi một vòng lặp.

Trường hợp thử nghiệm xé rách của bạn thực sự xảy ra do cuộc đua dữ liệu UB có thể đã được phát triển / thử nghiệm ở chế độ 32 bit hoặc với trình biên dịch giả cũ hơn đã tải riêng các thành viên cấu trúc.

Trong trường hợp của bạn, trình biên dịch có thể tối ưu hóa các cửa hàng từ vòng lặp vô hạn vì không có chương trình không có UB nào có thể quan sát chúng. datalà không _Atomichoặcvolatile , và không có tác dụng phụ nào khác trong vòng lặp. Vì vậy, không có cách nào bất kỳ độc giả có thể đồng bộ hóa với nhà văn này. Thực tế điều này xảy ra nếu bạn biên dịch với kích hoạt tối ưu hóa ( Godbolt hiển thị một vòng lặp trống ở dưới cùng của chính). Tôi cũng đã thay đổi struct thành hai long longvà gcc sử dụng một movdqalưu trữ 16 byte duy nhất trước vòng lặp. (Điều này không đảm bảo nguyên tử, nhưng nó là trong thực tế trên hầu hết các CPU, giả sử nó thẳng hàng, hoặc trên Intel chỉ đơn thuần là không vượt qua một ranh giới bộ nhớ cache-line. Tại sao là số nguyên phân trên một cách tự nhiên liên kết biến nguyên tử trên x86? )

Vì vậy, biên dịch với tối ưu hóa được kích hoạt cũng sẽ phá vỡ thử nghiệm của bạn và hiển thị cho bạn cùng một giá trị mỗi lần. C không phải là ngôn ngữ lắp ráp di động.

volatile struct two_intcũng sẽ buộc trình biên dịch không tối ưu hóa chúng đi, nhưng sẽ không buộc nó tải / lưu trữ toàn bộ cấu trúc nguyên tử. (Tuy nhiên, điều đó cũng không ngăn được họ làm như vậy.) Lưu ý rằng điều volatileđó không tránh được cuộc đua dữ liệu UB, nhưng trên thực tế, nó đủ để giao tiếp giữa các luồng và là cách mọi người chế tạo các nguyên tử cuộn bằng tay (cùng với asm nội tuyến) trước C11 / C ++ 11, đối với kiến ​​trúc CPU bình thường. Họ đang nhớ cache-mạch lạc như vậy volatiletrong thực tế hầu như tương tự như _Atomicvớimemory_order_relaxed cho tinh khiết-load và tinh khiết-store, nếu được sử dụng với nhiều loại thu hẹp đủ để trình biên dịch sẽ sử dụng một chỉ dẫn duy nhất, do đó bạn không nhận được xé. Và dĩ nhiênvolatilekhông có bất kỳ sự đảm bảo nào từ tiêu chuẩn ISO C so với mã viết biên dịch cho cùng một asm sử dụng _Atomicvà mo_relaxed.


Nếu bạn có một chức năng đã thực hiện global_var++;trên inthoặc long longbạn chạy từ chính không đồng bộ từ bộ xử lý tín hiệu, đó sẽ là một cách để sử dụng quyền truy cập lại để tạo UB chạy dữ liệu.

Tùy thuộc vào cách nó được biên dịch (đến đích bộ nhớ inc hoặc thêm hoặc tách riêng tải / inc / store), nó sẽ là nguyên tử hoặc không liên quan đến trình xử lý tín hiệu trong cùng một luồng. Xem num num ++ có thể là nguyên tử cho 'int num' không? để biết thêm về tính nguyên tử trên x86 và trong C ++. ( Thuộc tính stdatomic.h_Atomicthuộc tính của C11 cung cấp chức năng tương đương với std::atomic<T>mẫu của C ++ 11 )

Một ngắt hoặc ngoại lệ khác không thể xảy ra ở giữa một lệnh, vì vậy, phần bổ sung đích bộ nhớ là wrt nguyên tử. bối cảnh chuyển đổi trên CPU lõi đơn. Chỉ có một trình ghi DMA (bộ nhớ đệm) có thể "bước lên" một mức tăng từ add [mem], 1không có locktiền tố trên CPU lõi đơn. Không có lõi nào khác mà luồng khác có thể chạy.

Vì vậy, nó tương tự như trường hợp tín hiệu: bộ xử lý tín hiệu chạy thay vì thực thi thông thường của luồng xử lý tín hiệu, do đó, nó không thể được xử lý ở giữa một lệnh.


2
Tôi đã buộc phải chấp nhận câu trả lời tốt nhất của bạn, mặc dù câu trả lời của Icaru là đủ với tôi. Các khái niệm rõ ràng mà bạn nói với chúng tôi cung cấp cho tôi một nhóm các chủ đề để nghiên cứu tất cả các ngày này (và hơn nữa). Trên thực tế, tôi đã gặp khó khăn trong những gì bạn viết trong hai đoạn đầu tiên. Cảm ơn bạn! Nếu bạn công khai các bài viết trên internet về máy tính và lập trình, hãy cho chúng tôi liên kết!
Daniel Bandeira

17

Nhìn vào trình thám hiểm trình biên dịch godbolt (sau khi thêm vào phần còn thiếu #include <unistd.h>), người ta thấy rằng đối với hầu hết mọi trình biên dịch x86_64, mã được tạo sử dụng các bước di chuyển QWORD để tải oneszerostrong một lệnh.

        mov     rax, QWORD PTR main::ones[rip]
        mov     QWORD PTR data[rip], rax

Trang web của IBM cho biết On most machines, it takes several instructions to store a new value in data, and the value is stored one word at a time.điều này có thể đúng với cpus điển hình trong năm 2005 nhưng hiện tại mã không đúng. Thay đổi cấu trúc để có hai độ dài thay vì hai số nguyên sẽ cho thấy vấn đề.

Trước đây tôi đã viết rằng đây là "nguyên tử" lười biếng. Chương trình chỉ chạy trên một cpu duy nhất. Mỗi hướng dẫn sẽ hoàn thành theo quan điểm của cpu này (giả sử không có gì khác làm thay đổi bộ nhớ như dma).

Vì vậy, ở Ccấp độ không xác định rằng trình biên dịch sẽ chọn một lệnh duy nhất để viết cấu trúc và do đó, tham nhũng được đề cập trong bài báo của IBM có thể xảy ra. Trình biên dịch hiện đại nhắm mục tiêu cpus hiện tại sử dụng một hướng dẫn duy nhất. Một hướng dẫn duy nhất là đủ tốt để tránh tham nhũng cho một chương trình luồng đơn.


3
Hãy thử thay đổi kiểu dữ liệu từ intsang long longvà biên dịch thành 32 bit. Bài học là bạn không bao giờ biết nếu / khi nào nó sẽ phá vỡ.
ctrl-alt-delor

2
điều đó có nghĩa là, trong máy của tôi, việc gán hai giá trị này là một hoạt động nguyên tử? (xem xét việc biên dịch cho kiến ​​trúc x86_64)
Daniel Bandeira

1
long longvẫn biên dịch thành một lệnh cho x86-64: 16 byte movdqa. Trừ khi bạn vô hiệu hóa tối ưu hóa, như trong liên kết Godbolt của bạn. (Mặc định của GCC là -O0chế độ gỡ lỗi, chứa đầy tiếng ồn lưu trữ / tải lại và thường không thú vị để xem xét.)
Peter Cordes

Tôi đã thay đổi loại thành "dài dài" sau khi đọc tất cả các ý kiến. Kết quả thật thú vị: kết quả chờ đợi đã đạt được và, thiết lập một số bộ đếm, nó có thể cải thiện các quan niệm khác khi tốc độ của dữ liệu không khớp bị ảnh hưởng bởi phần còn lại của mã. Cảm ơn tất cả sự giúp đỡ!
Daniel Bandeira
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.