2012-02-27 41 views
11

Xem xét các mã đặc sau:Tại sao __sync_add_and_fetch hoạt động với biến 64 bit trên hệ thống 32 bit?

/* Compile: gcc -pthread -m32 -ansi x.c */ 
#include <stdio.h> 
#include <inttypes.h> 
#include <pthread.h> 

static volatile uint64_t v = 0; 

void *func (void *x) { 
    __sync_add_and_fetch (&v, 1); 
    return x; 
} 

int main (void) { 
    pthread_t t; 
    pthread_create (&t, NULL, func, NULL); 
    pthread_join (t, NULL); 
    printf ("v = %"PRIu64"\n", v); 
    return 0; 
} 

Tôi có một biến uint64_t mà tôi muốn tăng nguyên tử, bởi vì các biến là một bộ đếm trong một chương trình đa luồng. Để đạt được nguyên tử, tôi sử dụng số atomic builtins của GCC.

Nếu tôi biên dịch cho một hệ thống amd64 (-m64) thì mã bộ mã hóa được tạo ra dễ hiểu. Bằng cách sử dụng lock addq, bộ xử lý đảm bảo số gia tăng là nguyên tử.

400660:  f0 48 83 05 d7 09 20 lock addq $0x1,0x2009d7(%rip) 

Nhưng mã C cùng sản xuất một mã rất phức tạp ASM trên một hệ thống ia32 (-m32):

804855a:  a1 28 a0 04 08   mov 0x804a028,%eax 
804855f:  8b 15 2c a0 04 08  mov 0x804a02c,%edx 
8048565:  89 c1     mov %eax,%ecx 
8048567:  89 d3     mov %edx,%ebx 
8048569:  83 c1 01    add $0x1,%ecx 
804856c:  83 d3 00    adc $0x0,%ebx 
804856f:  89 ce     mov %ecx,%esi 
8048571:  89 d9     mov %ebx,%ecx 
8048573:  89 f3     mov %esi,%ebx 
8048575:  f0 0f c7 0d 28 a0 04 lock cmpxchg8b 0x804a028 
804857c:  08 
804857d:  75 e6     jne 8048565 <func+0x15> 

Dưới đây là những gì tôi không hiểu:

  • lock cmpxchg8bkhông đảm bảo rằng biến đã thay đổi chỉ được ghi nếu giá trị kỳ vọng vẫn nằm trong địa chỉ đích. Việc so sánh và trao đổi được đảm bảo xảy ra một cách nguyên tử.
  • Nhưng điều gì đảm bảo rằng việc đọc biến trong 0x804855a và 0x804855f là nguyên tử?

Có lẽ nó không quan trọng nếu có một "đọc bẩn", nhưng ai đó có thể xin phác thảo một đoạn ngắn bằng chứng rằng không có vấn đề?

Hơn nữa: Tại sao mã được tạo lại chuyển về 0x8048565 chứ không phải 0x804855a? Tôi tích cực rằng điều này chỉ đúng nếu các nhà văn khác, cũng chỉ tăng biến. Đây có phải là yêu cầu liên quan đến chức năng __sync_add_and_fetch không?

Trả lời

16

Các đọc là đảm bảo được nguyên tử do nó được đặt đúng cách (và nó phù hợp trên cùng một dòng bộ nhớ cache) và vì Intel đã spec cách này, vui lòng xem Kiến trúc Intel tay Vol 1, 4.4.1:

Toán hạng từ hoặc từ kép vượt qua ranh giới 4 byte hoặc toán hạng quadword vượt qua ranh giới 8 byte được coi là không được ký hiệu và yêu cầu hai chu kỳ bus bộ nhớ riêng biệt để truy cập.

Vol 3A 8.1.1:

Bộ xử lý Pentium (và bộ vi xử lý mới hơn từ) đảm bảo rằng hoạt động bộ nhớ bổ sung sau đây sẽ luôn luôn được thực hiện nguyên tử:

• Đọc hoặc viết một quadword xếp trên một 64-bit ranh giới

• 16-bit truy cập đến các địa điểm bộ nhớ uncached phù hợp trong một bus dữ liệu 32-bit

Các bộ xử lý gia đình P6 (và mới hơn vi xử lý từ) đảm bảo rằng thêm các hoạt động bộ nhớ sau sẽ luôn luôn được thực hiện nguyên tử:

• Unaligned 16-, 32-, và 64-bit truy cập vào bộ nhớ cache phù hợp trong một dòng bộ nhớ cache

Vì vậy, bằng cách căn chỉnh, nó có thể được đọc trong 1 chu kỳ, và nó phù hợp với một dòng bộ nhớ cache làm cho nguyên tử đọc.

Mã này nhảy trở lại 0x8048565 vì các con trỏ đã được nạp, không có cần phải tải chúng một lần nữa, như CMPXCHG8B sẽ thiết lập EAX:EDX với giá trị trong đích nếu nó không thành công:

CMPXCHG8B Mô tả cho Hướng dẫn sử dụng Intel ISA Vol. 2A:

So sánh EDX: EAX với m64. Nếu bằng nhau, hãy đặt ZF và tải ECX: EBX thành m64. Khác, xóa ZF và tải m64 vào EDX: EAX.

Do đó, mã chỉ cần tăng giá trị mới được trả về và thử lại. Nếu chúng ta này của nó trong mã C nó trở nên dễ dàng hơn:

value = dest; 
While(!CAS8B(&dest,value,value + 1)) 
{ 
    value = dest; 
} 
3

Việc đọc của biến trong 0x804855a và 0x804855f không cần phải là nguyên tử. Sử dụng các hướng dẫn so sánh-và-swap để tăng vẻ như thế này trong giả:

oldValue = *dest; 
do { 
    newValue = oldValue+1; 
} while (!compare_and_swap(dest, &oldValue, newValue)); 

Kể từ khi so sánh-và-swap kiểm tra rằng *dest == oldValue trước khi trao đổi, nó sẽ hoạt động như một bảo vệ - do đó nếu giá trị trong oldValue không chính xác, vòng lặp sẽ được thử lại, do đó, không có vấn đề gì nếu đọc không nguyên tử dẫn đến giá trị không chính xác.

Câu hỏi thứ hai của bạn là lý do tại sao dòng oldValue = *dest không nằm trong vòng lặp. Điều này là do hàm compare_and_swap sẽ luôn thay thế giá trị oldValue với giá trị thực tế là *dest. Vì vậy, về cơ bản nó sẽ thực hiện dòng oldValue = *dest cho bạn, và không có điểm nào trong việc thực hiện lại nó. Trong trường hợp của lệnh cmpxchg8b, nó sẽ đặt nội dung của toán hạng bộ nhớ trong edx:eax khi so sánh không thành công.

Các giả cho compare_and_swap là:

bool compare_and_swap (int *dest, int *oldVal, int newVal) 
{ 
    do atomically { 
    if (*oldVal == *dest) { 
     *dest = newVal; 
     return true; 
    } else { 
     *oldVal = *dest; 
     return false; 
    } 
    } 
} 

Bằng cách này, trong mã của bạn, bạn cần phải đảm bảo rằng v được liên kết đến 64 bit - nếu không nó có thể được phân chia giữa hai dòng bộ nhớ cache và các cmpxchg8b hướng dẫn sẽ không được thực hiện một cách nguyên tử. Bạn có thể sử dụng số __attribute__((aligned(8))) của GCC cho việc này.

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