2012-01-25 36 views
17

Tôi đang chạy trên máy 32 bit và tôi có thể xác nhận rằng các giá trị dài có thể xé bằng đoạn mã sau đây truy cập rất nhanh.Mô phỏng xé đôi trong C#

 static void TestTearingLong() 
     { 
      System.Threading.Thread A = new System.Threading.Thread(ThreadA); 
      A.Start(); 

      System.Threading.Thread B = new System.Threading.Thread(ThreadB); 
      B.Start(); 
     } 

     static ulong s_x; 

     static void ThreadA() 
     { 
      int i = 0; 
      while (true) 
      { 
       s_x = (i & 1) == 0 ? 0x0L : 0xaaaabbbbccccddddL; 
       i++; 
      } 
     } 

     static void ThreadB() 
     { 
      while (true) 
      { 
       ulong x = s_x; 
       Debug.Assert(x == 0x0L || x == 0xaaaabbbbccccddddL); 
      } 
     } 

Nhưng khi tôi thử một thứ gì đó tương tự như tăng gấp đôi, tôi không thể bị xé rách. Có ai biết tại sao không? Theo như tôi có thể nói từ spec, chỉ gán cho một phao là nguyên tử. Việc giao cho một đôi nên có nguy cơ rách.

static double s_x; 

    static void TestTearingDouble() 
    { 
     System.Threading.Thread A = new System.Threading.Thread(ThreadA); 
     A.Start(); 

     System.Threading.Thread B = new System.Threading.Thread(ThreadB); 
     B.Start(); 
    } 

    static void ThreadA() 
    { 
     long i = 0; 

     while (true) 
     { 
      s_x = ((i & 1) == 0) ? 0.0 : double.MaxValue; 
      i++; 

      if (i % 10000000 == 0) 
      { 
       Console.Out.WriteLine("i = " + i); 
      } 
     } 
    } 

    static void ThreadB() 
    { 
     while (true) 
     { 
      double x = s_x; 

      System.Diagnostics.Debug.Assert(x == 0.0 || x == double.MaxValue); 
     } 
    } 
+4

Câu hỏi ngu ngốc - rách là gì? Các hoạt động – Oded

+0

trên int được đảm bảo là nguyên tử liên quan đến truy cập bởi nhiều luồng. Không phải như vậy với longs. Tearing là nhận được một kết hợp của hai giá trị tạm thời (xấu). Anh ấy tự hỏi tại sao giống nhau không được nhìn thấy trong đôi, kể từ khi tăng gấp đôi cũng không đảm bảo hoạt động nguyên tử. – hatchet

+13

@Oded: Trên máy 32 bit, chỉ có 32 bit được ghi tại một thời điểm. Nếu bạn đang viết một giá trị 64 bit trên một máy 32 bit, và ghi vào cùng một địa chỉ cùng một lúc trên hai luồng khác nhau, bạn thực sự có * bốn * ghi, chứ không phải * hai *, vì ghi được thực hiện 32 bit tại một thời gian. Do đó có thể cho các chủ đề để chạy đua, và khi khói xóa biến chứa 32 bit hàng đầu được viết bởi một sợi, và 32 bit dưới cùng được viết bởi một luồng khác. Vì vậy, bạn có thể viết 0xDEADBEEF00000000 trên một sợi và 0x00000000BAADF00D trên một sợi khác và kết thúc với 0x0000000000000000 trong bộ nhớ. –

Trả lời

10
static double s_x; 

Đó là khó khăn hơn nhiều để chứng minh tác dụng khi bạn sử dụng một đôi. CPU sử dụng các lệnh chuyên dụng để nạp và lưu trữ một FLD và FSTP tương ứng. Nó dễ dàng hơn nhiều với dài vì không có lệnh đơn nào tải/lưu trữ số nguyên 64 bit ở chế độ 32 bit. Để quan sát nó, bạn cần phải có địa chỉ của biến không chính xác để nó nằm giữa ranh giới dòng bộ nhớ cache cpu.

Điều đó sẽ không bao giờ xảy ra với khai báo bạn đã sử dụng, trình biên dịch JIT đảm bảo rằng đôi được căn chỉnh đúng cách, được lưu trữ tại địa chỉ là bội số của 8. Bạn có thể lưu nó trong một trường của một lớp, chỉ cấp phát GC căn chỉnh đến 4 ở chế độ 32 bit. Nhưng đó là một trò crap.

Cách tốt nhất để làm điều đó là cố tình sắp xếp sai đôi bằng cách sử dụng con trỏ.Đặt không an toàn ở phía trước của lớp chương trình và làm cho nó trông tương tự như sau:

static double* s_x; 

    static void Main(string[] args) { 
     var mem = Marshal.AllocCoTaskMem(100); 
     s_x = (double*)((long)(mem) + 28); 
     TestTearingDouble(); 
    } 
ThreadA: 
      *s_x = ((i & 1) == 0) ? 0.0 : double.MaxValue; 
ThreadB: 
      double x = *s_x; 

này vẫn sẽ không đảm bảo một không thẳng hàng tốt (hehe) vì không có cách nào để kiểm soát chính xác nơi AllocCoTaskMem() sẽ sắp xếp phân bổ tương đối so với đầu của dòng bộ nhớ cache cpu. Và nó phụ thuộc vào sự kết hợp bộ nhớ cache trong lõi cpu của bạn (tôi là một Core i5). Bạn sẽ phải tinker với bù đắp, tôi đã nhận được giá trị 28 bằng cách thử nghiệm. Giá trị phải chia hết cho 4 nhưng không phải bằng 8 để thực sự mô phỏng hành vi heap của GC. Tiếp tục thêm 8 vào giá trị cho đến khi bạn nhận được gấp đôi để sắp xếp dòng bộ nhớ cache và kích hoạt xác nhận.

Để làm cho nó ít nhân tạo, bạn sẽ phải viết một chương trình lưu trữ đôi trong lĩnh vực của một lớp và nhận được bộ thu rác để di chuyển nó xung quanh trong bộ nhớ để nó bị lệch. Rất khó để đưa ra một chương trình mẫu mà đảm bảo điều này xảy ra.

Cũng lưu ý cách chương trình của bạn có thể chứng minh sự cố được gọi là chia sẻ sai. Chú thích lời gọi phương thức Start() cho luồng B và lưu ý chuỗi A chạy nhanh hơn bao nhiêu. Bạn đang thấy chi phí của CPU giữ dòng bộ nhớ cache nhất quán giữa các lõi CPU. Chia sẻ được dự định ở đây vì các chuỗi truy cập cùng một biến. Chia sẻ sai thực sự xảy ra khi các chuỗi truy cập các biến khác nhau được lưu trữ trong cùng một dòng bộ nhớ cache. Đây là lý do tại sao liên kết vấn đề, bạn chỉ có thể quan sát các rách cho một đôi khi một phần của nó là trong một dòng bộ nhớ cache và một phần của nó là khác.

+0

Tôi không hiểu cách đường ranh giới bộ nhớ đệm có thể gây xé. Tôi nghĩ rằng điều này chỉ được gây ra bởi giá trị chiếm nhiều không gian hơn kích thước của một thanh ghi. Bạn có thể vui lòng giải thích thêm về điều này một chút không? – Tudor

+0

@Tudor - đó là một hiệu ứng hoàn toàn khác, không liên quan đến kích thước đăng ký. Tập trung vào đoạn cuối, lưu ý cách đồng bộ hóa bộ nhớ cache cpu có một dòng bộ nhớ cache là đơn vị cập nhật. Một đôi không thẳng hàng mà nằm giữa một dòng yêu cầu * hai * cập nhật, tương tự như cách một yêu cầu dài hai đăng ký viết. Mà mất đủ thời gian để cho phép mã chạy trên lõi khác để quan sát rách. –

11

Như âm thanh lạ, điều đó phụ thuộc vào CPU của bạn. Trong khi đôi là không được bảo đảm không xé, chúng sẽ không có trên nhiều bộ vi xử lý hiện tại. Hãy thử một Sempron AMD nếu bạn muốn rách trong tình huống này.

EDIT: Đã học được cách khó khăn một vài năm trước đây.

+0

Điều này có phù hợp với kích thước của thanh ghi dấu chấm động không? – leppie

+0

TBH Tôi không có ý tưởng nhỏ, không bao giờ nhìn vào nó. Một daemon của tôi (Pascal miễn phí của tất cả các ngôn ngữ) đã bắt đầu tạo ra kết quả vô lý trên một và chỉ một máy trong số nhiều (có thể là 100), tất cả được thiết lập từ cùng một hình ảnh. chủ đề chính và chuỗi thứ cấp được tạo bởi GTK. Không có khóa nguyên thủy trong FPK sau đó ... (expletive, expletive) –

+0

Vâng, tôi sẽ không nghi ngờ nó nếu phần mở rộng MMX hoặc SSE trên CPU đã có một cái gì đó để làm với điều này. – antiduh

0

Làm một số đào, tôi đã tìm thấy một số thú vị liên quan đến hoạt động đọc dấu chấm động trên x86 kiến ​​trúc:

Theo Wikipedia, x86 đơn vị dấu chấm động lưu trữ các giá trị dấu chấm động trong thanh ghi 80-bit:

[...] bộ vi xử lý x86 tiếp theo sau đó tích hợp chức năng x87 trên chip làm cho x87 hướng dẫn một phần không tách rời của bộ chỉ dẫn x86. Mỗi thanh ghi x87, được gọi là ST (0) đến ST (7), rộng 80 bit và lưu trữ số trong định dạng độ chính xác mở rộng gấp đôi chuẩn IEEE .

Cũng câu hỏi này khác SO có liên quan: Some floating point precision and numeric limits question

Điều này có thể giải thích tại sao, mặc dù đôi là 64-bit, họ đang hoạt động trên nguyên tử.

0

Đối với những gì giá trị của nó chủ đề này và mẫu mã có thể được tìm thấy ở đây.

http://msdn.microsoft.com/en-us/magazine/cc817398.aspx

+0

Bài viết đó chỉ nói về lâu dài, không phải gấp đôi. – Tudor

+0

Đồng ý. Trên thực tế, tôi nghĩ rằng mã mẫu mà tôi đã đăng trong câu hỏi là từ bài đăng đó (ngoại trừ các công cụ tăng gấp đôi). (Tôi đã có nó trong một dự án thử nghiệm và đã quên nó trong một thời gian). –